Skip to content

Commit 7e9af16

Browse files
committed
Add CRT post-process effect with UI controls and shader integration
1 parent 22d1954 commit 7e9af16

11 files changed

Lines changed: 306 additions & 16 deletions

File tree

src/args.c

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const char* app_releases_address = "https://github.com/X65/emu/releases";
1212
const char full_name[] = FULL_NAME;
1313

1414
struct arguments arguments = {
15-
NULL, "-", false, false, false, false, false, NULL,
15+
NULL, "-", false, false, false, false, false, false, NULL,
1616
};
1717
static char args_doc[] = "[ROM.xex]";
1818

@@ -27,6 +27,7 @@ static struct argp_option options[] = {
2727
{ "zero-mem", 'z', 0, 0, "Fill memory with zeros" },
2828
{ "dap", 'd', 0, 0, "Enable Debug Adapter Protocol over stdin/stdout" },
2929
{ "dap-port", 'p', "PORT", 0, "Enable Debug Adapter Protocol over TCP port" },
30+
{ "crt", 'c', 0, 0, "Enable CRT post-process effect" },
3031
{ 0 }
3132
};
3233

@@ -42,6 +43,7 @@ static error_t parse_opt(int key, char* arg, struct argp_state* argp_state) {
4243
case 'o': args->output_file = arg; break;
4344
case 'd': args->dap = true; break;
4445
case 'p': args->dap_port = arg; break;
46+
case 'c': args->crt = true; break;
4547

4648
case 'l': app_load_labels(arg, false); break;
4749

@@ -74,4 +76,7 @@ void args_parse(int argc, char* argv[]) {
7476
if (sargs_exists("file")) {
7577
arguments.rom = sargs_value("file");
7678
}
79+
if (sargs_exists("crt")) {
80+
arguments.crt = true;
81+
}
7782
}

src/args.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ extern char program_version[];
1212
extern struct arguments {
1313
const char* rom;
1414
const char* output_file;
15-
bool silent, verbose, zeromem, joy, dap;
15+
bool silent, verbose, zeromem, joy, dap, crt;
1616
const char* dap_port;
1717
} arguments;
1818

src/common/gfx.c

Lines changed: 82 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,11 @@ typedef struct {
4545
struct {
4646
sg_buffer vbuf;
4747
sg_pipeline pip;
48+
sg_pipeline pip_crt;
4849
sg_pass_action pass_action;
4950
bool portrait;
51+
bool crt_enabled;
52+
gfx_crt_params_t crt_params;
5053
} display;
5154
struct {
5255
sg_view tex_view;
@@ -178,6 +181,36 @@ chips_dim_t gfx_pixel_aspect(void) {
178181
return state.offscreen.pixel_aspect;
179182
}
180183

184+
gfx_crt_params_t gfx_crt_default_params(void) {
185+
return (gfx_crt_params_t){
186+
.scanline_intensity = 0.25f,
187+
.mask_intensity = 0.30f,
188+
.curvature = 0.10f,
189+
.gamma = 1.10f,
190+
.vignette = 0.25f,
191+
};
192+
}
193+
194+
bool gfx_crt_enabled(void) {
195+
assert(state.valid);
196+
return state.display.crt_enabled;
197+
}
198+
199+
void gfx_crt_set_enabled(bool enabled) {
200+
assert(state.valid);
201+
state.display.crt_enabled = enabled;
202+
}
203+
204+
gfx_crt_params_t gfx_crt_get_params(void) {
205+
assert(state.valid);
206+
return state.display.crt_params;
207+
}
208+
209+
void gfx_crt_set_params(gfx_crt_params_t params) {
210+
assert(state.valid);
211+
state.display.crt_params = params;
212+
}
213+
181214
sg_view gfx_create_icon_texview(const uint8_t* packed_pixels, int width, int height, int stride, const char* label) {
182215
const size_t pixel_data_size = width * height * sizeof(uint32_t);
183216
uint32_t* pixels = malloc(pixel_data_size);
@@ -380,6 +413,19 @@ void gfx_init(const gfx_desc_t* desc) {
380413
.primitive_type = SG_PRIMITIVETYPE_TRIANGLE_STRIP
381414
});
382415

416+
state.display.pip_crt = sg_make_pipeline(&(sg_pipeline_desc){
417+
.shader = sg_make_shader(display_crt_shader_desc(sg_query_backend())),
418+
.layout = {
419+
.attrs = {
420+
[0].format = SG_VERTEXFORMAT_FLOAT2,
421+
[1].format = SG_VERTEXFORMAT_FLOAT2
422+
}
423+
},
424+
.primitive_type = SG_PRIMITIVETYPE_TRIANGLE_STRIP
425+
});
426+
state.display.crt_enabled = false;
427+
state.display.crt_params = gfx_crt_default_params();
428+
383429
// create an unpacked speaker icon image and sokol-gl pipeline
384430
{
385431
// textures must be 2^n for WebGL
@@ -419,9 +465,10 @@ void gfx_init(const gfx_desc_t* desc) {
419465

420466
/* apply a viewport rectangle to preserve the emulator's aspect ratio,
421467
and for 'portrait' orientations, keep the emulator display at the
422-
top, to make room at the bottom for mobile virtual keyboard
468+
top, to make room at the bottom for mobile virtual keyboard.
469+
Returns the applied viewport rect (in framebuffer pixels).
423470
*/
424-
static void apply_viewport(chips_dim_t canvas, chips_rect_t view, chips_dim_t pixel_aspect, gfx_border_t border) {
471+
static chips_rect_t apply_viewport(chips_dim_t canvas, chips_rect_t view, chips_dim_t pixel_aspect, gfx_border_t border) {
425472
float cw = (float) (canvas.width - border.left - border.right);
426473
if (cw < 1.0f) {
427474
cw = 1.0f;
@@ -447,6 +494,10 @@ static void apply_viewport(chips_dim_t canvas, chips_rect_t view, chips_dim_t pi
447494
vp_y = border.top + (ch - vp_h) * 0.5f;
448495
}
449496
sg_apply_viewportf(vp_x, vp_y, vp_w, vp_h, true);
497+
return (chips_rect_t){
498+
.x = (int)vp_x, .y = (int)vp_y,
499+
.width = (int)vp_w, .height = (int)vp_h,
500+
};
450501
}
451502

452503
void gfx_draw(chips_display_info_t display_info) {
@@ -538,13 +589,35 @@ void gfx_draw(chips_display_info_t display_info) {
538589
.action = state.display.pass_action,
539590
.swapchain = sglue_swapchain()
540591
});
541-
apply_viewport(display, display_info.screen, state.offscreen.pixel_aspect, state.border);
542-
sg_apply_pipeline(state.display.pip);
543-
sg_apply_bindings(&(sg_bindings){
544-
.vertex_buffers[0] = state.display.vbuf,
545-
.views[VIEW_tex] = state.offscreen.tex_view,
546-
.samplers[SMP_smp] = state.offscreen.smp,
547-
});
592+
const chips_rect_t vp = apply_viewport(display, display_info.screen, state.offscreen.pixel_aspect, state.border);
593+
if (state.display.crt_enabled) {
594+
sg_apply_pipeline(state.display.pip_crt);
595+
sg_apply_bindings(&(sg_bindings){
596+
.vertex_buffers[0] = state.display.vbuf,
597+
.views[VIEW_crt_tex] = state.offscreen.tex_view,
598+
.samplers[SMP_crt_smp] = state.offscreen.smp,
599+
});
600+
// fade out the shadow mask when the output is too small to resolve
601+
// a clean 3-pixel-wide triad (avoids ugly moire at low scales)
602+
const float mask_scale = (vp.height >= 2 * display_info.screen.height) ? 1.0f
603+
: (float)vp.height / (float)(2 * display_info.screen.height);
604+
const display_crt_fs_params_t crt_uniforms = {
605+
.output_size = { (float)vp.width, (float)vp.height },
606+
.scanline_intensity = state.display.crt_params.scanline_intensity,
607+
.mask_intensity = state.display.crt_params.mask_intensity * mask_scale,
608+
.curvature = state.display.crt_params.curvature,
609+
.gamma = state.display.crt_params.gamma,
610+
.vignette = state.display.crt_params.vignette,
611+
};
612+
sg_apply_uniforms(UB_display_crt_fs_params, &SG_RANGE(crt_uniforms));
613+
} else {
614+
sg_apply_pipeline(state.display.pip);
615+
sg_apply_bindings(&(sg_bindings){
616+
.vertex_buffers[0] = state.display.vbuf,
617+
.views[VIEW_tex] = state.offscreen.tex_view,
618+
.samplers[SMP_smp] = state.offscreen.smp,
619+
});
620+
}
548621
sg_draw(0, 4, 1);
549622
sg_apply_viewport(0, 0, display.width, display.height, true);
550623
sdtx_draw();

src/common/gfx.h

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
#pragma once
22
/*
33
Common graphics functions for the chips-test example emulators.
4-
5-
REMINDER: consider using this CRT shader?
6-
7-
https://github.com/mattiasgustavsson/rebasic/blob/master/source/libs/crtemu.h
84
*/
95
#include <stdint.h>
106
#include <stdbool.h>
@@ -38,6 +34,14 @@ typedef struct {
3834
gfx_draw_extra_t draw_extra_cb;
3935
} gfx_desc_t;
4036

37+
typedef struct {
38+
float scanline_intensity;
39+
float mask_intensity;
40+
float curvature;
41+
float gamma;
42+
float vignette;
43+
} gfx_crt_params_t;
44+
4145
void gfx_init(const gfx_desc_t* desc);
4246
void gfx_draw(chips_display_info_t display_info);
4347
void gfx_shutdown(void);
@@ -47,6 +51,12 @@ void gfx_disable_speaker_icon(void);
4751
chips_dim_t gfx_pixel_aspect(void);
4852
sg_view gfx_create_icon_texview(const uint8_t* packed_pixels, int width, int height, int stride, const char* label);
4953

54+
bool gfx_crt_enabled(void);
55+
void gfx_crt_set_enabled(bool enabled);
56+
gfx_crt_params_t gfx_crt_get_params(void);
57+
void gfx_crt_set_params(gfx_crt_params_t params);
58+
gfx_crt_params_t gfx_crt_default_params(void);
59+
5060
#ifdef __cplusplus
5161
} /* extern "C" */
5262
#endif

src/common/shaders.glsl

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,58 @@ void main() {
5858
}
5959
@end
6060

61+
// CRT post-process display shader (parallel to display_fs)
62+
@fs display_crt_fs
63+
layout(binding=0) uniform texture2D crt_tex;
64+
layout(binding=0) uniform sampler crt_smp;
65+
layout(binding=0) uniform display_crt_fs_params {
66+
vec2 output_size;
67+
float scanline_intensity;
68+
float mask_intensity;
69+
float curvature;
70+
float gamma;
71+
float vignette;
72+
float _pad0;
73+
float _pad1;
74+
};
75+
in vec2 uv;
76+
out vec4 frag_color;
77+
78+
vec2 crt_warp(vec2 p) {
79+
vec2 c = p * 2.0 - 1.0;
80+
c *= vec2(1.0 + (c.y * c.y) * curvature * 0.10,
81+
1.0 + (c.x * c.x) * curvature * 0.12);
82+
return c * 0.5 + 0.5;
83+
}
84+
85+
vec3 crt_mask(vec2 frag_px) {
86+
float m = mod(floor(frag_px.x), 3.0);
87+
vec3 c = vec3(1.0 - mask_intensity);
88+
if (m < 1.0) c.r = 1.0;
89+
else if (m < 2.0) c.g = 1.0;
90+
else c.b = 1.0;
91+
return c;
92+
}
93+
94+
void main() {
95+
vec2 wuv = crt_warp(uv);
96+
if (wuv.x < 0.0 || wuv.x > 1.0 || wuv.y < 0.0 || wuv.y > 1.0) {
97+
frag_color = vec4(0.0, 0.0, 0.0, 1.0);
98+
return;
99+
}
100+
vec3 col = texture(sampler2D(crt_tex, crt_smp), wuv).rgb;
101+
float scan_phase = wuv.y * output_size.y;
102+
float scan = 1.0 - scanline_intensity * (1.0 - cos(scan_phase * 3.14159265)) * 0.5;
103+
col *= scan;
104+
col *= crt_mask(wuv * output_size);
105+
float d = distance(wuv, vec2(0.5));
106+
col *= mix(1.0, max(0.0, 1.0 - vignette), smoothstep(0.4, 0.85, d));
107+
col = pow(max(col, vec3(0.0)), vec3(1.0 / max(gamma, 0.01)));
108+
frag_color = vec4(col, 1.0);
109+
}
110+
@end
111+
61112
@program offscreen offscreen_vs offscreen_fs
62113
@program offscreen_pal offscreen_vs offscreen_pal_fs
63114
@program display display_vs display_fs
115+
@program display_crt display_vs display_crt_fs

0 commit comments

Comments
 (0)