Repository navigation
fix(agc): take an HTILE fill from a value in memory as a depth fast clear - #3816
oneandonlydean merged 2 commits into
Conversation
|
Beast of Reincarnation (PPSA29343, Unreal Engine) runs the same 9-word kernel: at least 40 matches in the first minutes, mostly with value 0x40404040 over HTILE ranges of 0x1000 to 0x30000 bytes (a few with 0x10101010 and 0). Cherry-picked onto my branch for that title, it removes the stale blocks its title screen showed (Linux, RTX 3050 Laptop). We are still analyzing the remaining rendering issues of this title and an improvement for GPUs with little VRAM. |
|
Conventions fails on the checklist: the |
|
Data point from a UE5 title: Sword of the Sea (PPSA25344) runs this exact kernel every frame, and this PR alone fixes its depth. Setup: Windows 11, RTX 5070 Laptop, MinGW GCC 15.2, Release; current What the title does: before the first depth draw of each frame, it clears its 1920x1080 D32S8 depth target with two compute dispatches over the whole HTILE (0xc000 dwords, 768x1x1 groups of 64, stride-4 V# in
Since the matched one already covers the whole HTILE with 0, that's enough here. Result: 1:1 captures at 1920x1080, 240 s. "Dark" is the share of the sky band with luma < 22; the PS5 footage is 0.000 there.
Without the clear, the GPU depth image keeps the previous frame's depth/stencil. Particle-shaped black holes then accumulate over the title-screen sky. A frame replayed from a fresh depth surface was clean, which is how we tracked it down. If the inline-constant variant would be useful for another title, I can send its exact words. AI-assisted: yes (Claude Code) |
|
@alfalfa-star a small edge case we hit while testing a buffer-fill version of the same kernel on our side. The kernel reads its value with One more edge of the same kind. The kernel stores with AI-assisted: yes (Claude Code) |
|
@AlxcBTW thanks, both are right. I had also misread the kernel:
I logged Kena's own words in game ( @GustavoGamarra95 and @AlxcBTW, thanks for the Beast of Reincarnation and Sword of the Sea data. Since three UE titles run this kernel, it's likely UE's own HTILE clear. The inline-constant variant would be a separate matcher; I'd leave it out of this PR unless a title needs it alone. @oneandonlydean the AI-assisted line is filled in. AI-assisted: yes (Claude Code) |
|
Rechecked Beast of Reincarnation (PPSA29343) with f801797: its value V# and output format pass the new guards. In 170 s at 1080p output the driver noted 248 whole-HTILE clears (value 0), and the title screen renders without the stale blocks (Linux, RTX 3050 Laptop). Follow-up to "still analyzing" above: the blocks that remained on that title had a separate cause, storage results still pending under memory that a later dispatch wrote as a buffer (Unreal's transient allocator reuses it for both). That fix is #4654. |
|
Another UE5 data point: Minecraft Dungeons II (PPSA16064) clears its scene depth HTILE with this same 9-word kernel every frame, and this PR fixes its depth. Without it, the host depth image never clears. The depth pre-pass (greater-or-equal, reversed Z) keeps the nearest depth of the animated foliage from earlier frames. The base pass then fails its depth test, so two-sided masked materials (grass, flowers, leaves, the campfire flame) render solid black on the title screen, and gameplay scenes are mostly dark. The black shapes are larger than the plants because they hold the union of their earlier positions. A constant depth bias of about 10⁶ units on the base pass hides the symptom, which is how the stale depth showed up. The match: one dispatch per frame over the scene depth's HTILE, 0x60000 bytes, value 0. With 69a67ca and f801797 cherry-picked, the driver notes the clear every frame and the title renders the same as our reference build. That reference build ran this kernel as a buffer fill via an older local patch, which is how I found it. Tested by reviewer:
|
…lear PPSA01802 (Kena: Bridge of Spirits) clears its depth HTILE every frame with its own compute kernel: a 9-word dword fill that stores, through the stride-4 V# in user[0..3], a value loaded from the pointer in user[4..5]. Neither the DMA_DATA hook nor the fill kernels the driver recognizes saw it, so the depth kept the first frames' contents and the base pass and sky failed their depth test almost everywhere. The kernel is matched exactly when the dispatch is decoded, the value is read from guest memory, and the range is passed to NoteDepthMetadataFill like a fill, so the whole-HTILE and ZMask rules apply unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…raw value The kernel reads its value with s_buffer_load_dword through the V# in user[4..7] and stores it with buffer_store_format_x through the V# in user[0..3]. When the value V# covers less than a dword (NUM_RECORDS times its stride, or NUM_RECORDS bytes for stride 0) the load returns 0, and a store format other than 32_UINT, 32_SINT or 32_FLOAT converts the value, so in both cases the HTILE does not receive the dword read from memory. The match now requires both. Kena's own dispatch (format 32_UINT, a value V# of one 16-byte record) still matches. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
f801797 to
fd98327
Compare
The 9-word kernel boykopovar#3816 matches stores one dword per thread, loaded with s_buffer_load_dword through the descriptor in user[4..7]. Run as a compute dispatch, its stores are pending writes of unknown value, so a later read of DCC keys in that range waits for the GPU. fillBuffer now takes it as a uniform buffer fill when all of these hold: - the output descriptor's format is 32_UINT, 32_SINT or 32_FLOAT (20-22), so buffer_store_format_x stores the loaded dword unchanged; - the value descriptor covers the loaded dword: num_records times the stride (num_records alone when the stride is 0) is at least 4 bytes, the range the recompiler gives a scalar buffer load; - exactly 8 user SGPRs and group id X enabled, so the group id is s8; - a direct, non-partial dispatch, 64x1x1 threads, one group in Y and Z, and a 16-byte aligned range of whole 16-byte units. The value is read when the fill is recorded, after the labels queued before it, as the pattern-fill kernel's source is. When the value is one byte repeated and that byte is a DCC key code, the fill goes into the GPU key-store memo whatever ClassifyFill reports: the memo answers a key read only while this fill is still the newest pending write over the keys, and a fill over a color target's keys is not a storage image's keys fill. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 9-word kernel boykopovar#3816 matches stores one dword per thread, loaded with s_buffer_load_dword through the descriptor in user[4..7]. Run as a compute dispatch, its stores are pending writes of unknown value, so a later read of DCC keys in that range waits for the GPU. fillBuffer now takes it as a uniform buffer fill when all of these hold: - the output descriptor's format is 32_UINT, 32_SINT or 32_FLOAT (20-22), so buffer_store_format_x stores the loaded dword unchanged; - the value descriptor covers the loaded dword: num_records times the stride (num_records alone when the stride is 0) is at least 4 bytes, the range the recompiler gives a scalar buffer load; - exactly 8 user SGPRs and group id X enabled, so the group id is s8; - a direct, non-partial dispatch, 64x1x1 threads, one group in Y and Z, and a 16-byte aligned range of whole 16-byte units. The value is read when the fill is recorded, after the labels queued before it, as the pattern-fill kernel's source is. When the value is one byte repeated and that byte is a DCC key code, the fill goes into the GPU key-store memo whatever ClassifyFill reports: the memo answers a key read only while this fill is still the newest pending write over the keys, and a fill over a color target's keys is not a storage image's keys fill. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 9-word kernel boykopovar#3816 matches stores one dword per thread, loaded with s_buffer_load_dword through the descriptor in user[4..7]. Run as a compute dispatch, its stores are pending writes of unknown value, so a later read of DCC keys in that range waits for the GPU. fillBuffer now takes it as a uniform buffer fill when all of these hold: - the output descriptor's format is 32_UINT, 32_SINT or 32_FLOAT (20-22), so buffer_store_format_x stores the loaded dword unchanged; - the value descriptor covers the loaded dword: num_records times the stride (num_records alone when the stride is 0) is at least 4 bytes, the range the recompiler gives a scalar buffer load; - exactly 8 user SGPRs and group id X enabled, so the group id is s8; - a direct, non-partial dispatch, 64x1x1 threads, one group in Y and Z, and a 16-byte aligned range of whole 16-byte units. The value is read when the fill is recorded, after the labels queued before it, as the pattern-fill kernel's source is. When the value is one byte repeated and that byte is a DCC key code, the fill goes into the GPU key-store memo whatever ClassifyFill reports: the memo answers a key read only while this fill is still the newest pending write over the keys, and a fill over a color target's keys is not a storage image's keys fill. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd answer the keys from them Sword of the Sea clears DCC keys with two small compute kernels: the dword fill boykopovar#3816 matches (a value loaded through the descriptor in user[4..7]) and a dword fill of an inline constant through the descriptor in user[0..3]. Run as compute dispatches, their writes were noted with an unknown value, so the next refresh of a surface over those keys waited for the dispatch before it could read them: about 970 waits and 2.4 s per 10 s in the menu, all behind these two kernels. - Both kernels are stored through fillBuffer (the title-fill path) when the range is 16-byte aligned, the constant kernel's tgid is s4 and the value's descriptor covers the loaded dword (a load past it returns 0, which the compute dispatch keeps doing). The value is read once at record time, through the flush hook, and the fill is noted with its byte value. - WaitForKeyWriters answers the keys without a wait when the newest pending write over every key byte is such a fill and they agree with each other and with the bytes no pending write covers, as it already did for fills that leave the keys uncompressed. Temporary A/B switches (lab only): APS5_EXP_NO_DWORD_FILL_HLE=1 keeps the compute dispatches, APS5_EXP_NO_FILL_KEYS=1 waits as before. [fill] counts the fills taken this way. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 9-word kernel boykopovar#3816 matches stores one dword per thread, loaded with s_buffer_load_dword through the descriptor in user[4..7]. Run as a compute dispatch, its stores are pending writes of unknown value, so a later read of DCC keys in that range waits for the GPU. fillBuffer now takes it as a uniform buffer fill when all of these hold: - the output descriptor's format is 32_UINT, 32_SINT or 32_FLOAT (20-22), so buffer_store_format_x stores the loaded dword unchanged; - the value descriptor covers the loaded dword: num_records times the stride (num_records alone when the stride is 0) is at least 4 bytes, the range the recompiler gives a scalar buffer load; - exactly 8 user SGPRs and group id X enabled, so the group id is s8; - a direct, non-partial dispatch, 64x1x1 threads, one group in Y and Z, and a 16-byte aligned range of whole 16-byte units. The value is read when the fill is recorded, after the labels queued before it, as the pattern-fill kernel's source is. When the value is one byte repeated and that byte is a DCC key code, the fill goes into the GPU key-store memo whatever ClassifyFill reports: the memo answers a key read only while this fill is still the newest pending write over the keys, and a fill over a color target's keys is not a storage image's keys fill. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
What
Kena: Bridge of Spirits (PPSA01802) clears its depth HTILE every frame with its own compute kernel, which the driver didn't see as a clear. The depth image kept the first frames' contents. The base pass (greater-or-equal, reversed Z) and the sky then failed their depth test almost everywhere, and the title menu showed stale memory: black, white or striped, depending on the run.
The kernel is a 9-word dword fill, run as 2048x1x1 groups of 64 over the 0x80000-byte HTILE of a 3200x1800 D32S8 target:
The value comes from memory (through the V# in
user[4..7]), not from user data, so neither the DMA_DATA hook nor the known fill kernels match it.MatchMemoryValueFill(DepthSurface.cpp) matches the kernel exactly and requires:buffer_store_format_xwrites the value unconverted;The range is groups × 64 dwords, clipped to NUM_RECORDS.
NoteMemoryValueFillreads the value from guest memory when the dispatch is decoded and passes range and value toNoteDepthMetadataFill. The whole-HTILE coverage and ZMask/SMem rules of fix(agc): depth fast clears through the HTILE metadata #374 apply unchanged.Dispatch.cppcalls both next to the existingHtileDepthClearAddresshook, for direct dispatches with group counts only. The dispatch itself still runs.TechnicalDebt: the HTILE fast clear entry now names the second kernel, and notes that the value is read at decode, so a value that earlier GPU work in the same submission hasn't written back yet is not seen.
Relation to #2053
#2053 (esatturan) covers HTILE written by any kernel, by reading the words when the depth target is bound. On Kena it takes only the first clear. The first bind reads 90000 of 90000 words with ZMask 0. All 299 later binds return at
UnchangedSince, because the kernel's GPU stores don't move the write generation. Details are in #2053 (comment).This PR is narrower and catches the clear at the dispatch. The two can coexist: a CP-side note here, a memory read there. If #2053 learns to see GPU-written HTILE, this matcher is still harmless (it notes the same clear earlier), and I'm happy to drop it if the maintainers prefer one path.
Tested
Arch Linux, KDE Plasma, GCC, Release, RADV (RX 9070 XT, Mesa 26.2.4), on main a11c8f7.
New
memoryValueFillTestsinagc_driver_graphicscovers:The user data are Kena's own words (logged in game): output format 32_UINT, and a value V# of one 16-byte record.
agc_driver_depth_fast_cleargains memory-value fills over the harness HTILE:With
NoteMemoryValueFillstubbed out, it fails at "a memory value fill of ZMask 0 over the whole HTILE did not clear the depth to DB_DEPTH_CLEAR (1.0)".ctest: 832/832.tools/check_conventions.pypasses (both on the second commit too).Kena in game: on our local stack, since Kena doesn't reach its menu on bare main for other reasons (Recompiler: packed pixel ancillary word read whole after its VGPR is reused ("unsupported live use (used by BitFieldUExtract 16 11, SelectU32 ...)") #2548 shaders, layered draws). The stack was main 1e8fff6 + feat(agc): depth planes sampled as 2D arrays, cubes and integer views #425 + fix(libSceAgcDriver): retire a depth surface once another writer covers its memory #454 + feat(agc): render layered draws into 3D color targets #1077 + fix(recompiler): read NumWorkgroups in every mesh shader so RADV sizes its user data #3645 + feat(shader): give reads the masked-select pass keeps the whole pixel ancillary word AlxcBTW/AnyPS5#39, with the same change.
app.elf+0x1c6a46b, which happens with and without this change.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