Playing Stray (2022) on a CRT TV in 480i
on
One of my recent projects was to get my PC to output 480i signals to my recently acquired Trinitron TV for running modern and historical games on a CRT TV. (I'd definitely be better served by a CRT monitor, but I didn't have one and still don't 😿.) I learned many interesting (though irrelevant in today's LCD era) bits of trivia from this experience:
-
A modeline used to output 480i SDTV-compatible signals (525 scanlines) from a PC, differs from the same resolution's modeline under Coordinated Video Timings (arachnoid.com generates a 516-line modeline for 480i, and Linux's cvt tool doesn't document its interlacing flag because interlacing is not part of VGA or something).
- I wrote a quick and dirty GUI program for parsing and analyzing modeline strings at https://codeberg.org/nyanpasu64/modeline.
-
DVI and HDMI signals are effectively continuous signals with CRT-like timings, translated into digital signals (discrete-time at your GPU's configured dot clock, and quantized) and encoded using TMDS with special pixel values on the blue channel (outside of the 0-255 range) for hblank and vblank (TMDS Wikipedia). Whereas DP signals are packetized in some manner I haven't researched.
-
One common way to output 480i is through HDMI-to-component converters. But 480i has a too low pixel clock below the HDMI spec's minimum timing, and many GPUs cannot put out such a slow signal or converters cannot understand it. (I wonder how DP-to-component would fare, given it's packet-based and might not suffer from a minimum dot clock readable by the receiver chip.) A common workaround recommended by the developers of "CRT Emudriver" (AMD GPU drivers modded by in-place binary patching) is to set a 2560x240p or 480i "super resolution", then configure emulators and games to output stretched signals. Unfortunately, neither non-RetroArch games nor Windows's CRT Emudriver cannot rescale their image to non-square pixels and their 1440x480 images were squashed to a 4:3 aspect ratio, so I had to switch to Linux and xrandr for square images.
- I had to pick 1440x480i because otherwise the vblank pulses generated by my AliExpress LT8612EX HDMI-to-component converter were insufficiently staggered between even and odd scanlines. Since I don't have an oscilloscope, I diagnosed this by piping the sync-on-green composite signal into my motherboard's 192khz line-in capture, then recording in Audacity and comparing to correct NTSC timings (the vertical serrations should be evenly spaced and they weren't).
-
The Linux amdgpu driver is well capable of outputting a 1440x240p resolution (there are many NTSC modelines, with varying undesirable horizontal offsets and stretching, and vertical offsets), and xrandr is an excellent way to scale a 1280x960 framebuffer to 1440x240 or such (which is scaled back to square aspect ratio on-screen). Unfortunately Linux amdgpu's dc driver for its display controller (compatible with atomic modesetting) has missing support for interlacing, the easiest workaround being to pass
amdgpu.dc=0
to the Linux kernel command line (this disables atomic modesetting, switches to the legacy display controller driver, breaks HDMI audio, and makes the mouse cursor unaffected by Redshift so Plasma's text cursor becomes practically invisible on white text fields).- I debugged this extensively at https://gitlab.freedesktop.org/drm/amd/-/issues/1636, but failed to solve the problem. If I hack the driver to force interlacing on (not realizing I needed interleaving as well), the resulting analog signal repeats the first scanline hundreds of times followed by a birth-defect serration pulse, which I would be unsurprised if it would physically destroy some CRTs. I recorded a video of this signal on my CRT (epilepsy warning):
- Antonio Giner claims to have written a fix for interlacing in
amdgpu.dc=1
mode, for his GroovyArcade arcade CRT distribution. Sadly it's not upstreamed, doesn't currently work on my GPU, and I never did get around to porting it to my older RX 570, finding the correct modeline, and testing that it works.
Gameplay
-
Turns out modern games like Stray aren't optimized for interlaced displays (enabling vsync in Stray with interlacing on causes horrendous input lag) or 480-line-tall screens (HUD elements and jump prompts are too small to see), who would've known!
- I do think Celeste would work fairly well in 240p mode, but I haven't gotten around to testing that either (in either 240p, or 480i with
amdgpu.dc=0
or porting his patch). crt09 on reddit has mostly succeeded running Celeste at 240p though.
- I do think Celeste would work fairly well in 240p mode, but I haven't gotten around to testing that either (in either 240p, or 480i with
- Back in the good ol' days of analog TV and "480 lines ought to be enough for anybody", analog television sets actually displayed the borders of the incoming image outside the viewing area (known as overscan), due to manufacturing tolerances and to hide the unstable image at the edges.[citation needed] As a result, broadcast TV and pre-2000s console games knew not to put important visual elements near the edge of the screen. Unfortunately, Stray hails from the LCD future, and oh-so-helpfully places the jump prompts right at the border of the image. On a CRT SDTV, in Stray you can't even see where to jump up and down off-screen ledges or across off-screen gaps. To fix this, we need to shrink the image horizontally and vertically and put carefully-tuned black borders around it, in a process known as underscan.
-
On Linux, properly compensating for CRT television overscan is a nightmare. On amdgpu, xrandr's underscan option can only take off <=128 pixels evenly split between the left and right, or the top and bottom. Worse yet, the display controller outputs eg. 1340x460 of non-black image, by downscaling (in hardware?) a second time from the 1440x480 framebuffer downscaled in software from 1280x960, creating unnecessary vertical blur.
-
Since I don't know how to perform both downscaling steps in hardware with unmodified amdgpu drivers (like a non-square-pixel VSR), I opted to perform both downscaling steps in software, by writing a shell script to kill plasmashell and open a full-screen mpv showing a black .png file on a 1440x1030 framebuffer, then use KWin rules to overlay a 1280x960 borderless window of Stray at a precise position over mpv (with black mpv borders on all 4 sides), use xrandr to downscale 1440x1030 to 1440x480i, then kill mpv once Stray exits.
-
It's a terrible setup, and I wish there was a better way that didn't require fine-tuning the framebuffer size and window position in unintuitive ways, then rerunning the shell script, to adjust the margins. But CRTs and overscan is dead, interlacing is dead, 640x480 is dead, and only a fool or experimenter (or someone with complex-trauma depression seeking comfort in nostalgia and cats) would try hooking up modern computers and games to triply-dead CRT SDTVs and expecting them to work.
-
Bonus: Photos of my cat Moomoo