True, but it’s worth mentioning that most CRTs oversaturate red a lot when playing video games over composite or RF, which causes the electron gun bleed to increase compared to RGB or YPbPr component.
About the chroma lowpass/bandpass which NTSC shaders already kind of do, the “right way” to do them is to simulate the lowpass/bandpass that’s found in the schematics of the actual console and CRT. That’s how to get that blur to go to the right instead of symmetrical. There must be some way to simulate this quickly and efficiently, but I haven’t tried yet myself.
As said before, the electron gun bleed is different.
so it mixed of both, classic analog 
One of the reasons I wrote this is not just to simulate/emulate faulty CRT monitors, but because I know that even working monitors have these effects, albeit to a lesser extent. Therefore, I believe they are necessary to achieve the authentic experience, of course in small and reasonable proportions that don’t negatively affect the image
You just need to use an IIR filter as opposed to an FIR filter.
Recent test:
Added Grade, CRT Contrast +60%
(Color) Saturation 1.20
I think this is good but I don’t know why I have to do things to Saturation and Contrast- the difficulty here is that I’m not sure what’s expected.
shaders = "21"
feedback_pass = "0"
shader0 = "shaders_slang/p68k-fast-multipass-2025-11-07/p68k-fast-mp-pre-color-only.slang"
alias0 = ""
wrap_mode0 = "clamp_to_border"
mipmap_input0 = "false"
filter_linear0 = "false"
frame_count_mod0 = "2"
float_framebuffer0 = "true"
srgb_framebuffer0 = "false"
scale_type_x0 = "source"
scale_x0 = "1.000000"
scale_type_y0 = "source"
scale_y0 = "1.000000"
shader1 = "shaders_slang/p68k-fast-multipass-2025-11-07/p68k-fast-mp-post-color-only.slang"
alias1 = ""
wrap_mode1 = "clamp_to_border"
mipmap_input1 = "false"
filter_linear1 = "false"
frame_count_mod1 = "2"
float_framebuffer1 = "true"
srgb_framebuffer1 = "false"
scale_type_x1 = "source"
scale_x1 = "1.000000"
scale_type_y1 = "source"
scale_y1 = "1.000000"
shader2 = "shaders_slang/misc/shaders/grade-no-LUT.slang"
alias2 = ""
wrap_mode2 = "clamp_to_border"
mipmap_input2 = "false"
float_framebuffer2 = "false"
srgb_framebuffer2 = "false"
scale_type_x2 = "viewport"
scale_x2 = "1.000000"
scale_type_y2 = "viewport"
scale_y2 = "1.000000"
shader3 = "shaders_slang/crt/shaders/guest/advanced/stock.slang"
alias3 = ""
wrap_mode3 = "clamp_to_border"
mipmap_input3 = "false"
filter_linear3 = "false"
float_framebuffer3 = "false"
srgb_framebuffer3 = "false"
scale_type_x3 = "source"
scale_x3 = "1.000000"
scale_type_y3 = "source"
scale_y3 = "1.000000"
shader4 = "shaders_slang/crt/shaders/guest/advanced/stock.slang"
alias4 = "StockPass"
wrap_mode4 = "clamp_to_border"
mipmap_input4 = "false"
filter_linear4 = "false"
float_framebuffer4 = "false"
srgb_framebuffer4 = "false"
scale_type_x4 = "source"
scale_x4 = "1.000000"
scale_type_y4 = "source"
scale_y4 = "1.000000"
shader5 = "shaders_slang/crt/shaders/guest/advanced/afterglow0.slang"
alias5 = "AfterglowPass"
wrap_mode5 = "clamp_to_border"
mipmap_input5 = "false"
filter_linear5 = "true"
float_framebuffer5 = "false"
srgb_framebuffer5 = "false"
scale_type_x5 = "source"
scale_x5 = "1.000000"
scale_type_y5 = "source"
scale_y5 = "1.000000"
shader6 = "shaders_slang/crt/shaders/guest/advanced/pre-shaders-afterglow.slang"
alias6 = "PrePass0"
wrap_mode6 = "clamp_to_border"
mipmap_input6 = "false"
filter_linear6 = "true"
float_framebuffer6 = "false"
srgb_framebuffer6 = "false"
scale_type_x6 = "source"
scale_x6 = "1.000000"
scale_type_y6 = "source"
scale_y6 = "1.000000"
shader7 = "shaders_slang/crt/shaders/guest/advanced/ntsc/ntsc-pass1.slang"
alias7 = "NPass1"
wrap_mode7 = "clamp_to_border"
mipmap_input7 = "false"
filter_linear7 = "false"
float_framebuffer7 = "true"
srgb_framebuffer7 = "false"
scale_type_x7 = "source"
scale_x7 = "4.000000"
scale_type_y7 = "source"
scale_y7 = "1.000000"
shader8 = "shaders_slang/crt/shaders/guest/advanced/ntsc/ntsc-pass2.slang"
alias8 = ""
wrap_mode8 = "clamp_to_border"
mipmap_input8 = "false"
filter_linear8 = "true"
float_framebuffer8 = "true"
srgb_framebuffer8 = "false"
scale_type_x8 = "source"
scale_x8 = "0.500000"
scale_type_y8 = "source"
scale_y8 = "1.000000"
shader9 = "shaders_slang/crt/shaders/guest/advanced/ntsc/ntsc-pass3.slang"
alias9 = ""
wrap_mode9 = "clamp_to_border"
mipmap_input9 = "false"
filter_linear9 = "true"
float_framebuffer9 = "false"
srgb_framebuffer9 = "false"
scale_type_x9 = "source"
scale_x9 = "1.000000"
scale_type_y9 = "source"
scale_y9 = "1.000000"
shader10 = "shaders_slang/crt/shaders/guest/advanced/custom-fast-sharpen.slang"
alias10 = "NtscPass"
wrap_mode10 = "clamp_to_border"
mipmap_input10 = "false"
filter_linear10 = "true"
float_framebuffer10 = "false"
srgb_framebuffer10 = "false"
scale_type_x10 = "source"
scale_x10 = "1.000000"
scale_type_y10 = "source"
scale_y10 = "1.000000"
shader11 = "shaders_slang/crt/shaders/guest/advanced/stock.slang"
alias11 = "PrePass"
wrap_mode11 = "clamp_to_border"
mipmap_input11 = "true"
filter_linear11 = "true"
float_framebuffer11 = "false"
srgb_framebuffer11 = "false"
scale_type_x11 = "source"
scale_x11 = "1.000000"
scale_type_y11 = "source"
scale_y11 = "1.000000"
shader12 = "shaders_slang/crt/shaders/guest/advanced/avg-lum-ntsc.slang"
alias12 = "AvgLumPass"
wrap_mode12 = "clamp_to_border"
mipmap_input12 = "true"
filter_linear12 = "true"
float_framebuffer12 = "false"
srgb_framebuffer12 = "false"
scale_type_x12 = "source"
scale_x12 = "1.000000"
scale_type_y12 = "source"
scale_y12 = "1.000000"
shader13 = "shaders_slang/crt/shaders/guest/advanced/linearize-ntsc.slang"
alias13 = "LinearizePass"
wrap_mode13 = "clamp_to_border"
mipmap_input13 = "false"
filter_linear13 = "true"
float_framebuffer13 = "true"
srgb_framebuffer13 = "false"
scale_type_x13 = "source"
scale_x13 = "1.000000"
scale_type_y13 = "source"
scale_y13 = "1.000000"
shader14 = "shaders_slang/crt/shaders/guest/advanced/crt-guest-advanced-ntsc-pass1.slang"
alias14 = "Pass1"
wrap_mode14 = "clamp_to_border"
mipmap_input14 = "false"
filter_linear14 = "true"
float_framebuffer14 = "true"
srgb_framebuffer14 = "false"
scale_type_x14 = "viewport"
scale_x14 = "1.000000"
scale_type_y14 = "source"
scale_y14 = "1.000000"
shader15 = "shaders_slang/crt/shaders/guest/hd/gaussian_horizontal.slang"
alias15 = ""
wrap_mode15 = "clamp_to_border"
mipmap_input15 = "false"
filter_linear15 = "true"
float_framebuffer15 = "true"
srgb_framebuffer15 = "false"
scale_type_x15 = "absolute"
scale_x15 = "800"
scale_type_y15 = "source"
scale_y15 = "1.000000"
shader16 = "shaders_slang/crt/shaders/guest/advanced/gaussian_vertical.slang"
alias16 = "GlowPass"
wrap_mode16 = "clamp_to_border"
mipmap_input16 = "false"
filter_linear16 = "true"
float_framebuffer16 = "true"
srgb_framebuffer16 = "false"
scale_type_x16 = "absolute"
scale_x16 = "800"
scale_type_y16 = "absolute"
scale_y16 = "600"
shader17 = "shaders_slang/crt/shaders/guest/hd/bloom_horizontal.slang"
alias17 = ""
wrap_mode17 = "clamp_to_border"
mipmap_input17 = "false"
filter_linear17 = "true"
float_framebuffer17 = "true"
srgb_framebuffer17 = "false"
scale_type_x17 = "absolute"
scale_x17 = "800"
scale_type_y17 = "absolute"
scale_y17 = "600"
shader18 = "shaders_slang/crt/shaders/guest/advanced/bloom_vertical.slang"
alias18 = "BloomPass"
wrap_mode18 = "clamp_to_border"
mipmap_input18 = "false"
filter_linear18 = "true"
float_framebuffer18 = "true"
srgb_framebuffer18 = "false"
scale_type_x18 = "absolute"
scale_x18 = "800"
scale_type_y18 = "absolute"
scale_y18 = "600"
shader19 = "shaders_slang/crt/shaders/guest/advanced/crt-guest-advanced-ntsc-pass2.slang"
alias19 = ""
wrap_mode19 = "clamp_to_border"
mipmap_input19 = "false"
filter_linear19 = "true"
float_framebuffer19 = "true"
srgb_framebuffer19 = "false"
scale_type_x19 = "viewport"
scale_x19 = "1.000000"
scale_type_y19 = "viewport"
scale_y19 = "1.000000"
shader20 = "shaders_slang/crt/shaders/guest/advanced/deconvergence-ntsc.slang"
alias20 = ""
wrap_mode20 = "clamp_to_border"
mipmap_input20 = "false"
filter_linear20 = "true"
float_framebuffer20 = "false"
srgb_framebuffer20 = "false"
scale_type_x20 = "viewport"
scale_x20 = "1.000000"
scale_type_y20 = "viewport"
scale_y20 = "1.000000"
pfm_stdApproxGamma = "3.000000"
pf_gamma = "1.999996"
pf_color = "1.200000"
pf_hdr_enable = "2.000000"
g_CRT_l = "2.600000"
g_CRT_c = "80.000000"
wlr = "1.020000"
gr = "0.020000"
br = "0.020000"
cust_artifacting = "0.000000"
cust_fringing = "0.000000"
ntsc_scale = "1.500000"
ntsc_taps = "6.000000"
GAMMA_INPUT = "2.200000"
gamma_out = "2.200000"
m_glow = "2.000000"
glow = "0.020000"
brightboost = "1.000000"
brightboost1 = "1.000000"
gsl = "2.000000"
scanline1 = "5.000000"
scanline2 = "35.000000"
beam_min = "1.200000"
beam_size = "0.000000"
scans = "1.000000"
shadowMask = "12.000000"
maskstr = "0.800000"
mcut = "1.000000"
post_br = "1.500000"
textures = "SamplerLUT1;SamplerLUT2;SamplerLUT3;SamplerLUT4"
SamplerLUT1 = "shaders_slang/crt/shaders/guest/advanced/lut/trinitron-lut.png"
SamplerLUT1_mipmap = "false"
SamplerLUT1_wrap_mode = "clamp_to_border"
SamplerLUT2 = "shaders_slang/crt/shaders/guest/advanced/lut/inv-trinitron-lut.png"
SamplerLUT2_mipmap = "false"
SamplerLUT2_wrap_mode = "clamp_to_border"
SamplerLUT3 = "shaders_slang/crt/shaders/guest/advanced/lut/nec-lut.png"
SamplerLUT3_mipmap = "false"
SamplerLUT3_wrap_mode = "clamp_to_border"
SamplerLUT4 = "shaders_slang/crt/shaders/guest/advanced/lut/ntsc-lut.png"
SamplerLUT4_mipmap = "false"
SamplerLUT4_wrap_mode = "clamp_to_border"
And here’s another with no grade, default settings for guest-advanced. I have no idea what changed. Using the version from “p68k-fast-multipass-2025-11-07”
Edit: It seems to be only the ntsc colors preset that’s working, the others all result in the washed out image
shaders = "21"
feedback_pass = "0"
shader0 = "shaders_slang/p68k-fast-multipass-2025-11-07/p68k-fast-mp-pre-color-only.slang"
alias0 = ""
wrap_mode0 = "clamp_to_border"
mipmap_input0 = "false"
filter_linear0 = "false"
frame_count_mod0 = "2"
float_framebuffer0 = "true"
srgb_framebuffer0 = "false"
scale_type_x0 = "source"
scale_x0 = "1.000000"
scale_type_y0 = "source"
scale_y0 = "1.000000"
shader1 = "shaders_slang/p68k-fast-multipass-2025-11-07/p68k-fast-mp-post-color-only.slang"
alias1 = ""
wrap_mode1 = "clamp_to_border"
mipmap_input1 = "false"
filter_linear1 = "false"
frame_count_mod1 = "2"
float_framebuffer1 = "true"
srgb_framebuffer1 = "false"
scale_type_x1 = "source"
scale_x1 = "1.000000"
scale_type_y1 = "source"
scale_y1 = "1.000000"
shader2 = "shaders_slang/stock.slang"
alias2 = ""
wrap_mode2 = "clamp_to_border"
mipmap_input2 = "false"
float_framebuffer2 = "false"
srgb_framebuffer2 = "false"
scale_type_x2 = "viewport"
scale_x2 = "1.000000"
scale_type_y2 = "viewport"
scale_y2 = "1.000000"
shader3 = "shaders_slang/crt/shaders/guest/advanced/stock.slang"
alias3 = ""
wrap_mode3 = "clamp_to_border"
mipmap_input3 = "false"
filter_linear3 = "false"
float_framebuffer3 = "false"
srgb_framebuffer3 = "false"
scale_type_x3 = "source"
scale_x3 = "1.000000"
scale_type_y3 = "source"
scale_y3 = "1.000000"
shader4 = "shaders_slang/crt/shaders/guest/advanced/stock.slang"
alias4 = "StockPass"
wrap_mode4 = "clamp_to_border"
mipmap_input4 = "false"
filter_linear4 = "false"
float_framebuffer4 = "false"
srgb_framebuffer4 = "false"
scale_type_x4 = "source"
scale_x4 = "1.000000"
scale_type_y4 = "source"
scale_y4 = "1.000000"
shader5 = "shaders_slang/crt/shaders/guest/advanced/afterglow0.slang"
alias5 = "AfterglowPass"
wrap_mode5 = "clamp_to_border"
mipmap_input5 = "false"
filter_linear5 = "true"
float_framebuffer5 = "false"
srgb_framebuffer5 = "false"
scale_type_x5 = "source"
scale_x5 = "1.000000"
scale_type_y5 = "source"
scale_y5 = "1.000000"
shader6 = "shaders_slang/crt/shaders/guest/advanced/pre-shaders-afterglow.slang"
alias6 = "PrePass0"
wrap_mode6 = "clamp_to_border"
mipmap_input6 = "false"
filter_linear6 = "true"
float_framebuffer6 = "false"
srgb_framebuffer6 = "false"
scale_type_x6 = "source"
scale_x6 = "1.000000"
scale_type_y6 = "source"
scale_y6 = "1.000000"
shader7 = "shaders_slang/crt/shaders/guest/advanced/ntsc/ntsc-pass1.slang"
alias7 = "NPass1"
wrap_mode7 = "clamp_to_border"
mipmap_input7 = "false"
filter_linear7 = "false"
float_framebuffer7 = "true"
srgb_framebuffer7 = "false"
scale_type_x7 = "source"
scale_x7 = "4.000000"
scale_type_y7 = "source"
scale_y7 = "1.000000"
shader8 = "shaders_slang/crt/shaders/guest/advanced/ntsc/ntsc-pass2.slang"
alias8 = ""
wrap_mode8 = "clamp_to_border"
mipmap_input8 = "false"
filter_linear8 = "true"
float_framebuffer8 = "true"
srgb_framebuffer8 = "false"
scale_type_x8 = "source"
scale_x8 = "0.500000"
scale_type_y8 = "source"
scale_y8 = "1.000000"
shader9 = "shaders_slang/crt/shaders/guest/advanced/ntsc/ntsc-pass3.slang"
alias9 = ""
wrap_mode9 = "clamp_to_border"
mipmap_input9 = "false"
filter_linear9 = "true"
float_framebuffer9 = "false"
srgb_framebuffer9 = "false"
scale_type_x9 = "source"
scale_x9 = "1.000000"
scale_type_y9 = "source"
scale_y9 = "1.000000"
shader10 = "shaders_slang/crt/shaders/guest/advanced/custom-fast-sharpen.slang"
alias10 = "NtscPass"
wrap_mode10 = "clamp_to_border"
mipmap_input10 = "false"
filter_linear10 = "true"
float_framebuffer10 = "false"
srgb_framebuffer10 = "false"
scale_type_x10 = "source"
scale_x10 = "1.000000"
scale_type_y10 = "source"
scale_y10 = "1.000000"
shader11 = "shaders_slang/crt/shaders/guest/advanced/stock.slang"
alias11 = "PrePass"
wrap_mode11 = "clamp_to_border"
mipmap_input11 = "true"
filter_linear11 = "true"
float_framebuffer11 = "false"
srgb_framebuffer11 = "false"
scale_type_x11 = "source"
scale_x11 = "1.000000"
scale_type_y11 = "source"
scale_y11 = "1.000000"
shader12 = "shaders_slang/crt/shaders/guest/advanced/avg-lum-ntsc.slang"
alias12 = "AvgLumPass"
wrap_mode12 = "clamp_to_border"
mipmap_input12 = "true"
filter_linear12 = "true"
float_framebuffer12 = "false"
srgb_framebuffer12 = "false"
scale_type_x12 = "source"
scale_x12 = "1.000000"
scale_type_y12 = "source"
scale_y12 = "1.000000"
shader13 = "shaders_slang/crt/shaders/guest/advanced/linearize-ntsc.slang"
alias13 = "LinearizePass"
wrap_mode13 = "clamp_to_border"
mipmap_input13 = "false"
filter_linear13 = "true"
float_framebuffer13 = "true"
srgb_framebuffer13 = "false"
scale_type_x13 = "source"
scale_x13 = "1.000000"
scale_type_y13 = "source"
scale_y13 = "1.000000"
shader14 = "shaders_slang/crt/shaders/guest/advanced/crt-guest-advanced-ntsc-pass1.slang"
alias14 = "Pass1"
wrap_mode14 = "clamp_to_border"
mipmap_input14 = "false"
filter_linear14 = "true"
float_framebuffer14 = "true"
srgb_framebuffer14 = "false"
scale_type_x14 = "viewport"
scale_x14 = "1.000000"
scale_type_y14 = "source"
scale_y14 = "1.000000"
shader15 = "shaders_slang/crt/shaders/guest/hd/gaussian_horizontal.slang"
alias15 = ""
wrap_mode15 = "clamp_to_border"
mipmap_input15 = "false"
filter_linear15 = "true"
float_framebuffer15 = "true"
srgb_framebuffer15 = "false"
scale_type_x15 = "absolute"
scale_x15 = "800"
scale_type_y15 = "source"
scale_y15 = "1.000000"
shader16 = "shaders_slang/crt/shaders/guest/advanced/gaussian_vertical.slang"
alias16 = "GlowPass"
wrap_mode16 = "clamp_to_border"
mipmap_input16 = "false"
filter_linear16 = "true"
float_framebuffer16 = "true"
srgb_framebuffer16 = "false"
scale_type_x16 = "absolute"
scale_x16 = "800"
scale_type_y16 = "absolute"
scale_y16 = "600"
shader17 = "shaders_slang/crt/shaders/guest/hd/bloom_horizontal.slang"
alias17 = ""
wrap_mode17 = "clamp_to_border"
mipmap_input17 = "false"
filter_linear17 = "true"
float_framebuffer17 = "true"
srgb_framebuffer17 = "false"
scale_type_x17 = "absolute"
scale_x17 = "800"
scale_type_y17 = "absolute"
scale_y17 = "600"
shader18 = "shaders_slang/crt/shaders/guest/advanced/bloom_vertical.slang"
alias18 = "BloomPass"
wrap_mode18 = "clamp_to_border"
mipmap_input18 = "false"
filter_linear18 = "true"
float_framebuffer18 = "true"
srgb_framebuffer18 = "false"
scale_type_x18 = "absolute"
scale_x18 = "800"
scale_type_y18 = "absolute"
scale_y18 = "600"
shader19 = "shaders_slang/crt/shaders/guest/advanced/crt-guest-advanced-ntsc-pass2.slang"
alias19 = ""
wrap_mode19 = "clamp_to_border"
mipmap_input19 = "false"
filter_linear19 = "true"
float_framebuffer19 = "true"
srgb_framebuffer19 = "false"
scale_type_x19 = "viewport"
scale_x19 = "1.000000"
scale_type_y19 = "viewport"
scale_y19 = "1.000000"
shader20 = "shaders_slang/crt/shaders/guest/advanced/deconvergence-ntsc.slang"
alias20 = ""
wrap_mode20 = "clamp_to_border"
mipmap_input20 = "false"
filter_linear20 = "true"
float_framebuffer20 = "false"
srgb_framebuffer20 = "false"
scale_type_x20 = "viewport"
scale_x20 = "1.000000"
scale_type_y20 = "viewport"
scale_y20 = "1.000000"
pf_hdr_enable = "2.000000"
textures = "SamplerLUT1;SamplerLUT2;SamplerLUT3;SamplerLUT4"
SamplerLUT1 = "shaders_slang/crt/shaders/guest/advanced/lut/trinitron-lut.png"
SamplerLUT1_mipmap = "false"
SamplerLUT1_wrap_mode = "clamp_to_border"
SamplerLUT2 = "shaders_slang/crt/shaders/guest/advanced/lut/inv-trinitron-lut.png"
SamplerLUT2_mipmap = "false"
SamplerLUT2_wrap_mode = "clamp_to_border"
SamplerLUT3 = "shaders_slang/crt/shaders/guest/advanced/lut/nec-lut.png"
SamplerLUT3_mipmap = "false"
SamplerLUT3_wrap_mode = "clamp_to_border"
SamplerLUT4 = "shaders_slang/crt/shaders/guest/advanced/lut/ntsc-lut.png"
SamplerLUT4_mipmap = "false"
SamplerLUT4_wrap_mode = "clamp_to_border"
@PlainOldPants apologies if you already discussed this somewhere but I was wondering if you have done gamut analysis on more modern TV color enhancement features like on an LCD. It would be interesting if it is at all related to what consumer CRT TVs are doing with color.
I have not done that sort of analysis on any modern displays yet. I have a couple modern TV sets, which don’t have great image quality, but I seldom use them. They’re mainly used by other family members that I live with.
At least, I have tried playing around with settings. I was able to turn off a couple settings that had been “enhancing” the image by oversaturating it and causing details to get clamped and disappear. One thing that stuck out to me was that an LG set had a “Dynamic Color” feature that could be set to Off, Low, Mid, or High, and it was keeping colors near the red/yellow/green boundary in place while moving other colors towards blue. I might try to get pictures of this, and maybe get rough measurements with the X-Rite i1Display 2.
As for now, I don’t have the time to be working on anything here, but I’ve been checking on these forums every once in a while. I have too many other things going on.
It’s been too long since I last posted here. I just made this set of NES palettes and 128x128x128 LUTs based on consumer CRTs. The Panasonic, Toshiba, and RCA ones are the ones I actually own and have measured myself, whereas the Sony ones are solely based on data found online. All of this is generated using Chthon’s tool gamutthingy, with my own modifications to have a straight 2.2 power law gamma instead of Rec. 1886, and to manually adjust contrast to fit the colors better. That’s unlike my previous posts where I took colorimeter samples directly, which was not good because of how uncalibrated the CRTs were. For more information, see the included text document, but it’ll probably only leave you more confused.
https://drive.google.com/drive/folders/1Nww5ce7xO7j9_2M-_DUmCY1QuVU7jzng Using Google Drive instead of MediaFire to avoid dealing with ads.
Edit: I’ve just realized, the NES palettes are all a little bit wrong because I forgot to set the correct black levels. They are all set so that $1d is black. I’ll edit this post again later once I’ve uploaded palettes with the correct black levels, at 7.5 IRE for US palettes and at 0 IRE for JP palettes. The LUTs also have the CRT’s black level adjusted to match the console’s black level, but I’ve heard that consoles often output the same black level voltage for all regions, meaning either US TVs got crushed blacks, or JP TVs had lifted blacks. As for PAL, I don’t have any decent PAL CRT data yet.
I can be somewhat helpful with black levels: All CRT-era game consoles before circa 2000, except U.S. model Playstations, had black at 0 IRE. There’s citations for this in the gamutthingy readme.
U.S. users could uncrush blacks by turning up the brightness knob on their CRT.
U.S. developers may or may not have baked crushed blacks into their pixel artwork, depending on how the televisions hooked up to their dev kits had their brightness knob set.
Aside from eyeballing, you might be able to sort out if a given game has baked-in crushed blacks by checking its artworks’ palettes for colors that would crush if black was at 7.5 IRE. Presumably such colors would not have been used if they didn’t produce a visible difference on the artist’s screen. (This method is not useful for NES though, since the luma step size was too big.)
Thank you. I’ll look at the sources from the gamutthingy readme.
I doubt that the NES has its black level exactly at 0 IRE or 7.5 IRE. That is why I want to either take a video capture of my NES or use my colorimeter to sample an actual CRT to determine this. The Genesis/MegaDrive 240p test suite claims to have a black level of about 6 IRE.
Temporarily, I might as well pretend that these consoles are all exactly 0 or 7.5 IRE.
Figuring out the artist’s intent is a complex problem that requires looking across each game independently. Because of that complexity, I’m not trying to do the artist’s intent yet.
Instead, my goal for now is to get to what a typical end-user would see on these displays, which is a much simpler problem because, like myself, these end users don’t know the artist’s intent either. In that case, the end user would adjust their CRT for live broadcasts and leave it unchanged for video games. For specifics on how I did this, read the readme file that I included in that google drive link with the other LUTs and NES palettes.
For that purpose, it makes sense to leave the blacks crushed, even though it’s possible to fix using the brightness control.
There are some other ways I might try to set the black level and tint/color settings. I just haven’t gotten around to doing them yet.
One way that I’ve been meaning to try is watching YouTube videos (of real life) through ShaderGlass with the CRT color shaders and adjusting the settings by eye that way, including the black level. Maybe I could upload the shaders online too and crowd-source the settings. Doing this with my real 1989 RCA ColorTrak Remote is an option too, but it’ll be a bit wrong because the grayscale is slightly off from its intended point.
The other way is through a CRT’s default settings. Consumer-grade CRTs (but not professional ones like PVMs) starting in the late 80s typically have the contrast/brightness/tint/color/sharpness settings in an on-screen display with a “reset” button to get the defaults. As a kid (in the 2000s), I never knew that these settings existed, so chances are the average consumer didn’t ever touch them. Unfortunately, none of my CRTs have their default black level intact. I should at least be able to approximate the default tint/color from the 2000 Panasonic CRT, but not the default black level.
The black level of video is defined as 0 IRE (aside from systems using setup) by definition. This is how IRE works. It’s not tied to a particular voltage or nit value. The higher voltage of blanking is taken as the voltage for 0 IRE. 100 IRE is 714 mV above blanking. So this means consoles may have a white level slightly off 100 IRE but the black level will be 0 as long as black is the same color as the high blanking level.
The NES uses the same palette color for high blanking and black, so you can treat it as 0 IRE.
With 7.5 IRE setup, blanking level is still 0 and 100 is still always 714 mV. This means setup might not actually be perfectly at 7.5 IRE.
For a number of reasons, you can’t precisely measure IRE off a CRT with a colorimeter. You can get a rough estimate but it’s not going to be precise enough for what you need. Use an oscilloscope instead with 75 Ohm termination. Once you have voltage levels, you can reliably measure luminance off the CRT (say at 10 IRE steps). Plug that into one of the BT.1886 equations, and you should be able to get something close to a 2.4 power function (it doesn’t need to be perfectly linear, it will likely fluctuate all over from under 2 to 3). If you do, you know your IRE measurements are in the ballpark.
Also there is no ‘default’ black level. CRTs are analog. They’re not digital. Two identical TVs could have different black levels off the belt. Manufacturers did a very basic factory calibration to get in suitable range for TV but that would go out of date quickly as the components age rapidly within the first 100 hours of use.
The part I don’t understand is how the voltages for black and blanking are meant to be detected and kept consistent by the CRT display when switching between different input devices or TV channels, if 0 IRE is not consistently at the same voltage.
And yes, by “defaults” I meant the factory calibration, which is displayed to the user as all settings being centered except contrast (unless the TV only had physical knobs for settings, unlike later ones which had an OSD), but if you’re right about how much this black level varies across identical TVs and ages in 100 hours of use, then it’s surely not worth it to recover this, even if I were to average several CRTs.
About measuring IRE off a CRT, what I was planning to do was something similar to my nesdev forum post about the Toshiba Blackstripe color correction, where I sample up the grayscale from my HDMI-to-composite convertor at 3 IRE (or so) intervals to create a regression function, and then invert that function to convert from sampled NES grays back to IRE levels. As you’ve said, this wouldn’t be precise. I’ve tried using a Dazzle DVC100 to capture the video before, but I remember it having some kind of issues that I couldn’t diagnose.
If you’re confused about why I had a paragraph about picking a black level by eye while watching actual videos, that goes back to my main goal of matching what an end user would see on average, even if it’s not necessarily correct or precise.
The idea is to set the TV’s settings based on actual video, and then see what happens when I switch to channel 3 or 4 or to a composite input to play games. This is why I need to understand what “0 IRE” truly is when switching between different video sources, so I appreciate your help and your patience.
Until I get data using an oscilloscope, the best idea may be to use the simple rule of 0 IRE and 7.5 IRE for crushing or lifting blacks, and assume every console’s max white is 100 IRE when doing that.
Edit: I had to check and make sure I’m not crazy, but yes, even late 1980s TVs can have digital on-screen controls and a “reset” feature that maxes out contrast and centers everything else. https://crtdatabase.com/crts/sony/sony-kv-19-ts20 The manual for this one shows the year 1989.
Video signals are what we call AC-coupled signals, meaning we only care about the AC part. This was intentionally designed so we can ignore the DC bias, something much more difficult to control when RF transmission is involved. So when we talk about voltage of a video signal, it’s always relative to that inherit DC bias that exists on every system. The TV has what we call a clamping circuit (not to be confused with what clamp in GLSL does, we would call that ‘hard limiting’) that removes the DC bias relative to the TV (i.e. it sets the DC bias to whatever the TV’s DC bias for video signals is). That clamping circuit operates on the ‘back porch’ portion of the video signal, which is defined as 0 IRE. This is why 0 IRE is defined as the high blanking level. The hardware literally uses that level as its own voltage reference, so it makes sense to define 0 IRE in the same way.
I can give you some history here. The reason we have calibration is because of response changes in systems over time. In any business, whether it’s a TV studio or some research laboratory, only work that needs to get done gets done. Calibration is only done on things needing it. So the habit of calibrating screens came from that need. LCDs can be calibrated far less frequently. If CRTs had all come from the factory looking the same and never changed their response, that culture of calibration would never have developed in the first place.
I am just warning you that this will take a lot of time and will probably leave you unsatisfied with the results.
I’m curious how much this differs as well. You need a strong baseline for one side of the transmission line to do this, which is why I recommend a scope. If you’re just doing luminance measurements, something like a high-quality DVD player with 0 IRE black mode and a test DVD can be good enough as the reference. Then you can measure gray window steps on different inputs/channels and see how the levels change.
