Please show off what crt shaders can do!

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

Yeah that overlay shader looks really strange to me, like you’re just slapping a transparent mask on top of the image… not the biggest fan of how it works. So far, the best example you posted was the one using the code supplied by hunterk here.

I think you’ll get better results using that and substituting the new pattern for the old one.

Edit: as far as the rows of black pixels at the top/bottom, that’s to be expected because the mask tiles to 1080 pixels tall. Doesn’t matter though because those pixels just get cropped off anyway (on a 1080p display).

1 Like

@Nesguy

Wouldn’t that require more code to be written, as isn’t that mask @hunterk made different from the new mask pattern you’re suggestion?

1 Like

Yes, you’d need to take the code supplied by hunterk and substitute the new mask colors for the old ones, although I’m not quite sure how to do that since all the colors used in the code seem to be 100% values (how do you do red @ 85/255, for example?)

1 Like

You would just have to make some new color lines at the top. As it is, it has e.g. vec3 red = vec3(on, off, off); where on == 1.0 and off == 0.0. You could always add, e.g. vec3 purple = vec3(0.2, 0.0, 0.2); or vec3 brown = vec3(0.35, 0.15, 0.0); (since brown is really just dark orange).

So, just make those color definitions and then plug them into the array.

4 Likes

I should have been clear. I meant the bottom four pixels of your mask appearing black and not the respective colors they were meant to be.

Another test with this code. Thanks again to @hunterk.

else if(phosphor_layout == 21){
  vec3 lcd4x[4][4] = {
     {red,    green,  blue,   purple},
     {red,    green,  blue,   purple},
     {red,    green,  blue,   purple},
     {purple, purple, purple, purple}};

  w = int(floor(mod(coord.y, 4.0)));
  z = int(floor(mod(coord.x, 4.0)));
  
  weights = lcd4x[w][z];
  return weights;}

2 Likes