New CRT shader from Guest + CRT Guest Advanced updates

It’s missing some features…but i’m still working on some things, like better mask mitigation.

Here is an example of some new options:

10 Likes

I recently acquired an HDR1000 4K display and had a chance to test some shaders. So far, guest-advanced is the easiest and nicest experience.

I had to make the following adjustments:

Peak Luminance: 1000 Paper White: 750 Contrast: 5.8x

With these settings I’m matching the brightness of the raw non-HDR image and it passes PLUGE and grey bars.

However, I’m noticing a green(?) tint to grey bars. Any idea where I should start? Can anyone else replicate this? These are the changes I’ve made:

#reference "shaders_slang/crt/crt-guest-advanced.slangp"
glow = "0.000000"
brightboost = "1.100000"
brightboost1 = "1.000000"
gsl = "2.000000"
scanline1 = "8.000000"
scanline2 = "10.000000"
beam_min = "1.000000"
h_sharp = "8.999977"
s_sharp = "0.000000"
shadowMask = "6.000000"
maskstr = "1.000000"
mcut = "1.000000"
masksize = "2.000000"
slotmask = "1.000000"
slotmask1 = "1.000000"
double_slot = "3.000000"
2 Likes

Couldn’t this be due to the display calibration?

Sure, I’ll give it a try when I get a chance. I’m definitely interested in seeing if I can get other shaders to look better using RetroArch’s HDR implementation.

Is this the same Mini LED you got recently?

Anyway, I tried your settings. What worked for me was Peak and Paperwhite Luminance at 630. 6.3 was the lowest I could set the contrast and still barely make out the second to last bar in the grey ramp test and also differentiate between the 2 brightest bars.

While 5.3 might work in a dark room, I kinda settled on 5.7 for now after testing in games. So it was in fact actually pretty simple to get CRT-Guest-Advanced looking great in HDR. All that was needed was a little impetus.

This is just a starting point, imagine what can be done now with all the tools that we have at our disposal?

Concerning this, I noticed that with Expand Colour Gamut On, everything looked slightly cooler and more saturated, while with it off, things looked slightly warmer with less saturation. Whites looked more neutral with it on to me. I didn’t notice any tinting that would cause me to loose any sleep over.

In order to further raise awareness of what we are trying to accomplish, please allow me to share my first configuration override.

CyberLab NESGuy CRT-Guest HDR Calibration.cfg

aspect_ratio_index = "0"
custom_viewport_height = "1344"
custom_viewport_width = "1536"
menu_framebuffer_opacity = "0.000000"
video_aspect_ratio = "1.243300"
video_frame_delay_auto = "true"
video_hdr_display_contrast = "5.699999"
video_hdr_enable = "true"
video_hdr_max_nits = "630.000000"
video_hdr_paper_white_nits = "630.000000"
video_scale_integer = "true"
video_scale_integer_overscale = "true"

Screenies or it didn’t happen

2 Likes

I’m currently testing big changes to mask mitigation techniques, especially halation. I avoid to introduce a new technique / parameter…

Here is an example of my progress so far.

I guess I could make a release version soon, with some changes to positive bloom and both halations.

16 Likes

That looks really good. I want to try it soon. :grin:

3 Likes

New Release Version (2024-03-08-r1):

Notable changes:

  • Re-designed +bloom and halation effects.
  • New option to preserve “contrast” in bloom passes added.
  • Features are still “fresh”, changes possible. :smiley:

Download link:

https://mega.nz/file/9wA1hKxb#lPvrPuhjPYBJhCiSLXBb5p2KUgeqNtmYKJf1-yze6gU

14 Likes

Gave this a quick spin. How exactly does that bloom “contrast” setting function? Playing around with it it seems to function the same way as the setting “fine bloom/halation sampling” that’s above it?

1 Like

It preserves local contrast, altering/preserving only darker colors. Brighter colors are affected with full “bloom radius”, where fine bloom sampling alters bloom radius in general.

1 Like

Yep, the KTC M27P20P, it’s probably one of the best deals available right now.

I imagine these settings are going to differ based on the display’s HDR implementation. So there probably is no correct “default,” it’s just something that has to be fine-tuned.

Paper white can be pushed a bit higher - I like it about 750. It’s actually a bit brighter in highlights compared to the raw image, but who’s to say we can’t push things a little higher if it looks good? I think contrast is good somewhere between 5.6 - 5.8x depending on ambient light.

Yep I think that’s right. Slightly blue and slightly more saturated. So maybe this is just expected.

Overall, very painless and looks great. Just the slight tinting issue w/expanded gamut, which might be normal.

2 Likes

Fyi/for anyone wondering: Expand Colour Gamut Off locks/clamps colors to Rec 709/sRGB primaries.

Expand Colour Gamut On stretches the colors to Expanded 709, a non-standard gamut someone at ?Microsoft? cooked up because they thought Rec 709 color looked desaturated when converted into HDR. It results in oversaturated/skewed reds and greens that push towards DCI P3.

Off is either correct or at least substantially closer to correct depending on the material being displayed (i added all of those other additional gamuts to Megatron for a reason after all.)

2 Likes

I see the difference when I tried it on a few dark areas in different games, bringing it to about 2.00 seemed ok. I’m more or less waiting for the update that will have that mask mitigation changes. Looks and sounds interesting.

2 Likes

I recently played retroarch with this shader (Mega Bezel) via steamVR. I noticed that the moire effect was replicated perfectly like it was on a real CRT. Moving my head even moved the moire effect like a real TV. I wonder why that was? I’m guessing there’s something about supersampling or the pixels on the HMD not aligning perfectly with what’s on screen. Either way, it was cool and authentic.

2 Likes

@guest.r

I ported/translated the MD Palette and SMS Blue Lift functions from @Dogway’s Grade into crt-guest-advanced-ntsc/pre-shaders-afterglow if you would like to include them:

modified pre-shaders-afterglow.slang
#version 450

/*
   CRT Advanced Afterglow, color altering
   
   Copyright (C) 2019-2022 guest(r) and Dr. Venom
   
   This program is free software; you can redistribute it and/or
   modify it under the terms of the GNU General Public License
   as published by the Free Software Foundation; either version 2
   of the License, or (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
   
*/  

layout(push_constant) uniform Push
{
   vec4 SourceSize;
   vec4 OriginalSize;
   vec4 OutputSize;
   uint FrameCount;
   float TNTC;
   float LS;
   float LUTLOW, LUTBR;   
   float CP, CS;
   float WP;
   float wp_saturation;
   float AS, sat;
   float BP;
   float vigstr;
   float vigdef;
   float sega_fix;
   float sms_blue;
   float MD_Pal;
   float pre_bb;
   float contr;
} params;

#pragma parameter AS "          Afterglow Strength" 0.20 0.0 0.60 0.01
#define AS params.AS

#pragma parameter sat "          Afterglow saturation" 0.50 0.0 1.0 0.01
#define sat params.sat

#pragma parameter bogus_color "[ COLOR TWEAKS ]:" 0.0 0.0 1.0 1.0

#pragma parameter CS "          Display Gamut: sRGB, Modern, DCI, Adobe, Rec.2020" 0.0 0.0 4.0 1.0 

#pragma parameter CP "          CRT Profile: EBU | P22 | SMPTE-C | Philips | Trin." 0.0 -1.0 5.0 1.0 

#define CP params.CP
#define CS params.CS

#pragma parameter TNTC "          LUT Colors: Trin.1 | Trin.2 | Nec Mult. | NTSC" 0.0 0.0 4.0 1.0
#define TNTC params.TNTC

#pragma parameter LS "          LUT Size" 32.0 16.0 64.0 16.0
#define LS params.LS

#define LUTLOW 5.0  // "Fix LUT Dark - Range" from 0.0 to 50.0 - RGB singletons

#define LUTBR 1.0   // "Fix LUT Brightness" from 0.0 to 1.0

#pragma parameter WP "          Color Temperature %" 0.0 -100.0 100.0 5.0 

#pragma parameter wp_saturation "          Saturation Adjustment" 1.0 0.0 2.0 0.05

#pragma parameter pre_bb "          Brightness Adjustment" 1.0 0.0 2.0 0.01

#pragma parameter contr "          Contrast Adjustment" 0.0 -2.0 2.0 0.05

#pragma parameter sega_fix "          Sega Brightness Fix" 0.0 0.0 1.0 1.0

#pragma parameter sms_blue "          SMS Blue Lift" 0.0 0.0 1.0 1.0

#pragma parameter MD_Pal "          MD Palette" 0.0 0.0 1.0 1.0

#pragma parameter BP "          Raise Black Level" 0.0 -100.0 25.0 1.0

#pragma parameter vigstr "          Vignette Strength" 0.0 0.0 2.0 0.05

#pragma parameter vigdef "          Vignette Size" 1.0 0.5 3.0 0.10

#define WP params.WP
#define wp_saturation params.wp_saturation
#define BP params.BP

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 StockPass;
layout(set = 0, binding = 3) uniform sampler2D AfterglowPass;
layout(set = 0, binding = 4) uniform sampler2D SamplerLUT1;
layout(set = 0, binding = 5) uniform sampler2D SamplerLUT2;
layout(set = 0, binding = 6) uniform sampler2D SamplerLUT3;
layout(set = 0, binding = 7) uniform sampler2D SamplerLUT4;

#define COMPAT_TEXTURE(c,d) texture(c,d)


// Color profile matrices

const mat3 Profile0 = 
mat3(
 0.412391,  0.212639,  0.019331,
 0.357584,  0.715169,  0.119195,
 0.180481,  0.072192,  0.950532
);

const mat3 Profile1 = 
mat3(
 0.430554,  0.222004,  0.020182,
 0.341550,  0.706655,  0.129553,
 0.178352,  0.071341,  0.939322
);

const mat3 Profile2 = 
mat3(
 0.396686,  0.210299,  0.006131,
 0.372504,  0.713766,  0.115356,
 0.181266,  0.075936,  0.967571
);

const mat3 Profile3 = 
mat3(
 0.393521,  0.212376,  0.018739,
 0.365258,  0.701060,  0.111934,
 0.191677,  0.086564,  0.958385
);

const mat3 Profile4 = 
mat3(
 0.392258,  0.209410,  0.016061,
 0.351135,  0.725680,  0.093636,
 0.166603,  0.064910,  0.850324
);

const mat3 Profile5 = 
mat3(
 0.377923,  0.195679,  0.010514,
 0.317366,  0.722319,  0.097826,
 0.207738,  0.082002,  1.076960
);

const mat3 ToSRGB = 
mat3(
 3.240970, -0.969244,  0.055630,
-1.537383,  1.875968, -0.203977,
-0.498611,  0.041555,  1.056972
);

const mat3 ToModern = 
mat3(
 2.791723,	-0.894766,	0.041678,
-1.173165,	 1.815586, -0.130886,
-0.440973,	 0.032000,	1.002034
);

const mat3 ToDCI = 
mat3(
 2.493497,	-0.829489,	0.035846,
-0.931384,	 1.762664, -0.076172,
-0.402711,	 0.023625,	0.956885
);

const mat3 ToAdobe = 
mat3(
 2.041588, -0.969244,  0.013444,
-0.565007,  1.875968, -0.11836,
-0.344731,  0.041555,  1.015175
);

const mat3 ToREC = 
mat3(
 1.716651, -0.666684,  0.017640,
-0.355671,  1.616481, -0.042771,
-0.253366,  0.015769,  0.942103
); 

// Color temperature matrices

const mat3 D65_to_D55 = mat3 (
           0.4850339153,  0.2500956126,  0.0227359648,
           0.3488957224,  0.6977914447,  0.1162985741,
           0.1302823568,  0.0521129427,  0.6861537456);


const mat3 D65_to_D93 = mat3 (
           0.3412754080,  0.1759701322,  0.0159972847,
           0.3646170520,  0.7292341040,  0.1215390173,
           0.2369894093,  0.0947957637,  1.2481442225);


vec3 fix_lut(vec3 lutcolor, vec3 ref)
{
	float r = length(ref);
	float l = length(lutcolor);
	float m = max(max(ref.r,ref.g),ref.b);
	ref = normalize(lutcolor + 0.0000001) * mix(r, l, pow(m,1.25));
	return mix(lutcolor, ref, LUTBR);
}


float vignette(vec2 pos) {
	vec2 b = vec2(params.vigdef, params.vigdef) *  vec2(1.0, params.OriginalSize.x/params.OriginalSize.y) * 0.125;
	pos = clamp(pos, 0.0, 1.0);
	pos = abs(2.0*(pos - 0.5));
	vec2 res = mix(0.0.xx, 1.0.xx, smoothstep(1.0.xx, 1.0.xx-b, sqrt(pos)));
	res = pow(res, 0.70.xx);	
	return max(mix(1.0, sqrt(res.x*res.y), params.vigstr), 0.0);
}


vec3 plant (vec3 tar, float r)
{
	float t = max(max(tar.r,tar.g),tar.b) + 0.00001;
	return tar * r / t;
}

float contrast(float x)
{
	return max(mix(x, smoothstep(0.0, 1.0, x), params.contr),0.0);
}

float contrast_sigmoid_inv(float imgColor, float cont, float pivot)
{
	cont = pow(cont - 1., 3.);
	float knee  = 1. / (1. + exp (cont *  pivot));
	float shldr = 1. / (1. + exp (cont * (pivot - 1.)));
	imgColor = pivot - log(1. / (imgColor * (shldr - knee) + knee) - 1.) / cont;
	return imgColor;
}


void main()
{
   vec4 imgColor = COMPAT_TEXTURE(StockPass, vTexCoord.xy);
   vec4 aftglow = COMPAT_TEXTURE(AfterglowPass, vTexCoord.xy);
   
   float w = 1.0-aftglow.w;

   float l = length(aftglow.rgb);
   aftglow.rgb = AS*w*normalize(pow(aftglow.rgb + 0.01, vec3(sat)))*l;
   float bp = w * BP/255.0;
   
   if (params.sega_fix > 0.5) imgColor.rgb = imgColor.rgb * (255.0 / 239.0);
   
   imgColor.rgb = min(imgColor.rgb, 1.0);
   
   if (params.sms_blue > 0.5) imgColor.r = imgColor.r * (1.00), imgColor.g = imgColor.g * (1.00), imgColor.b = imgColor.b * (1.16);
   
   imgColor.rgb = min(imgColor.rgb, 1.0);

   if (params.MD_Pal > 0.5) imgColor.r = contrast_sigmoid_inv(imgColor.r,2.578419881,0.520674), imgColor.g = contrast_sigmoid_inv(imgColor.g,2.578419881,0.520674), imgColor.b = contrast_sigmoid_inv(imgColor.b,2.578419881,0.520674);
   
   imgColor.rgb = min(imgColor.rgb, 1.0);
   
   vec3 color = imgColor.rgb;
 
   if (int(TNTC) == 0)
   {
      color.rgb = imgColor.rgb;
   }
   else
   {
	  float lutlow = LUTLOW/255.0; float invLS = 1.0/LS;
	  vec3 lut_ref = imgColor.rgb + lutlow*(1.0 - pow(imgColor.rgb, 0.333.xxx));
	  float lutb = lut_ref.b * (1.0-0.5*invLS);	  
	  lut_ref.rg    = lut_ref.rg * (1.0-invLS) + 0.5*invLS; 
	  float tile1 = ceil (lutb * (LS-1.0));
	  float tile0 = max(tile1 - 1.0, 0.0);
	  float f = fract(lutb * (LS-1.0)); if (f == 0.0) f = 1.0;
	  vec2 coord0 = vec2(tile0 + lut_ref.r, lut_ref.g)*vec2(invLS, 1.0);
	  vec2 coord1 = vec2(tile1 + lut_ref.r, lut_ref.g)*vec2(invLS, 1.0);
	  vec4 color1, color2, res;
	  
      if (int(TNTC) == 1)
      {
         color1 = COMPAT_TEXTURE(SamplerLUT1, coord0);
         color2 = COMPAT_TEXTURE(SamplerLUT1, coord1);
         res = mix(color1, color2, f);
      }
      else if (int(TNTC) == 2)
      {
         color1 = COMPAT_TEXTURE(SamplerLUT2, coord0);
         color2 = COMPAT_TEXTURE(SamplerLUT2, coord1);
         res = mix(color1, color2, f);
      }	
      else if (int(TNTC) == 3)
      {
         color1 = COMPAT_TEXTURE(SamplerLUT3, coord0);
         color2 = COMPAT_TEXTURE(SamplerLUT3, coord1);
         res = mix(color1, color2, f);
      }	
      else if (int(TNTC) == 4)
      {
         color1 = COMPAT_TEXTURE(SamplerLUT4, coord0);
         color2 = COMPAT_TEXTURE(SamplerLUT4, coord1);
         res = mix(color1, color2, f);
      }	

      res.rgb = fix_lut (res.rgb, imgColor.rgb);
	  
      color = mix(imgColor.rgb, res.rgb, min(TNTC,1.0));
   }

	vec3 c = clamp(color, 0.0, 1.0);
	
	float p;
	mat3 m_out;
	
	if (CS == 0.0) { p = 2.2; m_out =  ToSRGB;   } else
	if (CS == 1.0) { p = 2.2; m_out =  ToModern; } else	
	if (CS == 2.0) { p = 2.6; m_out =  ToDCI;    } else
	if (CS == 3.0) { p = 2.2; m_out =  ToAdobe;  } else
	if (CS == 4.0) { p = 2.4; m_out =  ToREC;    }
	
	color = pow(c, vec3(p));
	
	mat3 m_in = Profile0;

	if (CP == 0.0) { m_in = Profile0; } else	
	if (CP == 1.0) { m_in = Profile1; } else
	if (CP == 2.0) { m_in = Profile2; } else
	if (CP == 3.0) { m_in = Profile3; } else
	if (CP == 4.0) { m_in = Profile4; } else
	if (CP == 5.0) { m_in = Profile5; }
	
	color = m_in*color;
	color = m_out*color;

	color = clamp(color, 0.0, 1.0);

	color = pow(color, vec3(1.0/p));	
	
	if (CP == -1.0) color = c;
	
	vec3 scolor1 = plant(pow(color, vec3(wp_saturation)), max(max(color.r,color.g),color.b));
	float luma = dot(color, vec3(0.299, 0.587, 0.114));
	vec3 scolor2 = mix(vec3(luma), color, wp_saturation);
	color = (wp_saturation > 1.0) ? scolor1 : scolor2;

	color = plant(color, contrast(max(max(color.r,color.g),color.b)));

	p = 2.2;
	color = clamp(color, 0.0, 1.0);	
	color = pow(color, vec3(p)); 
	
	vec3 warmer = D65_to_D55*color;
	warmer = ToSRGB*warmer;
	
	vec3 cooler = D65_to_D93*color;
	cooler = ToSRGB*cooler;
	
	float m = abs(WP)/100.0;
	
	vec3 comp = (WP < 0.0) ? cooler : warmer;
	
	color = mix(color, comp, m);
	color = pow(max(color, 0.0), vec3(1.0/p));

	if (BP > -0.5) color = color + aftglow.rgb + bp; else
	{ 
		color = max(color + BP/255.0, 0.0) / (1.0 + BP/255.0*step(- BP/255.0, max(max(color.r,color.g),color.b))) + aftglow.rgb;
	}
	
	color = min(color * params.pre_bb, 1.0);
	
	FragColor = vec4(color, vignette(vTexCoord.xy)); 
}
4 Likes

I’m sorry, but what are these used for again? Would you kindly, explain?

1 Like

SMS Blue Lift was Dogway’s attempt at implementing the Master System behavior described in this document, tho upon reviewing the original document and comparing the results, i have noticed that his implementation was flawed. (Only the first grey and blue color bars in SMSTestSuite’s Color bars test should have boosted blue, but instead both the first and second bars are boosted.)

MD Palette is an implementation/approximation of the Mega Drive/Genesis RGB palette discussed here.

4 Likes

Damn that is intense technology knowledge.

tho upon reviewing the original document and comparing the results

Was the flaw every addressed?

Nevermind, I see that you posted on the thread

I implemented the Sega MS Nonlinear Blue behavior as described in Notes & Measures: Nonlinear Blue on Sega Master System 1 & Other Findings, and renamed the Sega settings to better clarify their purpose and what platforms they are intended to be used with.

modified pre-shaders-afterglow.slang
#version 450

/*
   CRT Advanced Afterglow, color altering
   
   Copyright (C) 2019-2022 guest(r) and Dr. Venom
   
   This program is free software; you can redistribute it and/or
   modify it under the terms of the GNU General Public License
   as published by the Free Software Foundation; either version 2
   of the License, or (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
   
*/  

layout(push_constant) uniform Push
{
   vec4 SourceSize;
   vec4 OriginalSize;
   vec4 OutputSize;
   uint FrameCount;
   float TNTC;
   float LS;
   float LUTLOW, LUTBR;   
   float CP, CS;
   float WP;
   float wp_saturation;
   float AS, sat;
   float BP;
   float vigstr;
   float vigdef;
   float sega_fix;
   float sms_blue;
   float MD_Pal;
   float pre_bb;
   float contr;
} params;

#pragma parameter AS "          Afterglow Strength" 0.20 0.0 0.60 0.01
#define AS params.AS

#pragma parameter sat "          Afterglow saturation" 0.50 0.0 1.0 0.01
#define sat params.sat

#pragma parameter bogus_color "[ COLOR TWEAKS ]:" 0.0 0.0 1.0 1.0

#pragma parameter CS "          Display Gamut: sRGB, Modern, DCI, Adobe, Rec.2020" 0.0 0.0 4.0 1.0 

#pragma parameter CP "          CRT Profile: EBU | P22 | SMPTE-C | Philips | Trin." 0.0 -1.0 5.0 1.0 

#define CP params.CP
#define CS params.CS

#pragma parameter TNTC "          LUT Colors: Trin.1 | Trin.2 | Nec Mult. | NTSC" 0.0 0.0 4.0 1.0
#define TNTC params.TNTC

#pragma parameter LS "          LUT Size" 32.0 16.0 64.0 16.0
#define LS params.LS

#define LUTLOW 5.0  // "Fix LUT Dark - Range" from 0.0 to 50.0 - RGB singletons

#define LUTBR 1.0   // "Fix LUT Brightness" from 0.0 to 1.0

#pragma parameter WP "          Color Temperature %" 0.0 -100.0 100.0 5.0 

#pragma parameter wp_saturation "          Saturation Adjustment" 1.0 0.0 2.0 0.05

#pragma parameter pre_bb "          Brightness Adjustment" 1.0 0.0 2.0 0.01

#pragma parameter contr "          Contrast Adjustment" 0.0 -2.0 2.0 0.05

#pragma parameter sms_blue "          Sega MS Nonlinear Blue Fix" 0.0 0.0 1.0 1.0

#pragma parameter MD_Pal "          Sega MD RGB Palette" 0.0 0.0 1.0 1.0

#pragma parameter sega_fix "          Sega Brightness Fix (MD/G/CD/32X/2D Sat)" 0.0 0.0 1.0 1.0

#pragma parameter BP "          Raise Black Level" 0.0 -100.0 25.0 1.0

#pragma parameter vigstr "          Vignette Strength" 0.0 0.0 2.0 0.05

#pragma parameter vigdef "          Vignette Size" 1.0 0.5 3.0 0.10

#define WP params.WP
#define wp_saturation params.wp_saturation
#define BP params.BP

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 StockPass;
layout(set = 0, binding = 3) uniform sampler2D AfterglowPass;
layout(set = 0, binding = 4) uniform sampler2D SamplerLUT1;
layout(set = 0, binding = 5) uniform sampler2D SamplerLUT2;
layout(set = 0, binding = 6) uniform sampler2D SamplerLUT3;
layout(set = 0, binding = 7) uniform sampler2D SamplerLUT4;

#define COMPAT_TEXTURE(c,d) texture(c,d)


// Color profile matrices

const mat3 Profile0 = 
mat3(
 0.412391,  0.212639,  0.019331,
 0.357584,  0.715169,  0.119195,
 0.180481,  0.072192,  0.950532
);

const mat3 Profile1 = 
mat3(
 0.430554,  0.222004,  0.020182,
 0.341550,  0.706655,  0.129553,
 0.178352,  0.071341,  0.939322
);

const mat3 Profile2 = 
mat3(
 0.396686,  0.210299,  0.006131,
 0.372504,  0.713766,  0.115356,
 0.181266,  0.075936,  0.967571
);

const mat3 Profile3 = 
mat3(
 0.393521,  0.212376,  0.018739,
 0.365258,  0.701060,  0.111934,
 0.191677,  0.086564,  0.958385
);

const mat3 Profile4 = 
mat3(
 0.392258,  0.209410,  0.016061,
 0.351135,  0.725680,  0.093636,
 0.166603,  0.064910,  0.850324
);

const mat3 Profile5 = 
mat3(
 0.377923,  0.195679,  0.010514,
 0.317366,  0.722319,  0.097826,
 0.207738,  0.082002,  1.076960
);

const mat3 ToSRGB = 
mat3(
 3.240970, -0.969244,  0.055630,
-1.537383,  1.875968, -0.203977,
-0.498611,  0.041555,  1.056972
);

const mat3 ToModern = 
mat3(
 2.791723,	-0.894766,	0.041678,
-1.173165,	 1.815586, -0.130886,
-0.440973,	 0.032000,	1.002034
);

const mat3 ToDCI = 
mat3(
 2.493497,	-0.829489,	0.035846,
-0.931384,	 1.762664, -0.076172,
-0.402711,	 0.023625,	0.956885
);

const mat3 ToAdobe = 
mat3(
 2.041588, -0.969244,  0.013444,
-0.565007,  1.875968, -0.11836,
-0.344731,  0.041555,  1.015175
);

const mat3 ToREC = 
mat3(
 1.716651, -0.666684,  0.017640,
-0.355671,  1.616481, -0.042771,
-0.253366,  0.015769,  0.942103
); 

// Color temperature matrices

const mat3 D65_to_D55 = mat3 (
           0.4850339153,  0.2500956126,  0.0227359648,
           0.3488957224,  0.6977914447,  0.1162985741,
           0.1302823568,  0.0521129427,  0.6861537456);


const mat3 D65_to_D93 = mat3 (
           0.3412754080,  0.1759701322,  0.0159972847,
           0.3646170520,  0.7292341040,  0.1215390173,
           0.2369894093,  0.0947957637,  1.2481442225);


vec3 fix_lut(vec3 lutcolor, vec3 ref)
{
	float r = length(ref);
	float l = length(lutcolor);
	float m = max(max(ref.r,ref.g),ref.b);
	ref = normalize(lutcolor + 0.0000001) * mix(r, l, pow(m,1.25));
	return mix(lutcolor, ref, LUTBR);
}


float vignette(vec2 pos) {
	vec2 b = vec2(params.vigdef, params.vigdef) *  vec2(1.0, params.OriginalSize.x/params.OriginalSize.y) * 0.125;
	pos = clamp(pos, 0.0, 1.0);
	pos = abs(2.0*(pos - 0.5));
	vec2 res = mix(0.0.xx, 1.0.xx, smoothstep(1.0.xx, 1.0.xx-b, sqrt(pos)));
	res = pow(res, 0.70.xx);	
	return max(mix(1.0, sqrt(res.x*res.y), params.vigstr), 0.0);
}


vec3 plant (vec3 tar, float r)
{
	float t = max(max(tar.r,tar.g),tar.b) + 0.00001;
	return tar * r / t;
}

float contrast(float x)
{
	return max(mix(x, smoothstep(0.0, 1.0, x), params.contr),0.0);
}

float contrast_sigmoid_inv(float imgColor, float cont, float pivot)
{
	cont = pow(cont - 1., 3.);
	float knee  = 1. / (1. + exp (cont *  pivot));
	float shldr = 1. / (1. + exp (cont * (pivot - 1.)));
	imgColor = pivot - log(1. / (imgColor * (shldr - knee) + knee) - 1.) / cont;
	return imgColor;
}


void main()
{
   vec4 imgColor = COMPAT_TEXTURE(StockPass, vTexCoord.xy);
   vec4 aftglow = COMPAT_TEXTURE(AfterglowPass, vTexCoord.xy);
   
   float w = 1.0-aftglow.w;

   float l = length(aftglow.rgb);
   aftglow.rgb = AS*w*normalize(pow(aftglow.rgb + 0.01, vec3(sat)))*l;
   float bp = w * BP/255.0;
   
   if (params.sms_blue > 0.5)
   {
      if (imgColor.b < 0.5)
      {
	  if (imgColor.b > 0.25)
	  {
	       (imgColor.b = 0.4078431372549019607843137254902);
      	  }
      }
   }
   
   imgColor.rgb = min(imgColor.rgb, 1.0);

   if (params.MD_Pal > 0.5) imgColor.r = contrast_sigmoid_inv(imgColor.r,2.578419881,0.520674), imgColor.g = contrast_sigmoid_inv(imgColor.g,2.578419881,0.520674), imgColor.b = contrast_sigmoid_inv(imgColor.b,2.578419881,0.520674);
   
   imgColor.rgb = min(imgColor.rgb, 1.0);
   
   if (params.sega_fix > 0.5) imgColor.rgb = imgColor.rgb * (255.0 / 239.0);
   
   imgColor.rgb = min(imgColor.rgb, 1.0);
   
   vec3 color = imgColor.rgb;
 
   if (int(TNTC) == 0)
   {
      color.rgb = imgColor.rgb;
   }
   else
   {
	  float lutlow = LUTLOW/255.0; float invLS = 1.0/LS;
	  vec3 lut_ref = imgColor.rgb + lutlow*(1.0 - pow(imgColor.rgb, 0.333.xxx));
	  float lutb = lut_ref.b * (1.0-0.5*invLS);	  
	  lut_ref.rg    = lut_ref.rg * (1.0-invLS) + 0.5*invLS; 
	  float tile1 = ceil (lutb * (LS-1.0));
	  float tile0 = max(tile1 - 1.0, 0.0);
	  float f = fract(lutb * (LS-1.0)); if (f == 0.0) f = 1.0;
	  vec2 coord0 = vec2(tile0 + lut_ref.r, lut_ref.g)*vec2(invLS, 1.0);
	  vec2 coord1 = vec2(tile1 + lut_ref.r, lut_ref.g)*vec2(invLS, 1.0);
	  vec4 color1, color2, res;
	  
      if (int(TNTC) == 1)
      {
         color1 = COMPAT_TEXTURE(SamplerLUT1, coord0);
         color2 = COMPAT_TEXTURE(SamplerLUT1, coord1);
         res = mix(color1, color2, f);
      }
      else if (int(TNTC) == 2)
      {
         color1 = COMPAT_TEXTURE(SamplerLUT2, coord0);
         color2 = COMPAT_TEXTURE(SamplerLUT2, coord1);
         res = mix(color1, color2, f);
      }	
      else if (int(TNTC) == 3)
      {
         color1 = COMPAT_TEXTURE(SamplerLUT3, coord0);
         color2 = COMPAT_TEXTURE(SamplerLUT3, coord1);
         res = mix(color1, color2, f);
      }	
      else if (int(TNTC) == 4)
      {
         color1 = COMPAT_TEXTURE(SamplerLUT4, coord0);
         color2 = COMPAT_TEXTURE(SamplerLUT4, coord1);
         res = mix(color1, color2, f);
      }	

      res.rgb = fix_lut (res.rgb, imgColor.rgb);
	  
      color = mix(imgColor.rgb, res.rgb, min(TNTC,1.0));
   }

	vec3 c = clamp(color, 0.0, 1.0);
	
	float p;
	mat3 m_out;
	
	if (CS == 0.0) { p = 2.2; m_out =  ToSRGB;   } else
	if (CS == 1.0) { p = 2.2; m_out =  ToModern; } else	
	if (CS == 2.0) { p = 2.6; m_out =  ToDCI;    } else
	if (CS == 3.0) { p = 2.2; m_out =  ToAdobe;  } else
	if (CS == 4.0) { p = 2.4; m_out =  ToREC;    }
	
	color = pow(c, vec3(p));
	
	mat3 m_in = Profile0;

	if (CP == 0.0) { m_in = Profile0; } else	
	if (CP == 1.0) { m_in = Profile1; } else
	if (CP == 2.0) { m_in = Profile2; } else
	if (CP == 3.0) { m_in = Profile3; } else
	if (CP == 4.0) { m_in = Profile4; } else
	if (CP == 5.0) { m_in = Profile5; }
	
	color = m_in*color;
	color = m_out*color;

	color = clamp(color, 0.0, 1.0);

	color = pow(color, vec3(1.0/p));	
	
	if (CP == -1.0) color = c;
	
	vec3 scolor1 = plant(pow(color, vec3(wp_saturation)), max(max(color.r,color.g),color.b));
	float luma = dot(color, vec3(0.299, 0.587, 0.114));
	vec3 scolor2 = mix(vec3(luma), color, wp_saturation);
	color = (wp_saturation > 1.0) ? scolor1 : scolor2;

	color = plant(color, contrast(max(max(color.r,color.g),color.b)));

	p = 2.2;
	color = clamp(color, 0.0, 1.0);	
	color = pow(color, vec3(p)); 
	
	vec3 warmer = D65_to_D55*color;
	warmer = ToSRGB*warmer;
	
	vec3 cooler = D65_to_D93*color;
	cooler = ToSRGB*cooler;
	
	float m = abs(WP)/100.0;
	
	vec3 comp = (WP < 0.0) ? cooler : warmer;
	
	color = mix(color, comp, m);
	color = pow(max(color, 0.0), vec3(1.0/p));

	if (BP > -0.5) color = color + aftglow.rgb + bp; else
	{ 
		color = max(color + BP/255.0, 0.0) / (1.0 + BP/255.0*step(- BP/255.0, max(max(color.r,color.g),color.b))) + aftglow.rgb;
	}
	
	color = min(color * params.pre_bb, 1.0);
	
	FragColor = vec4(color, vignette(vTexCoord.xy)); 
}

Edit 20240314: Changed “if (imgColor.b > 0)” to “if (imgColor.b > 0.25)” in the Nonlinear Blue Fix to prevent potential errors.

6 Likes

Proof of concept implementation of automatic resolution-based dynamic NTSC Resolution Scaling for use with games that include pseudo hi-res material:

ntsc-pass1.slang
#version 450

// NTSC-Adaptive
// based on Themaister's NTSC shader


layout(push_constant) uniform Push
{
   vec4 OutputSize;
   vec4 OriginalSize;
   vec4 SourceSize;
   uint FrameCount;
   float quality, ntsc_sat, cust_fringing, cust_artifacting, ntsc_bright, ntsc_scale, ntsc_fields, ntsc_phase, ntsc_gamma, ntsc_rainbow1, ntsc_pseudo, ntsc_pscale;
} params;

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

#pragma parameter ntsc-row1 "------------------------------------------------" 0.0 0.0 0.0 1.0
#pragma parameter quality "INFO --> A&F Values: Svideo = 0.0 | Composite = 1.0 | RF = 2.0" 0.0 0.0 0.0 1.0
#pragma parameter cust_artifacting "NTSC Artifacting Value" 1.0 0.0 5.0 0.1
#pragma parameter cust_fringing "NTSC Fringing Value" 1.0 0.0 5.0 0.1
#pragma parameter ntsc-row2 "------------------------------------------------" 0.0 0.0 0.0 1.0
#pragma parameter ntsc_fields "NTSC Merge Fields: Auto | NO | YES" -1.0 -1.0 1.0 1.0
#pragma parameter ntsc_phase "NTSC Phase: Auto | 2 phase | 3 phase | Mixed" 1.0 1.0 4.0 1.0
#pragma parameter ntsc_scale "NTSC Resolution Scaling" 1.0 0.20 2.5 0.025
#pragma parameter ntsc_pseudo "Downsample Pseudo Hi-Res" 1.0 1.0 3.0 1.0
#pragma parameter ntsc_pscale "Downsample Resolution Scaling" 0.5 0.20 2.5 0.025
#pragma parameter ntsc_sat "NTSC Color Saturation" 1.0 0.0 2.0 0.01
#pragma parameter ntsc_bright "NTSC Brightness" 1.0 0.0 1.5 0.01
#pragma parameter ntsc_gamma "NTSC Filtering Gamma Correction" 1.0 0.25 2.5 0.025
#pragma parameter ntsc_rainbow1 "NTSC Coloring/Rainbow Effect" 0.0 -1.0 1.0 0.05


#define PI 3.14159265

#pragma stage vertex
layout(location = 0) in vec4 Position;
layout(location = 1) in vec2 TexCoord;
layout(location = 0) out vec2 vTexCoord;
layout(location = 1) out vec2 pix_no;
layout(location = 2) out float phase;
layout(location = 3) out float BRIGHTNESS;
layout(location = 4) out float SATURATION;
layout(location = 5) out float FRINGING;
layout(location = 6) out float ARTIFACTING;
layout(location = 7) out float CHROMA_MOD_FREQ;
layout(location = 8) out float MERGE;

void main()
{
	float res = min(params.ntsc_scale, 1.0);
	float OriginalSize = params.OriginalSize.x;
	float SourceSize = params.SourceSize.x;
	float OriginalSizeX = params.OriginalSize.x;
	float OriginalSizeY = params.OriginalSize.y;
	gl_Position = global.MVP * Position;
	vTexCoord = TexCoord;
		
if (params.ntsc_pseudo == 1.0) 
{
	pix_no = TexCoord * params.OriginalSize.xy * res * vec2(4.0,1.0);
}
	
if (params.ntsc_pseudo == 2.0) 
{
	if ((OriginalSizeX / OriginalSizeY) >= 2)
	{
	pix_no = (TexCoord * params.OriginalSize.xy * res * vec2(4.0,1.0)) * (params.ntsc_pscale);
	}
	else if ((OriginalSizeX / OriginalSizeY) < 2)
	{
	pix_no = TexCoord * params.OriginalSize.xy * res * vec2(4.0,1.0);
	}
}

if (params.ntsc_pseudo == 3.0) 
{
	if (SourceSize >= 480)
	{
	pix_no = (TexCoord * params.OriginalSize.xy * res * vec2(4.0,1.0)) * (params.ntsc_pscale);
	}
	else if (SourceSize < 480)
	{
	pix_no = TexCoord * params.OriginalSize.xy * res * vec2(4.0,1.0);
	}
}

if (params.ntsc_pseudo == 1.0) 
{	
	phase = (params.ntsc_phase < 1.5) ? ((OriginalSize > 300.0) ? 2.0 : 3.0) : ((params.ntsc_phase > 2.5) ? 3.0 : 2.0);
}
	
if (params.ntsc_pseudo == 2.0) 
{
	if ((OriginalSizeX / OriginalSizeY) >= 2)
	{
	phase = (params.ntsc_phase < 1.5) ? (((OriginalSize * (params.ntsc_pscale)) > 300.0) ? 2.0 : 3.0) : ((params.ntsc_phase > 2.5) ? 3.0 : 2.0);
	}
	else if ((OriginalSizeX / OriginalSizeY) < 2)
	{
	phase = (params.ntsc_phase < 1.5) ? ((OriginalSize > 300.0) ? 2.0 : 3.0) : ((params.ntsc_phase > 2.5) ? 3.0 : 2.0);
	}
}

if (params.ntsc_pseudo == 3.0) 
{
	if (SourceSize >= 480)
	{
	phase = (params.ntsc_phase < 1.5) ? (((OriginalSize * (params.ntsc_pscale)) > 300.0) ? 2.0 : 3.0) : ((params.ntsc_phase > 2.5) ? 3.0 : 2.0);
	}
	else if (SourceSize < 480)
	{
	phase = (params.ntsc_phase < 1.5) ? ((OriginalSize > 300.0) ? 2.0 : 3.0) : ((params.ntsc_phase > 2.5) ? 3.0 : 2.0);
	}
}

	if (params.ntsc_phase == 4.0) phase = 3.0;
	
	CHROMA_MOD_FREQ = (phase < 2.5) ? (4.0 * PI / 15.0) : (PI / 3.0);
	ARTIFACTING = params.cust_artifacting;
	FRINGING = params.cust_fringing;
	SATURATION = params.ntsc_sat;
	BRIGHTNESS = params.ntsc_bright;
	MERGE = 0.0;
	if (params.ntsc_fields == -1.0 && phase == 3.0) MERGE = 1.0;
	else if (params.ntsc_fields ==  0.0) MERGE = 0.0;
	else if (params.ntsc_fields ==  1.0) MERGE = 1.0;	
}

#pragma stage fragment
layout(location = 0) in vec2 vTexCoord;
layout(location = 1) in vec2 pix_no;
layout(location = 2) in float phase;
layout(location = 3) in float BRIGHTNESS;
layout(location = 4) in float SATURATION;
layout(location = 5) in float FRINGING;
layout(location = 6) in float ARTIFACTING;
layout(location = 7) in float CHROMA_MOD_FREQ;
layout(location = 8) in float MERGE;
layout(location = 0) out vec4 FragColor;
layout(set = 0, binding = 2) uniform sampler2D Source;

#define mix_mat  mat3(BRIGHTNESS, FRINGING, FRINGING, ARTIFACTING, 2.0 * SATURATION, 0.0, ARTIFACTING, 0.0, 2.0 * SATURATION)

const mat3 yiq2rgb_mat = mat3(
   1.0, 0.956, 0.6210,
   1.0, -0.2720, -0.6474,
   1.0, -1.1060, 1.7046);

vec3 yiq2rgb(vec3 yiq)
{
   return yiq * yiq2rgb_mat;
}

const mat3 yiq_mat = mat3(
      0.2989, 0.5870, 0.1140,
      0.5959, -0.2744, -0.3216,
      0.2115, -0.5229, 0.3114
);

vec3 rgb2yiq(vec3 col)
{
   return col * yiq_mat;
}

float get_luma(vec3 c)
{
	return dot(c, vec3(0.2989, 0.5870, 0.1140));
}


void main()
{
   vec3 col = texture(Source, vTexCoord).rgb;
	float OriginalSize = params.OriginalSize.x;
	float SourceSize = params.SourceSize.x;
	float OriginalSizeX = params.OriginalSize.x;
	float OriginalSizeY = params.OriginalSize.y;
	float res = 1;

if (params.ntsc_pseudo == 1.0) 
{
   res = params.ntsc_scale;
}

if (params.ntsc_pseudo == 2.0) 
{
	if ((OriginalSizeX / OriginalSizeY) >= 2)
	{
   res = params.ntsc_scale * params.ntsc_pscale;
	}
	else if ((OriginalSizeX / OriginalSizeY) < 2)
	{
   res = params.ntsc_scale;
	}
}

if (params.ntsc_pseudo == 3.0) 
{
	if (SourceSize >= 480)
	{
   res = params.ntsc_scale * params.ntsc_pscale;
	}
	else if (SourceSize < 480)
	{
   res = params.ntsc_scale;
	}
}
 
   vec3 yiq = rgb2yiq(col); float c3 = yiq.x;
   yiq.x = pow(yiq.x, params.ntsc_gamma);
   float lum = yiq.x;

   float mix02 = 0.0;
   float mix03 = 0.0;   
   vec2 dx = vec2(params.OriginalSize.z, 0.0);
   vec3 c1 = texture(Source, vTexCoord - dx).rgb;
   vec3 c2 = texture(Source, vTexCoord + dx).rgb;
   
if(abs(params.ntsc_rainbow1) > 0.025)
{ 
   vec2 dy = vec2(0.0, params.OriginalSize.w);
   vec3 c4 = texture(Source, vTexCoord + dy).rgb;  
   vec3 c5 = texture(Source, vTexCoord + dx +dy).rgb;
   vec3 c6 = texture(Source, vTexCoord + dx +dx).rgb;
   vec3 c7 = texture(Source, vTexCoord + 3.0*dx).rgb;
   
   c1.x = get_luma(c1);
   c2.x = get_luma(c2);
   c4.x = get_luma(c4);
   c5.x = get_luma(c5);
   c6.x = get_luma(c6);
   c7.x = get_luma(c7);
   
   float mix00 = min(5.0*min(min(abs(c3-c1.x),abs(c3-c2.x)),min(abs(c2.x-c6.x),abs(c6.x-c7.x))),1.0);
   float bar1  = 1.0 - min(7.0*min(max(max(c3,c4.x)-0.15,0.0), max(max(c2.x,c5.x)-0.15,0.0)),1.0);
   float bar2  = step(abs(c1.x-c2.x)+abs(c3-c6.x)+abs(c2.x-c7.x), 0.325);
   mix02 = bar1 * bar2 * mix00 * (1.0 - min(10.0*min(abs(c3-c4.x), abs(c2.x-c5.x)),1.0));
   mix02 = mix02*params.ntsc_rainbow1;
}
   
if (params.ntsc_phase == 4.0) 
{
   float mix01 = min(5.0*abs(c1.x-c2.x),1.0);
   c1.x = pow(c1.x, params.ntsc_gamma);
   c2.x = pow(c2.x, params.ntsc_gamma);
   yiq.x = mix(min(0.5*(yiq.x + max(c1.x,c2.x)), max(yiq.x , min(c1.x,c2.x))), yiq.x, mix01);
}   
   vec3 yiq2 = yiq;	
   vec3 yiqs = yiq;
   vec3 yiqz = yiq;
   
   float mod1 = 2.0;
   float mod2 = 3.0; 

if (MERGE > 0.5)
{
   float chroma_phase2 = (phase < 2.5) ? PI * (mod(pix_no.y, mod1) + mod(params.FrameCount+1, 2.)) : 0.6667 * PI * (mod(pix_no.y, mod2) + mod(params.FrameCount+1, 2.));
   float mod_phase2 = chroma_phase2 * (1.0-mix02) + pix_no.x * CHROMA_MOD_FREQ;
   float i_mod2 = cos(mod_phase2);
   float q_mod2 = sin(mod_phase2);
   yiq2.yz *= vec2(i_mod2, q_mod2); // Modulate.
   yiq2 *= mix_mat; // Cross-talk.
   yiq2.yz *= vec2(i_mod2, q_mod2); // Demodulate.   

   if (res > 1.025)
   {
      mod_phase2 = chroma_phase2 * (1.0-mix02) + pix_no.x * CHROMA_MOD_FREQ * res;
      i_mod2 = cos(mod_phase2);
      q_mod2 = sin(mod_phase2);
      yiqs.yz *= vec2(i_mod2, q_mod2); // Modulate.
      yiq2.x = dot(yiqs, mix_mat[0]);  // Cross-talk.
   }
}
   
   float chroma_phase = (phase < 2.5) ? PI * (mod(pix_no.y, mod1) + mod(params.FrameCount, 2.)) : 0.6667 * PI * (mod(pix_no.y, mod2) + mod(params.FrameCount, 2.));
   float mod_phase = chroma_phase * (1.0-mix02) + pix_no.x * CHROMA_MOD_FREQ;

   float i_mod = cos(mod_phase);
   float q_mod = sin(mod_phase);

   yiq.yz *= vec2(i_mod, q_mod); // Modulate.
   yiq *= mix_mat; // Cross-talk.
   yiq.yz *= vec2(i_mod, q_mod); // Demodulate.
   
    if (res > 1.025)
   {
      mod_phase = chroma_phase * (1.0-mix02) + pix_no.x * CHROMA_MOD_FREQ * res;
      i_mod = cos(mod_phase); 
      q_mod = sin(mod_phase);
      yiqz.yz *= vec2(i_mod, q_mod); // Modulate.
      yiq.x = dot(yiqz, mix_mat[0]); // Cross-talk.
   }
      
if (params.ntsc_phase == 4.0)
{
	yiq.x = lum; yiq2.x = lum;
}

   yiq = (MERGE < 0.5) ? yiq : 0.5*(yiq+yiq2);
   
   FragColor = vec4(yiq, lum);
} 
ntsc-pass2.slang
#version 450

// NTSC-Adaptive
// based on Themaister's NTSC shader


layout(push_constant) uniform Push
{
   vec4 OutputSize;
   vec4 OriginalSize;
   vec4 SourceSize;
   float ntsc_scale;
   float ntsc_phase;
   float auto_res;
   float ntsc_ring;
   float ntsc_cscale;
   float ntsc_cscale1;   
   float ntsc_taps;
   float ntsc_falloff;
   float ntsc_pseudo;
   float ntsc_pscale;
} params;

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


#pragma parameter ntsc_scale   "NTSC Resolution Scaling" 1.0 0.20 2.5 0.025
#pragma parameter ntsc_pseudo "Downsample Pseudo Hi-Res" 1.0 1.0 3.0 1.0
#pragma parameter ntsc_pscale "Downsample Resolution Scaling" 0.5 0.20 2.5 0.025
#pragma parameter ntsc_phase   "NTSC Phase: Auto | 2 phase | 3 phase | Mixed" 1.0 1.0 4.0 1.0
#pragma parameter ntsc-row3 "------------------------------------------------" 0.0 0.0 0.0 1.0
#pragma parameter ntsc_taps    "NTSC # of Taps (Filter Width)" 32.0 6.0 32.0 1.0
#pragma parameter ntsc_cscale  "NTSC Chroma Scaling / Bleeding (2-phase)" 1.0 1.00 4.00 0.05
#pragma parameter ntsc_cscale1 "NTSC Chroma Scaling / Bleeding (3-phase)" 1.0 0.20 2.25 0.05
#pragma parameter ntsc_falloff "NTSC Chroma Taps Fall-Off (2-phase)" 0.0 0.0 1.0 0.05
#pragma parameter ntsc-row4 "------------------------------------------------" 0.0 0.0 0.0 1.0
#pragma parameter ntsc_ring    "NTSC Anti-Ringing" 0.5 0.0 1.0 0.10

#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 - vec2(0.25 * params.OriginalSize.z/4.0, 0.0); // Compensate for decimate-by-2.
}

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


vec3 fetch_offset2(vec2 dx)
{
   return texture(Source, vTexCoord + dx).xyz + texture(Source, vTexCoord - dx).xyz;
}

vec3 fetch_offset3(vec3 dx)
{
   return vec3(texture(Source, vTexCoord + dx.xz).x + texture(Source, vTexCoord - dx.xz).x, texture(Source, vTexCoord + dx.yz).yz + texture(Source, vTexCoord - dx.yz).yz);
}

const mat3 yiq2rgb_mat = mat3(
   1.0, 0.956, 0.6210,
   1.0, -0.2720, -0.6474,
   1.0, -1.1060, 1.7046);

vec3 yiq2rgb(vec3 yiq)
{
   return yiq * yiq2rgb_mat;
}

const mat3 yiq_mat = mat3(
      0.2989, 0.5870, 0.1140,
      0.5959, -0.2744, -0.3216,
      0.2115, -0.5229, 0.3114
);

vec3 rgb2yiq(vec3 col)
{
   return col * yiq_mat;
}


const int TAPS_2_phase = 32;
const float luma_filter_2_phase[33] = float[33](
  -0.000174844,
  -0.000205844,
  -0.000149453,
  -0.000051693,
   0.000000000,
  -0.000066171,
  -0.000245058,
  -0.000432928,
  -0.000472644,
  -0.000252236,
   0.000198929,
   0.000687058,
   0.000944112,
   0.000803467,
   0.000363199,
   0.000013422,
   0.000253402,
   0.001339461,
   0.002932972,
   0.003983485,
   0.003026683,
  -0.001102056,
  -0.008373026,
  -0.016897700,
  -0.022914480,
  -0.021642347,
  -0.028863273,
   0.027271957,
   0.054921920,
   0.098342579,
   0.139044281,
   0.168055832,
   0.178571429);


const int TAPS_3_phase = 24;

const float chroma_filter_3_phase[25] = float[25](
  -0.000118847,
  -0.000271306,
  -0.000502642,
  -0.000930833,
  -0.001451013,
  -0.002064744,
  -0.002700432,
  -0.003241276,
  -0.003524948,
  -0.003350284,
  -0.002491729,
  -0.000721149,
   0.002164659,
   0.006313635,
   0.011789103,
   0.018545660,
   0.026414396,
   0.035100710,
   0.044196567,
   0.053207202,
   0.061590275,
   0.068803602,
   0.074356193,
   0.077856564,
   0.079052396);

const float luma_filter_4_phase[25] = float[25](
  -0.000472644,
  -0.000252236,
   0.000198929,
   0.000687058,
   0.000944112,
   0.000803467,
   0.000363199,
   0.000013422,
   0.000253402,
   0.001339461,
   0.002932972,
   0.003983485,
   0.003026683,
  -0.001102056,
  -0.008373026,
  -0.016897700,
  -0.022914480,
  -0.021642347,
  -0.028863273,
   0.027271957,
   0.054921920,
   0.098342579,
   0.139044281,
   0.168055832,
   0.178571429);

void main()
{

float luma_filter_3_phase[25] = float[25](
  -0.000012020,
  -0.000022146,
  -0.000013155,
  -0.000012020,
  -0.000049979,
  -0.000113940,
  -0.000122150,
  -0.000005612,
   0.000170516,
   0.000237199,
   0.000169640,
   0.000285688,
   0.000984574,
   0.002018683,
   0.002002275,
  -0.005909882,
  -0.012049081,
  -0.018222860,
  -0.022606931,
   0.002460860,
   0.035868225,
   0.084016453,
   0.135563500,
   0.175261268,
   0.220176552);

   float res = params.ntsc_scale;
   float OriginalSize = params.OriginalSize.x; 
   float SourceSize = params.SourceSize.x;
   float OriginalSizeX = params.OriginalSize.x;
   float OriginalSizeY = params.OriginalSize.y;
   float resdiv = 1;
	
if (params.ntsc_pseudo == 1.0) 
{
   if (OriginalSize >= 480) { resdiv = 1; }
   else if (OriginalSize < 480) { resdiv = 1; }
}
   
if (params.ntsc_pseudo == 2.0) 
{
   if ((OriginalSizeX / OriginalSizeY) >= 2) { resdiv = (params.ntsc_pscale); }
   else if ((OriginalSizeX / OriginalSizeY) < 2) { resdiv = 1; }
}
  
if (params.ntsc_pseudo == 3.0) 
{
   if (OriginalSize >= 480 { resdiv = (params.ntsc_pscale); }
   else if (OriginalSize < 480) { resdiv = 1; }
}

   vec2 one_x = 0.25*params.OriginalSize.zz / (res * resdiv);
   
   vec3 signal = vec3(0.0);
   float phase = (params.ntsc_phase < 1.5) ? (((OriginalSize * resdiv) > 300.0) ? 2.0 : 3.0) : ((params.ntsc_phase > 2.5) ? 3.0 : 2.0);
   if (params.ntsc_phase == 4.0) { phase = 3.0; luma_filter_3_phase = luma_filter_4_phase; }

   float offset = 0.0; float tf = 0.0; int i = 0; float j = 0.0;
   vec3 wsum = 0.0.xxx;
   vec3 sums = wsum;
   vec3 tmp  = wsum;  

   if(phase < 2.5)
   {
	  vec2 dx = vec2(one_x.x, 0.0); vec2 dx1 = dx;
	  int loopstart = int(TAPS_2_phase - params.ntsc_taps);
	  
	  float ltap = params.ntsc_taps + 1.0;
	  float cs_sub = params.ntsc_taps - params.ntsc_taps / params.ntsc_cscale;
	  float taps_fo = 0.0;
	  
      for (i = loopstart; i < 32; i++)
      {
         offset = float(i-loopstart); j = offset + 1.0; dx1 = (offset - params.ntsc_taps)*dx;
         sums = fetch_offset2(dx1); taps_fo = max(j-cs_sub, 0.0);
		 taps_fo = mix(taps_fo, taps_fo*taps_fo, params.ntsc_falloff);
         tmp = vec3(luma_filter_2_phase[i], taps_fo.xx);
         wsum = wsum + tmp;
         signal += sums * tmp;
      }
      taps_fo = ltap - cs_sub; taps_fo = mix(taps_fo, taps_fo*taps_fo, params.ntsc_falloff);
	  tmp = vec3(luma_filter_2_phase[TAPS_2_phase], taps_fo.xx);
      wsum = wsum + wsum + tmp;
      signal += texture(Source, vTexCoord).xyz * tmp;
      signal = signal / wsum;
   }
   else
   {
      one_x.y = one_x.y/params.ntsc_cscale1;
	  vec3 dx = vec3(one_x.x, one_x.y, 0.0); vec3 dx1 = dx;
	  
	  float iloop = min(params.ntsc_taps, TAPS_3_phase);
      int loopstart = int(24.0 - iloop);
	  
      for (i = loopstart; i < 24; i++)
      {
         offset = float(i-loopstart); j = offset + 1.0; dx1.xy = (offset - iloop)*dx.xy;
         sums = fetch_offset3(dx1);
         tmp = vec3(luma_filter_3_phase[i], chroma_filter_3_phase[i].xx);
         wsum = wsum + tmp;
         signal += sums * tmp;
      }
      tmp = vec3(luma_filter_3_phase[TAPS_3_phase], chroma_filter_3_phase[TAPS_3_phase], chroma_filter_3_phase[TAPS_3_phase]);
      wsum = wsum + wsum + tmp;
      signal += texture(Source, vTexCoord).xyz * tmp;
      signal = signal / wsum;
   }

   if (params.ntsc_ring > 0.05)
   {
      vec2 dx = vec2(params.OriginalSize.z / min((res * resdiv), 1.0), 0.0);
      float a = texture(Source, vTexCoord - 1.5*dx).a;
      float b = texture(Source, vTexCoord - 0.5*dx).a;
      float c = texture(Source, vTexCoord + 1.5*dx).a;
      float d = texture(Source, vTexCoord + 0.5*dx).a;
      float e = texture(Source, vTexCoord         ).a;	  
      signal.x = mix(signal.x, clamp(signal.x, min(min(min(a,b),min(c,d)),e), max(max(max(a,b),max(c,d)),e)), params.ntsc_ring);
   }
   
   vec3 orig = rgb2yiq(texture(PrePass0, vTexCoord).rgb);
   
   signal.x = clamp(signal.x, -1.0, 1.0);
   vec3 rgb = signal;
   
   FragColor = vec4(rgb, orig.x);
}  

Image Comparisons

Downsample Pseudo Hi-Res parameter options:

1: Off

2: Downsample is activated when the horizontal resolution is at least double the vertical resolution.

3: Downsample is activated when the horizontal resolution is 480 or greater. Needed for cores that double the vertical resolution of pseudo hi-res modes, such as Mesen-S. Doesn’t play nice with games that include interlaced modes or other non-pseudo hi-res material.

When the downsample is active, Auto NTSC Phase is derived from the final downsampled horizontal resolution.

This method isn’t quite perfect, and cannot be perfect so long as ntsc-pass1.slang and ntsc-pass2.slang utilize preset-level scale_type_x/scale_x settings (unless i was misinformed, and there is a way to modify preset scale_type_x/scale_x settings using shader code.)

In the meantime, some additional modifications to crt-guest-advanced-ntsc-pass1.slang, the old scale_type_x absolute/scale_x 1024 preset settings can work in tandem with this Downsample Pseudo Hi-Res parameter to perfect the downsample for Super Famicom games.

crt-guest-advanced-ntsc-pass1.slang
#version 450

/*
   CRT - Guest - NTSC - Pass1
   
   Copyright (C) 2018-2023 guest(r) - [email protected]

   Incorporates many good ideas and suggestions from Dr. Venom.
   I would also like give thanks to many Libretro forums members for continuous feedback, suggestions and caring about the shader.
   
   This program is free software; you can redistribute it and/or
   modify it under the terms of the GNU General Public License
   as published by the Free Software Foundation; either version 2
   of the License, or (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
   
*/

layout(push_constant) uniform Push
{
	vec4 SourceSize;
	vec4 OriginalSize;
	vec4 OutputSize;
	uint FrameCount;
	float SIGMA_HOR;
	float HSHARPNESS;
	float S_SHARP;
	float HARNG;
	float HSHARP; 
	float spike;
	float internal_res;
	float MAXS;
} params;

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


#pragma parameter bogus_filtering "[ FILTERING OPTIONS ]: " 0.0 0.0 1.0 1.0

#pragma parameter HSHARPNESS "          Horizontal Filter Range" 1.60 1.0 8.0 0.05
#define HSHARPNESS params.HSHARPNESS

#pragma parameter SIGMA_HOR "          Horizontal Blur Sigma" 0.80 0.1 7.0 0.025
#define SIGMA_HOR params.SIGMA_HOR

#pragma parameter S_SHARP "          Substractive Sharpness" 1.20 0.0 3.0 0.05
#define S_SHARP params.S_SHARP

#pragma parameter HSHARP "          Sharpness Definition" 1.20 0.0 2.0 0.10
#define HSHARP params.HSHARP

#pragma parameter MAXS "          Maximum Sharpness" 0.18 0.0 0.30 0.01
#define MAXS params.MAXS 

#pragma parameter HARNG "          Substractive Sharpness Ringing" 0.30 0.0 4.0 0.05
#define HARNG params.HARNG 

#pragma parameter spike "          Scanline Spike Removal" 1.0 0.0 2.0 0.10
#define spike params.spike

#define COMPAT_TEXTURE(c,d) texture(c,d)
#define TEX0 vTexCoord

#define OutputSize params.OutputSize
#define gl_FragCoord (vTexCoord * OutputSize.xy)

#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 * 1.00001;
}

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

float gaussian(float x)
{
	float invsqrsigma = 1.0/(2.0*SIGMA_HOR*SIGMA_HOR);
	return exp(-x*x*invsqrsigma);
}

void main()
{	
	vec2 prescalex = vec2(textureSize(LinearizePass, 0)) / params.OriginalSize.xy;

	vec4 SourceSize = params.OriginalSize * vec4(prescalex.x, prescalex.y, 1.0/prescalex.x, 1.0/prescalex.y);

	float f = fract(SourceSize.x * vTexCoord.x);
	f = 0.5 - f;
	vec2 tex = floor(SourceSize.xy * vTexCoord)*SourceSize.zw + 0.5*SourceSize.zw;
	vec3 color = 0.0.xxx;
	float scolor = 0.0;
	vec2 dx  = vec2(SourceSize.z, 0.0);

	float w = 0.0;
	float swsum = 0.0;
	float wsum = 0.0;
	vec3 pixel;
	
	float hsharpness = HSHARPNESS;
	vec3 cmax = 0.0.xxx;
	vec3 cmin = 1.0.xxx;
	float sharp = gaussian(hsharpness) * S_SHARP;
	float maxsharp = MAXS;
	float FPR = hsharpness;
	float fpx = 0.0;
	float sp = 0.0;
	float sw = 0.0;

	float ts = 0.025;
	vec3 luma = vec3(0.2126, 0.7152, 0.0722); 

	float LOOPSIZE = ceil(2.0*FPR);
	float CLAMPSIZE = round(2.0*LOOPSIZE/3.0);
	
	float n = -LOOPSIZE;
	
	do
	{
		pixel  = COMPAT_TEXTURE(LinearizePass, tex + n*dx).rgb;
		sp = max(max(pixel.r,pixel.g),pixel.b);
		
		w = gaussian(n+f) - sharp;
		fpx = abs(n+f-sign(n)*FPR)/FPR;
		if (abs(n) <= CLAMPSIZE) { cmax = max(cmax, pixel); cmin = min(cmin, pixel); }
		if (w < 0.0) w = clamp(w, mix(-maxsharp, 0.0, pow(clamp(fpx,0.0,1.0), HSHARP)), 0.0);
	
		color = color + w * pixel;
		wsum  = wsum + w;

		sw = max(w, 0.0) * (dot(pixel,luma) + ts); 
		scolor = scolor + sw * sp;
		swsum = swsum + sw;
		
		n = n + 1.0;
			
	} while (n <= LOOPSIZE);

	color = color / wsum;
	scolor = scolor / swsum;
	
	color = clamp(mix(clamp(color, cmin, cmax), color, HARNG), 0.0, 1.0); 
	
	scolor = clamp(mix(max(max(color.r, color.g),color.b), scolor, spike), 0.0, 1.0);
	
	FragColor = vec4(color, scolor);
}
4 Likes

Have you been able to take advantage of the local dimming on this yet when using it with CRT Shaders?

2 Likes

Unfortunately the local dimming with shaders is kind of a bummer, just dims the entire image too much and causes some unwanted changes to the colors. It’s awesome with modern games, though.

2 Likes