Please show off what crt shaders can do!

I took the liberty to mod your ntsc shader to add some of my tests and suggestions, hopefully I didn’t make a mess. Also commented the color correction block to compare signal bandwidth only. I’m not sure if I want to add YCbCr to it. By the way I read that composite has a brightness roll off of 3 dB, I think that along the pedestal (black level) could be the origin of TV levels and PC levels. I think I should multiply it by 0.707.

#version 450

layout(push_constant) uniform Push
{
	vec4 SourceSize;
	vec4 OriginalSize;
	vec4 OutputSize;
	uint FrameCount;
	float SPLIT;
	float I_RES;
	float Q_RES;
	float I_SHIFT;
	float Q_SHIFT;
	float Y_MUL;
	float I_MUL;
	float Q_MUL;
} params;

// Suggestions:
// I: 1.3   Q: 0.4 (for FCC NTSC analogue standard -old-)
// I: 1.0   Q: 1.0 (for FCC NTSC standard 4:1:1)
// I: 2.0   Q: 2.0 (for FCC NTSC S-Video 4:2:2)

#pragma parameter SPLIT "Split" 0.0 -1.0 1.0 0.1
#pragma parameter I_RES "I Mhz" 1.3 0.4 4.0 0.05
#pragma parameter Q_RES "Q Mhz" 0.4 0.4 4.0 0.05
#pragma parameter I_SHIFT "I Shift" 0.0 -1.0 1.0 0.02
#pragma parameter Q_SHIFT "Q Shift" 0.0 -1.0 1.0 0.02
#pragma parameter Y_MUL "Y Multiplier" 1.0 0.0 2.0 0.1
#pragma parameter I_MUL "I Multiplier" 1.0 0.0 2.0 0.1
#pragma parameter Q_MUL "Q Multiplier" 1.0 0.0 2.0 0.1

layout(std140, set = 0, binding = 0) uniform UBO
{
	mat4 MVP;
} global;

#pragma stage vertex
layout(location = 0) in vec4 Position;
layout(location = 1) in vec2 TexCoord;
layout(location = 0) out vec2 vTexCoord;

void main()
{
   gl_Position = global.MVP * Position;
   vTexCoord = TexCoord;
}

#pragma stage fragment
layout(location = 0) in vec2 vTexCoord;
layout(location = 0) out vec4 FragColor;
layout(set = 0, binding = 2) uniform sampler2D Source;

vec4 RGB_YIQ(vec4 col)
{
	mat3 conv_mat = mat3(
 	0.299996928307425,  0.590001575542717,  0.110001496149858,
 	0.599002392519453, -0.277301256521204, -0.321701135998249,
 	0.213001700342824, -0.52510120528935,  0.312099504946526);

	col.rgb *= conv_mat;

	return col;
}

vec4 YIQ_RGB(vec4 col)
{
	mat3 conv_mat = mat3(
 	1.0,  0.946882217090069,  0.623556581986143,
 	1.0, -0.274787646298978, -0.635691079187380,
 	1.0, -1.108545034642030,  1.709006928406470);

	col.rgb *= conv_mat;

	return col;
}

// to Studio Swing (in YIQ space) (for footroom and headroom)
vec4 PCtoTV(vec4 col)
{
   col *= 255;
   col.x = ((col.x * 219) / 255) + 16;
   col.y = (((col.y - 128) * 224) / 255) + 112;
   col.z = (((col.z - 128) * 224) / 255) + 112;
   return vec4(col.xyz, 1.0) / 255;
}


// to Full Swing (in YIQ space)
vec4 TVtoPC(vec4 col)
{
   col *= 255;
   float colx = ((col.x - 16) / 219) * 255;
   float coly = (((col.y - 112) / 224) * 255) + 128;
   float colz = (((col.z - 112) / 224) * 255) + 128;
   return vec4(colx,coly,colz, 1.0) / 255;
}


void main()
{
	#define ms *pow(10.0, -9.0)
	#define MHz *pow(10.0, 9.0);

	const float max_col_res_I = (params.I_RES / 2.0) * 52.6 ms * 315.0/88.0 MHz;
	const float max_col_res_Q = (params.Q_RES / 2.0) * 52.6 ms * 315.0/88.0 MHz;
	const float max_lum_res = 52.6 ms * 315.0/88.0 MHz;

	const int viewport_col_resy = int(ceil((params.OutputSize.x / params.OriginalSize.x) * (params.OriginalSize.x / max_col_res_I)));
	const int viewport_col_resz = int(ceil((params.OutputSize.x / params.OriginalSize.x) * (params.OriginalSize.x / max_col_res_Q)));
	const int viewport_lum_res =  int(ceil((params.OutputSize.x / params.OriginalSize.x) * (params.OriginalSize.x / max_lum_res)));

	if(vTexCoord.x - params.SPLIT - 1.0 > 0.0 || vTexCoord.x - params.SPLIT < 0.0)
	{
		FragColor = vec4(texture(Source, vTexCoord).rgb, 1.0);
	}
	else
	{
		vec4 col = vec4(0.0, 0.0, 0.0, 1.0);

		col += RGB_YIQ(texture(Source, vTexCoord));

		for(int i = 1; i < viewport_col_resy; i++)
		{
			col.y += RGB_YIQ(texture(Source, vTexCoord - vec2((i - viewport_col_resy/2) * params.OutputSize.z, 0.0))).y;
		}
		for(int i = 1; i < viewport_col_resz; i++)
		{
			col.z += RGB_YIQ(texture(Source, vTexCoord - vec2((i - viewport_col_resz/2) * params.OutputSize.z, 0.0))).z;
		}
		for(int i = 1; i < viewport_lum_res; i++)
		{
			col.x += RGB_YIQ(texture(Source, vTexCoord - vec2((i - viewport_col_resy/2) * params.OutputSize.z, 0.0))).x;
		}

		col.y /= viewport_col_resy;
		col.z /= viewport_col_resz;
		col.x /= viewport_lum_res;
		col = PCtoTV(col);


//		col.y = mod((col.y + 1.0) + params.I_SHIFT, 2.0) - 1.0;
//		col.y = 0.9 * col.y + 0.1 * col.y * col.x;
//
//		col.z = mod((col.z + 1.0) + params.Q_SHIFT, 2.0) - 1.0;
//		col.z = 0.4 * col.z + 0.6 * col.z * col.x;
//		col.x += 0.5*col.y;
//
//		col.z *= params.Q_MUL;
//		col.y *= params.I_MUL;
//		col.x *= params.Y_MUL;

		col = clamp(col,vec4(0.0,-0.5957,-0.5226,0.0),vec4(1.0,0.5957,0.5226,1.0));
	   
		FragColor = YIQ_RGB(TVtoPC(col));
	}
}
3 Likes

Nah you can do with it as you like. I forgot to mention it, but right now it’s experimental, purely for fun and released under the GPL license :blush: have fun!

Edit: and besides I have a job interview on Tuesday for a .NET /Cobol dev job so if things go well I’ll have less free time for RA shaders anyway. Unless I can get paid for this gig? :laughing:

3 Likes

Hmm yeah I don’t get it, lol. It’s an improvement, though! For some reason there’s still some weird stuff going on between the pixels- it should be 1:1 mapping with no pixels being cutoff or stuff between the pixels. Only things I can think of is that there’s some kind of glow/halation effect or some kind of scaling/filtering being done other than nearest neighbor. The mask is also disappearing over bright colors/white. If it’s working as intended, one white pixel should look exactly like this:

Edit: I think this may just be how the blend overlay shader works, IIRC. Unfortunately I’m not sure if there’s a way to address it.

test1

1 Like

I think that’s just how that overlay shader works.

1 Like

Lol, yep. Just realized that too. That would explain the mask disappearing over bright colors, and possibly also explain the weird stuff that’s occasionally between pixels.

Do you know of an easy change to the code for this? Seems like it’s currently decreasing opacity as color intensity increases, and instead it should just match the color intensity of the mask colors to the intensity of the pixel color (I think).

1 Like

What you’re describing is just a straight multiplication, which is easy enough to do but it’s also easy to put into the subpixel_mask function:

/*
A collection of CRT mask effects that work with LCD subpixel structures for
small details

author: hunterk
license: public domain

How to use it:

Multiply your image by the vec3 output:
FragColor.rgb *= mask_weights(gl_FragCoord.xy, 1.0, 1);

The function needs to be tiled across the screen using the physical pixels, e.g.
gl_FragCoord (the "vec2 coord" input). In the case of slang shaders, we use
(vTexCoord.st * OutputSize.xy).

The "mask_intensity" (float value between 0.0 and 1.0) is how strong the mask
effect should be. Full-strength red, green and blue subpixels on a white pixel
are the ideal, and are achieved with an intensity of 1.0, though this darkens
the image significantly and may not always be desirable.

The "phosphor_layout" (int value between 0 and 19) determines which phophor
layout to apply. 0 is no mask/passthru.

Many of these mask arrays are adapted from cgwg's crt-geom-deluxe LUTs, and
those have their filenames included for easy identification
*/

vec3 mask_weights(vec2 coord, float mask_intensity, int phosphor_layout){
   vec3 weights = vec3(1.,1.,1.);
   float on = 1.;
   float off = 1.-mask_intensity;
   vec3 red     = vec3(on,  off, off);
   vec3 green   = vec3(off, on,  off);
   vec3 blue    = vec3(off, off, on );
   vec3 magenta = vec3(on,  off, on );
   vec3 yellow  = vec3(on,  on,  off);
   vec3 cyan    = vec3(off, on,  on );
   vec3 black   = vec3(off, off, off);
   int w, z = 0;
   
   // This pattern is used by a few layouts, so we'll define it here
   vec3 aperture_weights = mix(magenta, green, floor(mod(coord.x, 2.0)));
   
   if(phosphor_layout == 0) return weights;

   else if(phosphor_layout == 1){
      // classic aperture for RGB panels; good for 1080p, too small for 4K+
      // aka aperture_1_2_bgr
      weights  = aperture_weights;
      return weights;
   }

   else if(phosphor_layout == 2){
      // 2x2 shadow mask for RGB panels; good for 1080p, too small for 4K+
      // aka delta_1_2x1_bgr
      vec3 inverse_aperture = mix(green, magenta, floor(mod(coord.x, 2.0)));
      weights               = mix(aperture_weights, inverse_aperture, floor(mod(coord.y, 2.0)));
      return weights;
   }

   else if(phosphor_layout == 3){
      // slot mask for RGB panels; looks okay at 1080p, looks better at 4K
      vec3 slotmask[3][4] = {
         {magenta, green, black,   black},
         {magenta, green, magenta, green},
         {black,   black, magenta, green}
      };
      
      // find the vertical index
      w = int(floor(mod(coord.y, 3.0)));

      // find the horizontal index
      z = int(floor(mod(coord.x, 4.0)));

      // use the indexes to find which color to apply to the current pixel
      weights = slotmask[w][z];
      return weights;
   }

   else if(phosphor_layout == 4){
      // classic aperture for RBG panels; good for 1080p, too small for 4K+
      weights  = mix(yellow, blue, floor(mod(coord.x, 2.0)));
      return weights;
   }

   else if(phosphor_layout == 5){
      // 2x2 shadow mask for RBG panels; good for 1080p, too small for 4K+
      vec3 inverse_aperture = mix(blue, yellow, floor(mod(coord.x, 2.0)));
      weights               = mix(mix(yellow, blue, floor(mod(coord.x, 2.0))), inverse_aperture, floor(mod(coord.y, 2.0)));
      return weights;
   }
   
   else if(phosphor_layout == 6){
      // aperture_1_4_rgb; good for simulating lower 
      vec3 ap4[4] = vec3[](red, green, blue, black);
      
      z = int(floor(mod(coord.x, 4.0)));
      
      weights = ap4[z];
      return weights;
   }
   
   else if(phosphor_layout == 7){
      // aperture_2_5_bgr
      vec3 ap3[5] = vec3[](red, magenta, blue, green, green);
      
      z = int(floor(mod(coord.x, 5.0)));
      
      weights = ap3[z];
      return weights;
   }
   
   else if(phosphor_layout == 8){
      // aperture_3_6_rgb
      
      vec3 big_ap[7] = vec3[](red, red, yellow, green, cyan, blue, blue);
      
      w = int(floor(mod(coord.x, 7.)));
      
      weights = big_ap[w];
      return weights;
   }
   
   else if(phosphor_layout == 9){
      // reduced TVL aperture for RGB panels
      // aperture_2_4_rgb
      
      vec3 big_ap_rgb[4] = vec3[](red, yellow, cyan, blue);
      
      w = int(floor(mod(coord.x, 4.)));
      
      weights = big_ap_rgb[w];
      return weights;
   }
   
   else if(phosphor_layout == 10){
      // reduced TVL aperture for RBG panels
      
      vec3 big_ap_rbg[4] = vec3[](red, magenta, cyan, green);
      
      w = int(floor(mod(coord.x, 4.)));
      
      weights = big_ap_rbg[w];
      return weights;
   }
   
   else if(phosphor_layout == 11){
      // delta_1_4x1_rgb; dunno why this is called 4x1 when it's obviously 4x2 /shrug
      vec3 delta1[2][4] = {
         {red,  green, blue, black},
         {blue, black, red,  green}
      };
      
      w = int(floor(mod(coord.y, 2.0)));
      z = int(floor(mod(coord.x, 4.0)));
      
      weights = delta1[w][z];
      return weights;
   }
   
   else if(phosphor_layout == 12){
      // delta_2_4x1_rgb
      vec3 delta[2][4] = {
         {red, yellow, cyan, blue},
         {cyan, blue, red, yellow}
      };
      
      w = int(floor(mod(coord.y, 2.0)));
      z = int(floor(mod(coord.x, 4.0)));
      
      weights = delta[w][z];
      return weights;
   }
   
   else if(phosphor_layout == 13){
      // delta_2_4x2_rgb
      vec3 delta[4][4] = {
         {red,  yellow, cyan, blue},
         {red,  yellow, cyan, blue},
         {cyan, blue,   red,  yellow},
         {cyan, blue,   red,  yellow}
      };
      
      w = int(floor(mod(coord.y, 4.0)));
      z = int(floor(mod(coord.x, 4.0)));
      
      weights = delta[w][z];
      return weights;
   }

   else if(phosphor_layout == 14){
      // slot mask for RGB panels; too low-pitch for 1080p, looks okay at 4K, but wants 8K+
      vec3 slotmask[3][6] = {
         {magenta, green, black, black,   black, black},
         {magenta, green, black, magenta, green, black},
         {black,   black, black, magenta, green, black}
      };
      
      w = int(floor(mod(coord.y, 3.0)));

      z = int(floor(mod(coord.x, 6.0)));

      weights = slotmask[w][z];
      return weights;
   }
   
   else if(phosphor_layout == 15){
      // slot_2_4x4_rgb
      vec3 slot2[4][8] = {
         {red,   yellow, cyan,  blue,  red,   yellow, cyan,  blue },
         {red,   yellow, cyan,  blue,  black, black,  black, black},
         {red,   yellow, cyan,  blue,  red,   yellow, cyan,  blue },
         {black, black,  black, black, red,   yellow, cyan,  blue }
      };
   
      w = int(floor(mod(coord.y, 4.0)));
      z = int(floor(mod(coord.x, 8.0)));
      
      weights = slot2[w][z];
      return weights;
   }

   else if(phosphor_layout == 16){
      // slot mask for RBG panels; too low-pitch for 1080p, looks okay at 4K, but wants 8K+
      vec3 slotmask[3][4] = {
         {yellow, blue,  black,  black},
         {yellow, blue,  yellow, blue},
         {black,  black, yellow, blue}
      };
      
      w = int(floor(mod(coord.y, 3.0)));

      z = int(floor(mod(coord.x, 4.0)));

      weights = slotmask[w][z];
      return weights;
   }
   
   else if(phosphor_layout == 17){
      // slot_2_5x4_bgr
      vec3 slot2[4][10] = {
         {red,   magenta, blue,  green, green, red,   magenta, blue,  green, green},
         {black, blue,    blue,  green, green, red,   red,     black, black, black},
         {red,   magenta, blue,  green, green, red,   magenta, blue,  green, green},
         {red,   red,     black, black, black, black, blue,    blue,  green, green}
      };
   
      w = int(floor(mod(coord.y, 4.0)));
      z = int(floor(mod(coord.x, 10.0)));
      
      weights = slot2[w][z];
      return weights;
   }
   
   else if(phosphor_layout == 18){
      // same as above but for RBG panels
      vec3 slot2[4][10] = {
         {red,   yellow, green, blue,  blue,  red,   yellow, green, blue,  blue },
         {black, green,  green, blue,  blue,  red,   red,    black, black, black},
         {red,   yellow, green, blue,  blue,  red,   yellow, green, blue,  blue },
         {red,   red,    black, black, black, black, green,  green, blue,  blue }
      };
   
      w = int(floor(mod(coord.y, 4.0)));
      z = int(floor(mod(coord.x, 10.0)));
      
      weights = slot2[w][z];
      return weights;
   }
   
   else if(phosphor_layout == 19){
      // slot_3_7x6_rgb
      vec3 slot[6][14] = {
         {red,   red,   yellow, green, cyan,  blue,  blue,  red,   red,   yellow, green,  cyan,  blue,  blue},
         {red,   red,   yellow, green, cyan,  blue,  blue,  red,   red,   yellow, green,  cyan,  blue,  blue},
         {red,   red,   yellow, green, cyan,  blue,  blue,  black, black, black,  black,  black, black, black},
         {red,   red,   yellow, green, cyan,  blue,  blue,  red,   red,   yellow, green,  cyan,  blue,  blue},
         {red,   red,   yellow, green, cyan,  blue,  blue,  red,   red,   yellow, green,  cyan,  blue,  blue},
         {black, black, black,  black, black, black, black, black, red,   red,    yellow, green, cyan,  blue}
      };
      
      w = int(floor(mod(coord.y, 6.0)));
      z = int(floor(mod(coord.x, 14.0)));
      
      weights = slot[w][z];
      return weights;
   }
   
   else if(phosphor_layout == 20){
      vec3 lcd4x[4][4] = {
         {red,   green, blue,  black},
         {red,   green, blue,  black},
         {red,   green, blue,  black},
         {black, black, black, black}
      };
   
      w = int(floor(mod(coord.y, 4.0)));
      z = int(floor(mod(coord.x, 4.0)));
      
      weights = lcd4x[w][z];
      return weights;
   }

   else return weights;
}

This is the new bit:

else if(phosphor_layout == 20){
      vec3 lcd4x[4][4] = {
         {red,   green, blue,  black},
         {red,   green, blue,  black},
         {red,   green, blue,  black},
         {black, black, black, black}
      };
   
      w = int(floor(mod(coord.y, 4.0)));
      z = int(floor(mod(coord.x, 4.0)));
      
      weights = lcd4x[w][z];
      return weights;
   }
3 Likes

Now that I think about it, that might not look great/as intended… :confused:

It might be better to just use 100% black and just lower the strength of the mask as a whole.

1 Like

Personally I like the 50% black look.

1 Like

I mean it should be technically possible, I’d assume.

I mean Android is open source (I think, it’s just Google play framework nonsense that’s closed source).

I’m not very clear on how dosbox works and everything, but I assume dosbox is a VM of dos.(Probably with a bunch of specific things being done to make it does friendly.)

So theoretically we could have a VM of Android in a similar vein (I imagine alot of optimizations would have to be done to VM it properly), where it runs apk’s as content for the system.

If I’m way off base on this please correct me, as I have ZERO knowledge about dosbox or anything relevant for this, just some musings.

2 Likes

I just think it’d be much more likely for RA to get a Chromium core than an Android core. It would also feel right just like the PS4 has a browser, and Steam has a browser IMO :thinking:

2 Likes

Another test with subpixel mask provided by @hunterk.

Shadow Mask Strength - 0.5, Scanline Weight - 0.5 (to disable it)

Looks way better than just an overlay mask!

3 Likes

I’m in the same boat as you, just an end user, with little to no knowledge of how any of this works. Might as well be magic for all I know.

2 Likes

It was just guess work on my part. I’ll even be fine if RA could directly stream from any of the streaming services as long as I can use CRT shaders with them.

2 Likes

Soooo, I just came to a horrible realization.

Started to convert some of my disc based game libraries to chd for the space saving.

And… My Dreamcast and Saturn collection are not in the correct formats for this. (Dreamcast is in in cdi, which from my research completely shite. My Saturn library’s issue is none of the games have cue files so chdman is like nah you can’t do that.)

Already have solutions for both of those libraries with the exception of my indie collection for Dreamcast, guess I’m just going to sacrifice the 10 or so GB for it.

Just finished fixing my PC-Engine CD collection, wasn’t that big so it wasn’t that much of an issue. And thankfully I don’t have to fix my ps1 collection, I would’ve cried. That shit would’ve taken forever. Also my Sega CD collection was oddly alright too.

Sorry to go completely off topic, I just wanted to share my struggle, lol.

EDIT: Also a heads-up for anyone planning on converting their disc based games, in the case of Dreamcast games, to be able convert to chd you need to convert from the gdi file not the cue flycast will refuse to boot the chd for it if it’s converted from the chd regardless of whether the game runs perfectly fine as a gdi file. Had to Google-fu my way to that answer, every other system I tried to convert games for worked fine converting from cue, except Dreamcast.

PlayStation 1, Sega/Mega CD, PC-Engine CD/TurboGrafx CD, Saturn all seem to work fine converting from cue to chd for the cores I’m running. (Beetle PSX HW, Beetle Saturn, Genesis Plus GX, Flycast, Beetle PCE)

Also thanks for the love @guest.r and @Doriphor !

EDIT2: Cleaned up some explanations as it was hard to read, still is but about to die will fix later, lol.

3 Likes

Looks good. You can increase mask strength further to make the mask more accurate. Using grey in lieu of black helps keep highlights bright, but the trade-off is that it looks kinda weird in some situations.

Another thing to try would be something like this. I make no guarantees though :stuck_out_tongue:

image

2 Likes

Make sure you’re using at least ChdMan v5. I made the mistake of doing my entire Dreamcast library in v4 and now I have shoddy rips that I can’t convert back to gdi or use in ReDream

4 Likes

@guest.r

Just noticed that I’m getting some weird randomly doubled lines with guest-dr-venom-ntsc-stock.

Also, is there a way to blend dithering even further, like how GTU or TVout tweaks does it? Or is there a limitation based on the type of filtering being used?

edit: more weird stuff

3 Likes

the stock preset sets the first pass to 240 px height, so if you’re feeding it a 224 px image, it’ll get stretched up, which will double a few lines. For low-res content, you should use the regular preset.

That ‘stock’ preset should probably be renamed to “240p-downsample” or something more informative.

5 Likes

Thanks, just checked. I’m running v5, don’t use redream but it’s nice to know nonetheless.

1 Like

Purple does look better than 100% or 50% black to me. Not sure what is going on with the bottom 4 pixel though, they just look similar to black, maybe its the overlay shader?

1 Like