Repository navigation
feat(recompiler): FLAT addresses in the shared and private apertures reach LDS and scratch - #1190
Conversation
|
Yōtei's ray-traversal kernels (see my note on #1182) are a real-world case for this one: they push the traversal stack with Replaying the seven programs on #1182 + #1190, the two big kernels recompile (their next stop is LDS over the device limit), and the results match what I got with my own version. Two gaps showed up:
Tested on an RTX 3080 (Windows 11) and on lavapipe. I can share the execution test, which covers per-lane LDS/scratch/global mixes, x2/x4 and byte/short accesses, if you'd like it for this PR. |
adriadam10
left a comment
There was a problem hiding this comment.
Thanks, the approach (routing on the high dword in the compute FLAT path) looks reasonable. Things to fix before this can land:
- Rebase: the branch carries
a2fb99fb7(the #1182 commit), which is already onmainas20e055c24; it now conflicts intests/Graphics.cpp. Please rebase so only the aperture commit remains (the serialization version should then be the next free version after what has landed). - Exact or throw: only 32-bit loads and stores are routed. Sub-dword stores (
SpirvMemoryInstructions.cpp:852, "still stored through BDA") and flat atomics (EmitBdaAtomicpath, ~line 1224) whose address is in the shared/private aperture go to BDA with a bogus address and silently do the wrong thing. Either route them too or throw when the aperture can't be excluded; a plausible wrong result is worse than the previous failure. - Tests:
FlatApertures.cppcovers only dword load/store. Please add rows for the paths above (route or throw), a lane-varying case mixing LDS/scratch/global addresses in one wave, and a wave64 run. - Source: the 0x8000/0x7000 bases are described as "as seen in GTA V's traversal addresses", not verified. Please cite where they come from (SH_MEM_BASES values, or a reference); we can't measure the console's apertures on our Linux oracle.
- TechnicalDebt: list what stays unmodelled (e.g.
src_shared_base/src_private_base, operand 235, reported by @Celegans12 on #1182) under "Unknown function info"/the matching section.
bb70ded to
f36e64c
Compare
|
Thanks. Rebased onto main, so only the aperture commit remains (the #1182 commit is gone). This push:
|
|
I ran Ghost of Yōtei (PPSA26344 v1.008) for an hour with this PR at bb70ded, the version before your last push, and the two big ray-traversal kernels run now. 0x8000173b00 and 0x800014e300 push their traversal stacks through your aperture path and ran on queue 0x51, about 47 dispatches and 2.0 s a frame, with no device fault, no BDA fault and no GPU hang. The SCRATCH_EN skips from the run before (13.9 a frame, now covered by #1182) are gone too. The LDS half is now #1776, which doesn't depend on this one. At bb70ded your shared-aperture arm went through the same LDS element pointer, so it followed the device-memory LDS without changes; I haven't checked your new byte-wise and atomic routes against it yet. The cost has moved to pipeline creation: each data variant of these kernels is about 1.06M SPIR-V words and took 215-243 s to create, which has nothing to do with your change. The three small kernels (0x8000356b00 and the two that stop at a runtime formatted V#) didn't come up in this run, so I couldn't try the operand 235-238 handling in the game. I'll put your current head f36e64c into the next run and report back. DetailsWindows 11, RTX 3080 Laptop, our integration tree on main 4b005b2 (which has #1182) + this PR at bb70ded + #1776 (as 1a4c9b07, before its rebase onto #639) + our other open PRs; a 1,200 s capture pass, then 3,600 s.
All nine watchdog "stuck" lines in the run were pipeline compiles and cleared; 0x8000117800 and 0x8000167500 did not run. |
|
@Celegans12 thanks for the hour-long run. Good to see the traversal stacks of both kernels going through the apertures on Yōtei too. |
|
@MichonGoddijn231849 the new Since #639 the buffer path follows OOB_SELECT in V# dword3. The test's It is still too slow for CI: 21 s on NVIDIA, 14 s on RADV, about 300 s on lavapipe (the CI run timed out at 120 s). Nearly all of it is lavapipe compiling each 64-bit atomic kernel once, 5-38 s each (16 kernels, about 210-250 s in total). The shared/private/guest rows reuse the kernel and cost about 0.2 s each. Fewer distinct 64-bit ops, or splitting the file (each |
|
Ran the new test on real hardware since CI Windows skips it: RTX 4050 laptop, Windows, MinGW 15.2 build of f36e64c, ANYPS5_GPU=4050. First run after the build timed out at 60s (same picture as the Linux job: alive past device setup, then silence after the write-watch line, no result rows). Every run after that passes in about a second, five in a row, plus one 14s pass with APS5_TRACE_DISPATCH_IO on. So I could not reproduce a logic hang. This looks like cold Vulkan pipeline compilation on the first run rather than an infinite loop in the routing: a fresh CI runner pays the same cost, and llvmpipe pays it slower, which fits the 120s Linux timeout. If it keeps biting CI, a longer timeout for this test or a shared pipeline cache would tell the two apart for good. |
|
@danzaio thanks, that fits a cold pipeline compile. The Linux job's 120 s timeout on lavapipe is probably the same thing. @CouchRiv your report is a different failure, and I had missed it: |
|
Fourth driver for @CouchRiv's failure. Head It fails in 0.25 s, so this is not the cold pipeline compile @danzaio saw; the BDA fault at address 0 fits the OOB_SELECT reading of the V# ( Tested with AI assistance (Claude Code). |
|
Verified the fix commit on real hardware: RTX 4050 laptop, Windows, MinGW 15.2 build of f36e64c, ANYPS5_GPU=4050. agc_driver_flat_apertures passes in about 1.5s, all 121 rows including the shared/private aperture routing and the sub-dword store cases from the report. Nothing else in the run looks off. AI-assisted: yes. |
|
Rebased onto
Tested on Windows, RTX 5080:
@adriadam10 the branch holds only the two aperture commits now, and it no longer changes the request serialization version. |
adriadam10
left a comment
There was a problem hiding this comment.
Thanks for the fixes and to everyone who ran it. I measured the routed semantics on real RDNA2 (gfx1035, HSA, LDS of 4096 bytes, Linux's unaligned mode) with flat accesses into the shared aperture. Unaligned in-range loads and stores (byte-wise, little-endian), lo >= 0x10000 reading 0 with no wrap, flat_atomic_add/_x2 and an out-of-range atomic returning 0 all match the PR. Two things don't:
-
The aperture is a 4 GB window, not a 16-bit tag (
SpirvMemoryInstructions.cpp:355-356). On gfx1035,src_shared_base/src_shared_limitread0x00010000_00000000/0x00010000_ffffffff, andsrc_private_*read0x00020000_00000000/0x00020000_ffffffff. That is the gfx9+ layout in Linux'skfd_flat_memory.c(MAKE_LDS_APP_BASE_V9() = 0x1 << 48,MAKE_LDS_APP_LIMIT(base) = base | 0xFFFFFFFF): the SH_MEM_BASES field becomes bits 63:48 and the window covers 4 GB from there.SplitApertureonly compares the top 16 bits, so a high dword of0x8000_0001reaches LDS here while the hardware sends it to global memory. Comparing the whole high dword with0x80000000/0x70000000fixes it. That also makes the TechnicalDebt line at:330inaccurate: there is a public source for the layout, and only the PS5's base values are inferred. -
An access that runs past the end of LDS is dropped as a whole, not byte by byte (
:397-405,:442-447). If any byte is out of range, the hardware reads 0 or writes nothing for that dword access. Multi-dword accesses are handled per dword. The rows that differ (LDS pre-filled so byteaholdsa & 0xff, store data0xaabbccdd,0x11223344):access lo hardware PR flat_load_dword0xffd0x000000000x00fffefdflat_load_dword0xfff0x000000000x000000ffflat_load_ushort0xfff0x00000x00ffflat_store_dword0xffedword 0xffcunchanged0xfffefdfc0xccddfdfcflat_store_short0xfffdword 0xffcunchanged0xfffefdfc0xddfefdfcflat_store_dwordx20xffafirst dword written, second dropped: dword 0xffc=0xfffeaabb0x3344aabbChecking that the element's last byte is in range before the byte loop (and skipping the whole element otherwise) would match these. Rows for them in
FlatApertures.cppwould pin the behaviour.
I couldn't measure scratch reached through flat addresses or the console's alignment mode (SH_MEM_CONFIG). The TechnicalDebt line already lists both as inferred, which is fine.
|
Thanks @adriadam10, both measurements are now in
New rows in
With the old byte-wise behaviour the dword row fails ("lane 31 read 0x00b5392f back from an access past the end of LDS"); with the change it passes. The other 21 flat/global/BDA tests, |
|
With that call changed to AI-assisted: yes (Claude Code Fable 5.1 xhigh) |
…reach LDS and scratch A compute FLAT access whose address has the console's shared aperture in its high dword (0x8000xxxx) reads or writes the workgroup's LDS at its low dword, and one in the private aperture (0x7000xxxx) the lane's scratch; other addresses stay guest memory through BDA. GTA V's ray traversal keeps its stack in LDS and spills it to scratch through such addresses; treated as guest memory the pushes were lost and the traversal never ended (device lost). An execution test round-trips both apertures. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… apertures; table test Sub-dword stores and unaligned dword accesses go byte-wise; 32- and 64-bit FLAT atomics route to LDS (64-bit under the workgroup lock), scratch (read-modify-write) or BDA. agc_driver_flat_apertures covers them in 121 rows, with mixed-address waves and wave64. The aperture bases and the unmodelled operands 235-238 are listed as inferred. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… apertures Main's StoreAddressU32x2..x4 (one BDA lookup per wide store, b958425) bypassed the aperture routing: a flat_store_dwordx2 into the shared aperture went to BDA at 0x8000000000000000 and LDS kept its old value. The wide store now stores each component through the aperture like the 32-bit store, and keeps the single BDA lookup outside the apertures. The aperture test's V# word 3 is 0x31016fac (OOB_SELECT 3, raw), as in main's other execution tests: with OOB_SELECT 0 its output buffer read as out of range after boykopovar#639, which was the failure reported on four drivers. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…past the end is dropped whole Measured on RDNA2 (gfx1035) in review: - the shared and private apertures are 4 GB windows (the gfx9+ layout of kfd_flat_memory.c), so a flat address routes by its whole high dword, 0x80000000 or 0x70000000, not by its top 16 bits; - a dword or short access whose last byte is past the end of LDS reads 0 or writes nothing as a whole; multi-dword accesses are checked per dword. The aperture test gets rows that straddle and pass the end of LDS; the TechnicalDebt line names the public source of the layout and keeps only the PS5's base values as inferred. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
4ab7fb7 to
df25847
Compare
adriadam10
left a comment
There was a problem hiding this comment.
Thanks for the fixes and for everyone's runs. Rebased onto current main and merging:
- Rebased: the only conflict was
docs/dev/TechnicalDebt.md(adjacent lines), both kept. - Added a commit:
FlatApertures.cpp:81now passes the fifthgpuargument toGuestAllocations::Mutation().Add, as @orishamir reported; the test did not compile onmain. - Checked on RADV after the rebase:
agc_driver_flat_apertures(121 rows) and the FLAT, global, BDA, LDS and scratch tests pass; the test fails with the recompiler changes reverted. - The 4 GB windows and the drop-whole-access-past-LDS behaviour match the gfx1035 measurements from the earlier review.
Reading src_shared_base, src_shared_limit, src_private_base or src_private_limit (scalar source codes 235-238) refused the shader since boykopovar#1940, because their values were unknown. They now read as the 64-bit apertures that the flat routing from boykopovar#1190 classifies: a base is the aperture's high dword over a low dword of 0, a limit is the same high dword over 0xffffffff (shared 0x80000000, private 0x70000000). A 64-bit read (s_mov_b64, a 64-bit VALU source, a wave64 lane mask) gets the whole value and a 32-bit read gets the low dword. A flat pointer built from the high dword therefore reaches LDS or scratch, and 64-bit compares against the limits order as on the hardware. The flat routing and the reads share one definition of the two high dwords. Sources: LLVM SIRegisterInfo.td (aperture registers are 64-bit, their low 32 bits are always 0 for a base and -1 for a limit) and getSegmentAperture in AMDGPULegalizerInfo.cpp and SIISelLowering.cpp (a 64-bit read whose high half is the aperture); the RDNA 2 ISA guide, sections 5.2, 5.8 (SH_MEM_BASES holds 16-bit bases) and 9.3; the KFD's MAKE_LDS_APP_LIMIT (limit = base | 0xffffffff); the gfx1030 and gfx1035 readouts on boykopovar#1190 and boykopovar#3803. The PS5's two high dwords stay inferred, as the shader technical debt records. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reading src_shared_base, src_shared_limit, src_private_base or src_private_limit (scalar source codes 235-238) refused the shader since boykopovar#1940, because their values were unknown. They now read as the 64-bit apertures that the flat routing from boykopovar#1190 classifies: a base is the aperture's high dword over a low dword of 0, a limit is the same high dword over 0xffffffff (shared 0x80000000, private 0x70000000). A 64-bit read (s_mov_b64, a 64-bit VALU source, a wave64 lane mask) gets the whole value and a 32-bit read gets the low dword. A flat pointer built from the high dword therefore reaches LDS or scratch, and 64-bit compares against the limits order as on the hardware. The flat routing and the reads share one definition of the two high dwords. Sources: LLVM SIRegisterInfo.td (aperture registers are 64-bit, their low 32 bits are always 0 for a base and -1 for a limit) and getSegmentAperture in AMDGPULegalizerInfo.cpp and SIISelLowering.cpp (a 64-bit read whose high half is the aperture); the RDNA 2 ISA guide, sections 5.2, 5.8 (SH_MEM_BASES holds 16-bit bases) and 9.3; the KFD's MAKE_LDS_APP_LIMIT (limit = base | 0xffffffff); the gfx1030 and gfx1035 readouts on boykopovar#1190 and boykopovar#3803. The PS5's two high dwords stay inferred, as the shader technical debt records. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reading src_shared_base, src_shared_limit, src_private_base or src_private_limit (scalar source codes 235-238) refused the shader since boykopovar#1940, because their values were unknown. They now read as the 64-bit apertures that the flat routing from boykopovar#1190 classifies: a base is the aperture's high dword over a low dword of 0, a limit is the same high dword over 0xffffffff (shared 0x80000000, private 0x70000000). A 64-bit read (s_mov_b64, a 64-bit VALU source, a wave64 lane mask) gets the whole value and a 32-bit read gets the low dword. A flat pointer built from the high dword therefore reaches LDS or scratch, and 64-bit compares against the limits order as on the hardware. The flat routing and the reads share one definition of the two high dwords. Sources: LLVM SIRegisterInfo.td (aperture registers are 64-bit, their low 32 bits are always 0 for a base and -1 for a limit) and getSegmentAperture in AMDGPULegalizerInfo.cpp and SIISelLowering.cpp (a 64-bit read whose high half is the aperture); the RDNA 2 ISA guide, sections 5.2, 5.8 (SH_MEM_BASES holds 16-bit bases) and 9.3; the KFD's MAKE_LDS_APP_LIMIT (limit = base | 0xffffffff); the gfx1030 and gfx1035 readouts on boykopovar#1190 and boykopovar#3803. The PS5's two high dwords stay inferred, as the shader technical debt records. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reading src_shared_base, src_shared_limit, src_private_base or src_private_limit (scalar source codes 235-238) refused the shader since boykopovar#1940, because their values were unknown. They now read as the 64-bit apertures that the flat routing from boykopovar#1190 classifies: a base is the aperture's high dword over a low dword of 0, a limit is the same high dword over 0xffffffff (shared 0x80000000, private 0x70000000). A 64-bit read (s_mov_b64, a 64-bit VALU source, a wave64 lane mask) gets the whole value and a 32-bit read gets the low dword. A flat pointer built from the high dword therefore reaches LDS or scratch, and 64-bit compares against the limits order as on the hardware. The flat routing and the reads share one definition of the two high dwords. Sources: LLVM SIRegisterInfo.td (aperture registers are 64-bit, their low 32 bits are always 0 for a base and -1 for a limit) and getSegmentAperture in AMDGPULegalizerInfo.cpp and SIISelLowering.cpp (a 64-bit read whose high half is the aperture); the RDNA 2 ISA guide, sections 5.2, 5.8 (SH_MEM_BASES holds 16-bit bases) and 9.3; the KFD's MAKE_LDS_APP_LIMIT (limit = base | 0xffffffff); the gfx1030 and gfx1035 readouts on boykopovar#1190 and boykopovar#3803. The PS5's two high dwords stay inferred, as the shader technical debt records. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reading src_shared_base, src_shared_limit, src_private_base or src_private_limit (scalar source codes 235-238) refused the shader since boykopovar#1940, because their values were unknown. They now read as the 64-bit apertures that the flat routing from boykopovar#1190 classifies: a base is the aperture's high dword over a low dword of 0, a limit is the same high dword over 0xffffffff (shared 0x80000000, private 0x70000000). A 64-bit read (s_mov_b64, a 64-bit VALU source, a wave64 lane mask) gets the whole value and a 32-bit read gets the low dword. A flat pointer built from the high dword therefore reaches LDS or scratch, and 64-bit compares against the limits order as on the hardware. The flat routing and the reads share one definition of the two high dwords. Sources: LLVM SIRegisterInfo.td (aperture registers are 64-bit, their low 32 bits are always 0 for a base and -1 for a limit) and getSegmentAperture in AMDGPULegalizerInfo.cpp and SIISelLowering.cpp (a 64-bit read whose high half is the aperture); the RDNA 2 ISA guide, sections 5.2, 5.8 (SH_MEM_BASES holds 16-bit bases) and 9.3; the KFD's MAKE_LDS_APP_LIMIT (limit = base | 0xffffffff); the gfx1030 and gfx1035 readouts on boykopovar#1190 and boykopovar#3803. The PS5's two high dwords stay inferred, as the shader technical debt records. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reading src_shared_base, src_shared_limit, src_private_base or src_private_limit (scalar source codes 235-238) refused the shader since boykopovar#1940, because their values were unknown. They now read as the 64-bit apertures that the flat routing from boykopovar#1190 classifies: a base is the aperture's high dword over a low dword of 0, a limit is the same high dword over 0xffffffff (shared 0x80000000, private 0x70000000). A 64-bit read (s_mov_b64, a 64-bit VALU source, a wave64 lane mask) gets the whole value and a 32-bit read gets the low dword. A flat pointer built from the high dword therefore reaches LDS or scratch, and 64-bit compares against the limits order as on the hardware. The flat routing and the reads share one definition of the two high dwords. Sources: LLVM SIRegisterInfo.td (aperture registers are 64-bit, their low 32 bits are always 0 for a base and -1 for a limit) and getSegmentAperture in AMDGPULegalizerInfo.cpp and SIISelLowering.cpp (a 64-bit read whose high half is the aperture); the RDNA 2 ISA guide, sections 5.2, 5.8 (SH_MEM_BASES holds 16-bit bases) and 9.3; the KFD's MAKE_LDS_APP_LIMIT (limit = base | 0xffffffff); the gfx1030 and gfx1035 readouts on boykopovar#1190 and boykopovar#3803. The PS5's two high dwords stay inferred, as the shader technical debt records. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(recompiler): set layerExported in the disk cache entry round trip boykopovar#5097 added ShaderInfo::layerExported to the entry encoder, and this series moves that line into encodeShaderInfo, which plan entries use too. The entry round trip left the flag false, so an encoder and a decoder that both skipped it still passed. Set it like the other ShaderInfo flags: with layerExported dropped from both, the round trip now fails with "entry round trip: shader info differs". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit 5d3ea5f) * fix(shader): read both version 21 request layouts (cherry picked from commit dffdb50) * test(shader): cover version 21 F16 request layout (cherry picked from commit 0e087e9) * fix(shader): require the version 22 F16 request flag (cherry picked from commit 2ad825f) * fix(recompiler): accept SDWA source selects on v_cvt_f32_ubyte1, ubyte2 and ubyte3 v_cvt_f32_ubyte0 accepts every SDWA source selector, but the other three byte conversions had no VOP1 SDWA rule, so any SDWA source select on them was refused ("VOP1 SDWA source selector 4 of opcode 0x12 at pc 0xfcc is not supported"). The selected and optionally sign-extended source goes through the same operand path as for ubyte0 before byte N is extracted: SDWA supplies source 0 (RDNA 2 ISA guide 6.8 and 13.3.7) and the opcode reads S0.u[8N+7:8N] (12.8). agc_driver_sdwa_cvt_f32_ubyte: WORD_0 and WORD_1 selects on ubyte1, and sign-extended WORD_1 and BYTE_3 selects on ubyte2 and ubyte3. (cherry picked from commit 7b2104e) * feat(recompiler): read the scalar aperture sources as values Reading src_shared_base, src_shared_limit, src_private_base or src_private_limit (scalar source codes 235-238) refused the shader since apertures that the flat routing from boykopovar#1190 classifies: a base is the aperture's high dword over a low dword of 0, a limit is the same high dword over 0xffffffff (shared 0x80000000, private 0x70000000). A 64-bit read (s_mov_b64, a 64-bit VALU source, a wave64 lane mask) gets the whole value and a 32-bit read gets the low dword. A flat pointer built from the high dword therefore reaches LDS or scratch, and 64-bit compares against the limits order as on the hardware. The flat routing and the reads share one definition of the two high dwords. Sources: LLVM SIRegisterInfo.td (aperture registers are 64-bit, their low 32 bits are always 0 for a base and -1 for a limit) and getSegmentAperture in AMDGPULegalizerInfo.cpp and SIISelLowering.cpp (a 64-bit read whose high half is the aperture); the RDNA 2 ISA guide, sections 5.2, 5.8 (SH_MEM_BASES holds 16-bit bases) and 9.3; the KFD's MAKE_LDS_APP_LIMIT (limit = base | 0xffffffff); the gfx1030 and gfx1035 readouts on boykopovar#1190 and boykopovar#3803. The PS5's two high dwords stay inferred, as the shader technical debt records. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit dc1c12a) --------- Co-authored-by: Niko <nikobilger2000@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: MarcZX100 <marczxrr@gmail.com> Co-authored-by: Dean Galvin <dean.galvin@live.com> Co-authored-by: fdfawef324 <287205748+fdfawef324@users.noreply.github.com>
What
A compute FLAT access whose address has the console's shared aperture in its high dword (0x8000xxxx) reads or writes the workgroup's LDS at its low dword. One in the private aperture (0x7000xxxx) reaches the lane's scratch. Other addresses stay guest memory through BDA, as before. This holds for every FLAT access kind in compute:
GTA V's ray traversal keeps its stack in LDS and spills it to scratch through such addresses. Treated as guest memory, the pushes were lost and the traversal never ended (device lost).
tests/execution/FlatApertures.cppruns a table of 121 rows: dword loads and stores, sub-dword stores (aligned and crossing a dword), 32- and 64-bit atomics on shared, private and guest addresses, a wave whose lanes mix LDS, scratch and guest addresses, and wave64.Source of the aperture bases: the values GTA V's traversal addresses use (shared 0x8000, private 0x7000 in the high 16 bits). No public source confirms them for gfx10 or the console; they, the unaligned aperture semantics and the out-of-range LDS/scratch behaviour are listed as inferred in TechnicalDebt.md, with the scalar operands 235-238 (
src_shared_base,src_private_base, ...) that stay unmodelled.Tested
Windows 11 Pro, AMD Ryzen 5 7600, NVIDIA GeForce RTX 5080 (driver 32.0.16.1074), 32 GB, MinGW-w64 GCC 15.2.0.
gta-v/testingbranch (Performance mode, the prologue); not re-run with the routed atomics and byte-wise accesses yet.agc_driver_flat_aperturespasses (83 of its 121 rows fail on the previous version), and the FLAT, global, LDS, BDA, atomic, scratch and recompiler tests pass. Fullctest: all pass except the relinker's Python tests (my local Python does not find their helper modules) andguest_gameupdate.Checklist
main; no other open PR implements the same functionsNotImplemented_nid_no_patch); silent stubs are listed in TechnicalDebt.mdfiles or images added to the repository (attach them to this PR)🤖 Generated with Claude Code