@beans lots of interest indeed! I just updated my shaders and found yours. Trying it out at the moment, and I like what I’m seeing a lot, it is next gen GTU. Excellent work 
This is a cool shader, I like the automatic brightness mitigation idea, but it’s too bright on my HDR1000 display. Some additional mask controls (to make the mask darker) would go a long way towards making this shader compatible with a wider range of displays. Maybe an “override brightness mitigation with mask strength” option?
I want to add a mask strength parameter along with the NTSC simulation. I’m out of town and won’t be able to finish this up until next week at the earliest.
@beans Your Mega Drive filters seem reasonable, assuming low output impedances and high input impedances (as it appears from the data sheet I have, though no specific values are given). The luma filter should start to roll off at around 8 MHz. That roll off does not become strong until about 17-18 MHz. This is because the notch filter is cascaded with a lowpass (it’s hard to determine where the cutoff is exactly because the capacitance (C34) is illegible, but it’s in video range). You can filter the two stages separately as the Y input has a high impedance, so the lowpass part does not significantly load the notch part. The existence of the lowpass is baffling to me and I don’t know why it’s there. Perhaps it was added to reduce some noise discovered late in design.
The CXA-1145 chip is supposed to have a 180 ns ‘delay line’ where these filters are installed. Now a notch filter certainly isn’t a delay line, but I was not able to find any info about what part would be used for a ‘delay line’ in this case and how that would affect the signal. I am guessing it would provide a better composite image than the notch filter by slightly misaligning the luma and chroma to reduce overlapping frequency peaks (changes in luma generally correspond to changes in chroma), but this is totally speculation.
The CXA-1145 has a minimum RGB output frequency response of 5.0 MHz. The data sheet shows a much higher range in a lab setup. The Y and C frequency responses are the same.
The chroma filter is a bandpass centered a little off subcarrier frequency. The design overlaps the two filters so that the result ends up broader than I expected. It’s highly dependent on the input and output impedance, but no less than 2.8 MHz or so. That equates to a baseband chroma bandwidth of 1.4 MHz, close to the 1.3 MHz SMPTE encoding standard.
Conclusions: Between the SNES and Genesis we see that chroma is filtered for both, but the SNES allows a wider bandwidth. Only the Genesis employs a notch filter, and does so in place of a delay line puting its use of its video chip out-of-spec. The SNES does not. The Genesis also imposes an arbitrary lowpass filter on its luma. Combined with the notch, this results in a very poor luma frequency response. The SNES does not filter its luma at all, in either composite or S-Video. Neither systems filter their RGB.
Edit: I realized I posted this in the wrong thread, but I guess you can figure out what I’m talking about anyway.
The Mega Drive’s lack of delay line makes sense actually. It’s exactly why the dot pattern doesn’t shift over lines and we get the strong rainbow effects.
I think this is better anyway. I don’t want to take over @PlainOldPants’ thread.
The Mega Drive’s lack of delay line makes sense actually. It’s exactly why the dot pattern doesn’t shift over lines and we get the strong rainbow effects.
I think that is actually because the Mega Drive’s line length is 228 chroma cycles long, instead of the standard 227.5.
I had assumed that the delay line was supposed to compensate for delay from the chroma filter, assuming that luma wouldn’t be filtered and would therefore be ahead of the chroma. As it is, I don’t know if the filters would have the same delay, so the chroma and luma might be slightly misaligned.
The CXA-1145 has a minimum RGB output frequency response of 5.0 MHz. The data sheet shows a much higher range in a lab setup. The Y and C frequency responses are the same.
I suspect that the RGB signal is, in effect, filtered by the response of the DAC and buffers. The SNES, in particular the 2-chip varieties, has a notoriously soft RGB signal. The SNES behavior is complex, though, and would be hard to simulate.
The luma filter should start to roll off at around 8 MHz. That roll off does not become strong until about 17-18 MHz.
Are these numbers correct? That seems pretty high. TVs should be filtering out anything in this range at the inputs anyway.
it’s hard to determine where the cutoff is exactly because the capacitance (C34) is illegible, but it’s in video range
I found a forum post with the values. I think I linked it in the other thread, but I can pull it up later.
Thank you for your analysis! I’m out of town and away from my computer for now, but I want to resume looking at this later and I think I’ll have some questions. I think we can get pretty close to accurate composite simulations for at least a few consoles.
Well this is the sort of thing where it’s not easy to understand why its happening. Like I wouldn’t expect transistor frequency response degradation to have that nonlinear effect we see in the 2-chip version. And this is just one example, but the video quality can be very different even between the same generation of systems, so there should be caution with trying to simulate the exact response of something based off a single set of measurements. Here is a further discussion and the conclusion is that the issue may be due to poor component tolerances: https://www.retrorgb.com/snesversioncompare.html
They are correct. I am only looking at this filter, not the system frequency response. But keep in mind that at 8 MHz, the luma is already attenuated. That is, while the notch part is sloping back up until around 8 MHz, the overall response is attenuating starting at about 1.8 MHz. I am assuming a very low output impedance and high input impedance. As the impedances close in, the filter’s slopes soften.
There’s also the question of dynamic range. I don’t know what dynamic range analog TVs have, probably less than 8 effective bits. Standards treat -30 dB as enough attenuation to be considered blank. What we can conclude here is that the rolloff from the lowpass part outweighs the effect of the notch. Ignoring the notch part, and just filtering based off of the lowpass response should already give you a result very close to the actual console, especially if you use a notch in the TV simulation side.
I did find other schematics for other variations and it shows a 110 pF cap that is consistently used across versions. The frequency response graph you see uses that new 110 pF value.
Sure. When I started looking into this stuff years ago I found that it’s better to let good be the enemy of perfect. There are so many variables involved that it’s better to either do one thing very detailed or do many things in a more abstract, practical manner.
well, old crts indeed has a longer phosphor decay time, but I don’t think we can take the “Slow Mo Guys” clip as a reference because it’s been edited in terms of lighting, and video compression on YouTube and in editing has an effect in killing details
anyway I asked to get some shots so we can know better
but for now, we got https://www.youtube.com/watch?v=FOs8LfPifoA and seems there are some Phosphor-persistence from previous field
also
Yeah, I posted in another thread about phosphor persistence that I’ve changed my mind. I can see it on my CRT with bright, scrolling elements like the Nintendo logo drifting down the screen in the Super Mario World intro. I am thinking about the best way to do it. I’d like to get some photos so I can match the effect to the real thing, but I probably won’t have time for a while. I’d like to finish the NTSC simulation first.
What is the gif that you posted?
it’s from some youtube channel that do crt tv repairing, notice how the white text dont flickering in interlace
It’s easy to see the phosphor effect in person with a real CRT. The phosphors used for TVs stabilized by the late 70s. Before that, the decay times were longer, but once the phosphor formulation was stabilized, they are consistently reported as being 100-200 μs for green and blue and 1-2 ms for red to go to 10% of excitation brightness.
The question remains for how to map the input RGB signal to ‘peak brightness’ and I don’t have a good answer to that question. A typical CRT is under 100 cd/m^2 average white level, but the phosphors are much brighter than that at peak. If we assume the peak brightness is equal to input RGB we end up with a decay that’s too fast. There is also a secondary variable that causes the tail of the glow to hold for a longer period. That variable is 0.5 to 2.0, so two phosphors can have the same 10% decay time but one can have a longer tail than the other.
The other question is how impulses blend. For example in Frame 1, phosphor R gets excited at value A, then in Frame 2, phosphor R gets excited again at value B. Does the total excitation equal A+B or something else? Light is generally additive, but the phosphors are energized by electrons, not photons, and I don’t know if the phosphor could get ‘saturated’ and stop absorbing electrons at some point. I think most (all?) shaders use max(A, B) instead to avoid messing with the color balance. It’s easier to control, but (probably) less accurate.
FYI, one of the motivators for laser projectors was the fact that they don’t have a perceivable decay, so could be used for very fast scanning applications. Faster decaying phosphors were developed, but have compromises that lasers didn’t need to deal with.
I think, ideally, the output value for each frame would be the decay function integrated over the frame time. We could work backwards and figure out what the sum of all of those integrals is (basically the integral from 0 to infinity), and find the peak value that would result in a full brightness white value mapping to white on the screen. The integral should be the initial value over the decay constant (for exponential decay), so the math works out reasonably well. This is assuming the decayed value blends additively on reactivation.
I think there are two tricky points. One is as you said: how do we handle reactivation? Blending additively makes the math easy, but I have no idea if that is accurate.
The other is that the decay functions are either multi-exponential or a mixture of exponential and power law. I have no idea how to make power law decay work in a shader (it’s not memoryless and I have no idea how blending would work). Multi-exponential decay should be doable but it would have to be thought about. I wouldn’t want to expose a dozen parameters just for decay configuration.
I’d be tempted to ignore the different decay rates for different phosphor colors just because of the complexity it adds.
The decay impulse response is hyperbolic decay in the form 1/(1+g*t)^n where g and n are constants. You can implement this with feedback with good accuracy: ((x^(-1/n) + g)^-n, where x is the feedback color and g is scaled by the frame time (divide g by frame rate). This is what Scanline Classic does (and my older phosphor method now found in CRT Yah).
g is an inverted time constant that scales exponentially. The smaller g is, the longer it takes for the response to decay. n is a trapping constant. The smaller n is, the more electrons become trapped and the tail is made longer.
Given a 10% time constant, there are infinite number of combinations of g and n that will match to that time constant, so you actually need two design parameters: 10% time constant and n-field integration power, for example. While the red phosphor takes much longer to get to 10% initially, it actually has a smaller trapping and this can result in a cyan-colored tail.
The integral from 0 to infinity only resolves for positive n > 1. It is better to integrate to some bounds. The phosphors are meant to blend a bit for interlacing, so an integration time of 2 fields should be reasonable. But when I implement this I don’t get a nice tail. What I can do is fudge the numbers around to get a response that looks very realistic, but I can’t completely make sense of them.
Yup, I clearly remember the tail left from the c64 cursor on a black background, by keeping the spacebar pressed just for fun.
I found this paper that characterized the decay of some phosphors: https://www.cl.cam.ac.uk/~mgk25/ieee02-optical.pdf
The blue and green are a combination of exponential and hyperbolic decay. The red is purely multi-exponential. The exponential terms are all basically decayed within milliseconds, so they are probably not relevant.
Doesn’t it make sense to just select the parameters so that the integral converges? After all, we are modeling real physical systems, and the phosphor can’t output infinite light with a finite energy input.
The equation is simply a model for short term response of the phosphors, but the tail for the blue and green phosphors lasts for very long time. That’s why we need to select an n < 1 for the blue and green phosphors. It’s fine that it doesn’t converge in practice. Exponential decay at these timescales can be estimated acceptably with the hyperbolic function, but not vice versa, which is why I simplified the shader to rely only on a hyperbolic function.
The latest code, including the NTSC simulation, is here. This is still a work in progress. The main issue is that interlacing is still not supported properly in combination with the NTSC simulation. There are several other smaller things I’d like to work out as well.
There are several systems available:
- 0 Standard NTSC This is a decent fallback and should work for later consoles like the PS2 and Gamecube.
- 1 NES This is not to be used with raw NTSC output from NES emulators, but does a decent job with the default RGB output. At some point I’d like to make a version that deals with the raw composite output.
- 2 Master System
- 3 PCEngine (TurboGrafx-16) Note that this isn’t actually working yet. I have to decide what to do about not being able to get the number of lines from the core.
- 4 Mega Drive (Genesis)
- 5 SNES
- 6 Saturn
- 7 Playstation There was some guesswork involved here because the documentation is a little sparse. I think it is pretty accurate, though.
- 8 N64
The N64 and Playstation presets should work with upscaled content. Everything else requires the core to output at the original resolution (borders, like in Genesis Plus GX, are fine). The resolution is used for switching modes. So, for example, switching between 256 pixels wide and 512 pixels wide on the SNES is accounted for. Same with 256 pixels and 320 pixels on the MD/Genesis.
The Saturn, Playstation, and N64 presets are all pretty close to standard NTSC. It might make sense to combine them all.
There is a parameter to choose a notch filter and a non-adaptive, 3-line comb filter for NTSC decoding. The notch filter will be blurrier and have more rainbow chroma artifacts, which the comb filter will be sharper and have fewer chroma artifacts but have dot crawl along vertical chroma transitions.
Some things I plan to work on:
- I’d like to add some older systems (Atari and ColecoVision) as well as some 8-bit computers.
- I’d like to set the filters to mimic the filters on the actual hardware (to simulate the MD/Genesis’ terrible composite, for example). Currently the same filters are used with every system.
- There’s some cleanup and documentation to be done in the codebase.
- I’d like to add an adaptive comb filter. I’ve been playing with this. There are a lot of different ways to make the comb filter adaptive.
Not related to NTSC, there is a new parameter to turn down the mask mitigation so that you can have the mask at full strength if you want (you just lose brightness).
Here are some screenshots, one with the comb filter and one with the notch filter. You can see that the comb filter is too sharp for the Genesis’ poor composite output. The luma trap filter needs to be simulated (moving the luma bandwidth lower is a decent approximation). The hanging dots that are characteristic of non-adaptive comb filters are also clearly visible. This is much worse on Genesis than anything else. With the notch filter, the rainbow artifacts are a little worse than they would be if the luma trap filter was fully simulated.
EDIT: Oops, I switched up the names of these images.
Most of the other systems look better than the MD/Genesis, but you really have to see it in motion rather than as a static screenshot.
Be careful with the Genesis because different emulators output different colors. BlastEm outputs based on the voltages output by the VDP (and subsequently displayed on the TV), while Genesis Plus GX and pretty much everything else outputs the internal RGB values linearly. The RGB values you want are from the VDP’s output, as in BlastEm. In my code, I’ve already set up a function that converts other Genesis emulators’ colors into BlastEm’s via a quick array lookup. Feel free to copy it.
I’ll also note, in BlastEm, overscan cropping is controlled via RetroArch’s global setting.
Yes, I’ve read through the issue thread. I’m hesitant to add an option for only one core, but I expect the per-system configuration is going to evolve when I get to having different filters for different systems.
I thought GPX was going to patch their palette for that. Am I hallucinating?
My opinion: avoid individual hacks; too much time to debug and maintain when emu authors should update their code. I found an issue with Geolith’s palette, alerted the author and he fixed it.
As Mask of Destiny explained, there is nothing complicated, it’s just that I never took the time implementing it (it also requires some design changes in vdp rendering code). This will be done someday, eventually…

