@@ -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+
181214sg_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
452503void 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 ();
0 commit comments