Repository navigation
Description
What
On main 01e5f8f, SuperTuxKart (PPSA69421 00.001.000) draws the starting-line referee solid black during "Ready! / Set! / Go!". The referee is the Thunderbird (models/thunderbird.spm, texture thunderbird.png), and the traffic-light sign it carries (traffic_light.png, shader="unlit" in materials.xml) is black as well. The black is uniform: the shapes are right and they move with the animation, but there is no texture detail. The rest of the tutorial track is lit and textured. Black surfaces in this title are also mentioned in #7307 (Abyss track, NVIDIA).
Reproduce
Tutorial race, no other input: answer "Yes" to the tutorial prompt through APS5_PAD_INPUT_FILE (left stick right, then cross). The countdown starts about 40 s later. The executable is converted with the catalog's --trap-sdk-syscall-stubs.
History: #5321 fixed the same symptom for the rest of the scene
First-parent bisect on the race start, classifying by the brightness of the sand beside the road:
18c438b10(Oct 14): the whole 3D view is black.0c4e4302d,5010908d6,08a7f7a8c,9d48178c5: the sky renders, but every textured surface is dark. Only the kart's headlight spots show, and the minimap is missing.- First lit commit:
e6e36c118, the merge of fix(agc): count adjacent registered allocations as covering a storage-image write-back #5321 (RegisteredReadableCoversacross adjacent registered allocations).
Since then the referee is the remaining black surface. It stays black on every build I tried:
main6b8228b, 661f8b7, 3fd1cc3 and 01e5f8f;- with perf(agc): avoid redundant texture-array CPU transfers #6718, fix(agc): restore write-watch coverage for remapped pages #6759, fix(agc): import selected texture layers within registered ranges #6820, fix(agc): cover the console's DCC key extent for 1, 2, 8 and 16-byte elements #7896 and fix(agc): keep storage image results recorded after a direct write when its completion stamps it again #8126 merged into
main; - fix(shader): read quad_perm DPP lanes in pixel shaders through quad operations #6290 and fix(shader): emulate sRGB sampling when unsupported #5371 at their own heads.
Ruled out
- Missing texels in guest memory. STK resizes
traffic_light.png(128x384, RGB) to a 128x512 RGBA8 texture with 10 mips. When the countdown starts, that texture is sampled with non-zero texels in guest memory: 504 of 1535 sampled 256-byte strides are non-zero. The bytes are there. - The storage-image view of the texture. With
APS5_NO_SAMPLED_FROM_STORAGE=1, the texture is read as a snapshot, and the referee is still black. - The vertex format.
thunderbird.spmhas the same SPM header askarts/tux/tux.spm: animated (SPMA), normals only, no vertex colours, no tangents. The Tux kart draws correctly. - A different shader. The draw-program line just before each lookup of the 128x512 texture is the same vertex/fragment pair that draws the rest of the race. In that run it was
0x263883000/0x263885000, used from the first race frame onwards (about 3,100 draws).
Observation
The black matches texel (0, 0) of both textures:
| Texture | Texel (0, 0) | Average |
|---|---|---|
thunderbird.png |
(0, 0, 0, 0) | (31, 63, 99, 165) |
traffic_light.png |
(13, 13, 12) | (37, 33, 16) |
So the referee's draw seems to sample one constant coordinate, or a different texture that reads as zero, rather than its stored texels. I haven't found which of the referee's per-draw inputs is wrong: texture coordinates, its texture descriptor, or per-instance data.
Setup
Windows 11 26300, AMD Radeon 860M (driver 32.0.13058.8001), WinLibs GCC 15.2.0, Release. Screenshots of each stage are available if useful.
AI-assisted: yes (Claude Code)
Activity
LeandroLP23 commentedon Oct 20, 2026
Found the trigger: the black is the referee's point light, whose pixel shader's
discardis not honoured.STK attaches a point light to the referee in
src/graphics/referee.cpp:addLight(..., 0.7f, 2.0f, 0.7f, 0, 0, ...), red, recoloured with the traffic light. The light pass,data/shaders/pointlight.vertandpointlight.frag, draws each point light as a screen-space quad fromUpdateClipRegion. That quad is the axis-aligned black rectangle. Inside it the fragment shader does:I patched only
pointlight.fragin the run folder; the package and the emulator are unchanged. Same scene,main01e5f8f:pointlight.fragdiscard;as the first statementif (att <= 0.) { Diff = vec4(0.); Spec = vec4(0.); return; }instead ofdiscardSo the pixels the shader discards (everything farther than
radiusfrom the light, which is nearly the whole quad) still reach the light accumulation targets, and the scene there goes black. Writing zero to them explicitly avoids that.The compiled pixel shader is the one used only while the referee is on screen (81–83 draws, from race start to "Go!"). In its IR after
translate, block$1(pc 0x00–0x70) samples depth and normals, computesatt, and ends with ACO's discard:s0 = s0 & ~vcc(the exact mask);SCC = s0 != 0;exec = wqm(s0).Block
$5(pc 0x260–0x26c) is the early-exit block:SetAttributeof zeros to a null export, then the program ends. The two MRT exports at the end of block$4useexec = s0. So whether a discarded lane is killed depends on that lane'sexecreaching thevmexport (pixel_valid_mask_active, thenOpKillat the end inSpirvModuleEmitter.cpp), on both the early-exit path and the normal path. I haven't yet found which of the two keeps the discarded lanes alive. I can attach the full IR dump of that shader if useful.The black mesh of the bird is the same effect: the bird lies inside the light's quad.
AI-assisted: yes (Claude Code)
[-]AGC: SuperTuxKart (PPSA69421) draws the starting-line referee and its sign solid black[/-][+]AGC: SuperTuxKart (PPSA69421): the referee's point light ignores its discard and blacks out the scene behind it[/+]LeandroLP23 commentedon Oct 20, 2026
Two corrections to my comment above, plus one more data point.
The early exit is ruled out. I patched
pointlight.fragso that every wave keeps live lanes:One column in eight returns with zero output, so the exact mask never becomes empty and ACO's discard early-exit block can't be taken. The rectangle is still uniformly black, on
maind50ec76. So the discarded lanes survive on the normal path to the exports, lane by lane, and not only when a whole wave is discarded.The IR I described is the wrong shader. Program
0x258605000discards when a value is below 0.5. That is an alpha test, so it is most likely the referee's own material (thunderbird.pnghas alpha), notpointlight.frag. Its per-lane mask handling looks correct to me:s0 &= ~vcc, the thread bit becomesold bit && !vcc, and the exports useexec = s0.I haven't isolated the point-light program yet. Its pair is used only while the referee is on screen, but with
APS5_DUMP_IR=allno dumped program contains the20.0literal fromenergy * 20.. Either the light program is compiled through a path that dump doesn't cover, or the compiler folded the literal. The GLSL A/B above is the reliable reproduction in the meantime.AI-assisted: yes (Claude Code)
simonbbby commentedon Oct 20, 2026
Linux/NVIDIA reproduction and a capture of the actual point-light fragment program.
SuperTuxKart PPSA69421 00.001.000, catalog SHA-256
f2240bf0e67cf585ae7c6acc16068e1b8bef5e3c1b5f7e8ae550053d937b712a; Ubuntu 24.04.3, GCC 13.3, RTX 3090 / NVIDIA 595.99.02, subgroup 32.Baseline qualification: source is
main56b299c3a9f3484b2db38021964f0117f8701f93plus only #7897'sApplicationHeap.cppproduction hunk. Unmodified Linux main stops earlier: GDB observesstrdup("u_fog_color")return null, then Mesa's hash path reachesstrlen(nullptr)while linking its first GL program. The allocator hunk lets it reach the race. This is not an unmodified-main Linux rendering result.Rebuilt and reran on newer main
44e3389c882c8ee3249e16933aad6479c3e00d08plus the same allocator hunk: the original shader still produces the rectangle, including after #7954's pass-input barrier fix. The controls and captured request described below belong to56b299c3, not that newer rerun.The tutorial prompt reproduces the black referee/rectangle. For repeatable shader controls I also launched
./app.elf -N -t tutorial --numkarts=1, with a fresh process,ANYPS5_NO_SHADER_CACHE=1,APS5_NO_PIPELINE_JOURNAL=1, and the NVIDIA ICD restricted:if (att <= 0.) discard;replaced with explicit zeroDiff/Specand returnOpKill, keeping the draw metadataProgram isolation: the zero-output edit changes exactly one fragment's normalized
after resourcesIR:0x2553fd000, paired with vertex0x2553fb000in these runs. The0x41a00000(20.0) constant is in that vertex program's IR, explaining why searching only pixel programs misses it. These addresses are run-specific.The fragment is 308 dwords, wave32,
pixelKillEnable=1. I captured its request after draw SRT memory materialization; a preparation-stage dump alone cannot replay the runtime descriptor reads. The captured request successfully recompiles standalone (35,016 SPIR-V words); this is a compile replay, not a standalone GPU rendering reproduction. The actual runtime artifact also contains the valid-mask stores and three conditionalOpKillpaths; a simply missing kill instruction is not established. A small separate wave32 WQM →s_andn2_b32discard → VM export control passes on this RTX 3090, so that sequence alone does not reproduce the defect.This isolates a reproducible title-backed case and identifies the previously unidentified light program; it does not yet establish a production fix. Temporary diagnostics and the rejected initializer were removed. No Khronos validation layer is installed on this runner, and no Windows/performance/full-suite claim is made.
LeandroLP23 commentedon Oct 20, 2026
Narrowed further. This now looks less like the shader's kill and more like what happens to killed pixels in a draw whose other pixels write.
The point-light program and its SPIR-V. The program is
0x255213000(vertex, the only one with the20.0literal andUpdateClipRegion's square roots) with0x255215000(fragment). I dumped its SPIR-V locally fromRecompiler.cpp, right before validation and optimization, and disassembled it. The kill looks right:vmexport storespixel_valid_mask_activefrom the export's exec;!helper && !(exec && att < FLT_MIN), as a phi over the spotlight branch;OpKillunless the mask is set orgl_HelperInvocationis true;The program runs on every race frame, before and after the referee is on screen. I haven't identified which other lights it draws there, or whether their quads cover visible geometry.
pointlight.fragvariants (same scene,maind50ec76, only the shader file in the run folder changed):if (att <= 0.) discard;)if (!(att > 0.)) discard;(NaN discarded too)if (sscale != 0.)→if (false))discard;first, so every pixel is discarded{ Diff = vec4(0.); Spec = vec4(0.); return; }instead ofdiscardSo
attis not NaN, and the spotlight branch and ACO's early exit don't matter. The rectangle goes black only when one draw has both written and discarded pixels. Then the discarded pixels don't keep what the light-accumulation targets held before the draw.My guess is that the problem sits in how the driver handles these targets (Diff/Spec, additively blended) rather than in the shader. For example, the pixels the draw covers could be stored from an image that wasn't loaded with the target's previous contents, which would be fine for written pixels and wrong for killed ones. I haven't confirmed it. Next I want to look at the color-target path for this draw.
AI-assisted: yes (Claude Code)
simonbbby commentedon Oct 20, 2026
I tried a smaller target-preservation control on main
5f7e30233e62ae5fdb7a6e3257698bc2e550bcc2. It does not reproduce the loss yet:R16G16B16A16_SFLOATMRTs, wave32, compressed FP16 exports,ONE + ONEadditive blending for color and alpha,pixelKillEnable=true.vmexport sequence.This only checks linear, uncompressed-metadata targets through
Graphics::Draw; it does not reproduce STK's actual attachment layout, aliases, depth state or preceding resource transitions. It leaves the title-specific target load/preservation hypothesis open, while providing a passing control for mixed discard plus GPU-written additive MRT contents.The test-only patch against that main is below; no production change is proposed.
Mixed-discard MRT control (replace the existing PixelValidMask fixture temporarily)
LeandroLP23 commentedon Oct 20, 2026
Root cause found, and it is neither the kill nor the light targets as I guessed above: the point light's flat inputs all read 0.
pointlight.fragreads its 12 flat inputs throughv_interp_mov_f32P20. That is ps5-opengl'sflatshade_firstlowering, vertex 2 of a triangle. Under the first provoking vertex,mainread each one as vertex 2 minus vertex 0, which is 0 for a flat value. Soatt = 0 * -inf = NaN, andatt <= 0.is false for NaN, so nothing was discarded and the whole quad wrote NaN. My earlier!(att > 0.)variant didn't catch it, because the shader compiler is allowed to fold it back intoatt <= 0.. A bit test onattdoes catch it, and then the referee renders. A per-input check showedradiusexactly 0.Fix: #8538. It reads such a P20 read as the provoking vertex, like #4030 does for P10. With it, the unmodified game draws the referee correctly in 2 of 2 runs.
fix(recompiler): read P20 of a flat input as the provoking vertex
fix(recompiler): read P20 of a flat input as the provoking vertex
fix(recompiler): read P20 of a flat input as the provoking vertex
fix(recompiler): read P20 of a flat input as the provoking vertex
fix(recompiler): read P20 of a flat input as the provoking vertex
Betr0x commentedon Oct 27, 2026
@LeandroLP23 A question about SuperTuxKart (PPSA69421), since this issue is the most detailed run of it I could find: roughly what frame rate do you see in the tutorial race on your setup, and which OS/GPU is that on? A number from the
[present]line or the window title is enough.Background: on macOS (Apple M4, x86-64 under Rosetta 2, MoltenVK 1.4.2) the menus run at ~25 FPS but 3D scenes (story-mode overworld) at ~1.7 FPS. Most of that is the macOS write watch (~25,000 page faults per frame at ~18 µs each under Rosetta); with a local prototype that re-arms rewritten pages once per present it reaches ~6 FPS. A Windows/Linux figure would show how much of the remaining gap is macOS-specific.
AI-assisted: yes
LeandroLP23 commentedon Oct 27, 2026
@Betr0x On Windows the tutorial race now runs at about 12 FPS on current
main.Setup: Windows 11 26300, Radeon 860M iGPU (AMD 32.0.13058.8001), Ryzen AI 7 350, WinLibs GCC 15.2.0, Release,
main3fea2a937b. PPSA69421 00.001.000 is converted with the catalog's settings, and the race is started throughAPS5_PAD_INPUT_FILE("Yes" on the tutorial prompt, then cross).The title's own counter agrees: 11.6 to 12.0 fps over 5 s. The slowest frame is 99 to 118 ms, and the main thread is at about 33 to 42 % of one core.
For comparison, the same race on this machine measured 1.70 and 2.16 FPS on
maind34da5d694(Oct 22), as posted in #6718. Since then #6718 and #7465 have landed. #6718 alone gave 2 to 3 times in that thread, by removing a storage round trip on every pass. So on Windows, most of the old gap was not the write watch.AI-assisted: yes (Claude Code)