fix(vk): terrain tiles the GPU drew are written into guest memory (the ground garbling after minutes in one area) #2545

Merged
jarynclouatre merged 1 commit from fix/drawn-tile-writeback into main 2026-11-12 03:05:21 +00:00
jarynclouatre commented 2026-11-11 22:48:34 +00:00 (Migrated from github.com)

After some minutes of play inside one area the ground can turn into a woven or smeared pattern and stay that way until a
restart: Dodongo's Cavern's floor, the sand outside Gerudo Valley, Hyrule Field. The terrain's normal tiles are all zero
there. Their texels were drawn on the GPU and never reached guest memory, and the game copies that memory.

Refs #1717 (and the reports folded into it: #2213, #2276, #2297). Tracking: #2508.

What goes wrong

  • The game bakes each terrain chunk's normal tile on the GPU: 260 x 260, RG8 (NVN 0xd), nine levels, drawn once.
  • Later it moves a chunk's tiles into another block with a CPU copy of their memory, and makes new textures over the
    copies. On the console the drawn texels are in that memory. Here they are only in the Vulkan image, so the copy of a
    drawn tile is all zero, and the terrain samples a zero normal tile.
  • The log line vk: upload over a drawn image: ... 260x260x1 x1 NVN 0xd ... finalized is a second way into the same
    fault: the game writes a finalized tile's memory (zeros, before the slot is used again) and that memory is uploaded
    over the drawn image.
  • A map change resets the tile pool, so a tour of teleports does not show it. It takes play inside one map.

What changes

  • A small texture (1 MB or less, in a pool's own memory) with a mip chain is noted when its GPU contents begin.
  • Two frames later, if it was drawn in that one frame only, its image is copied to a staging buffer. The copies of all
    the textures due in a frame are recorded with the present's uploads, as the VRAM compaction's copies are. Nothing
    waits for the GPU: a later frame, once the GPU is past that recording, writes the texels into memory. A first version
    submitted the batch on its own and waited: 3 to 8 ms of the frame per batch, and 232 ms once behind an area load's
    uploads.
  • The write uses the write-back that evictions and remaps already use, so pages the CPU wrote since the drawing keep
    the CPU's bytes. A texture destroyed, drawn or uploaded over between the copy and the write, or whose storage another
    image was drawn over, is dropped. An image with a copy outstanding is kept until the GPU is past it.
  • The texels go only into memory the CPU has not written since: the texture's memory is read right before the write and
    has to be all zero (the game clears a tile's slot before it draws the tile) or still hold the texture's own last
    write-back. The page generations know a CPU write only once its capture was taken, so this check does not rely on
    them. A texture left out this way is counted in the log line.
  • The game's threads run while the replay works, so the write itself is a compare and write: the texels are staged
    against the memory the check read, the memory is compared with it once more, and each word goes in only where it
    still holds that value. If a guest thread wrote a word meanwhile, the words this write put in are taken out again the
    same way. Either all of the texture's texels are in its memory or the memory is as the game has it.
  • Left out: textures drawn again in a later frame (render targets in use), ones another image whose storage overlaps
    theirs (from any start address) was drawn in that frame or since (that memory is the newer image's), ones the GPU
    only cleared (a font cache, whose glyphs come from the CPU), compressed, depth and multisampled ones, and anything
    flushable() already refuses.
  • The rule is in gpu/vk/drawn_writeback.h. RX_VK_NO_DRAWN_WRITEBACK=1 turns the change off.
  • One log line for the first 12 batches and every 200th after: how many textures, the time, the totals.

Credit and overlap

  • @snoozedDev's recording on #1717 pinned the cause (the zero tiles made from memory, the CPU copy) and showed that a
    write-back two frames after the drawing holds for 80 minutes. The two-frame rule here follows that result. Their case
    of a tile finalized the frame after its drawing is covered: a finalize does not stop the write-back.
  • @Joey-Magnus took #1717 earlier today (15:11 UTC). I have been working on this one since 2026-11-10, starting from
    my own play (the woven sand outside Gerudo Valley, then Dodongo's Cavern) and its logs (the upload over a drawn image line in three sessions with the woven ground and in none without), and I only learned of the claim this
    afternoon, when the fix was already built and in testing. I am not on Discord, where claims are made, so I had no way
    to claim it there. Sorry for the overlap, Joey: I am
    opened this as a draft first so the work and the numbers were in one place, not to step on yours. I am happy to compare
    notes, fold the best parts of both into one PR, test yours on my PC, or close this one if yours lands first.

Testing

  • Game-free: python3 -I -B tests/test_drawn_writeback.py passes (Linux, g++): the rule's cases, the compare and
    write with a guest write before, during and after it and its undo, and that
    vk_texture.cpp still notes a texture when its GPU contents begin, asks the rule each frame and checks the memory
    before the write.
    tests/test_compressed_over_rt.py and tests/test_astc_bc_policy.py pass too. tests/run_all.py on Linux: 361 passed, 62
    skipped (no Vulkan headers or .NET here), 1 failed: test_bake_portability.py, which needs the zstandard module this
    machine does not have.
  • In the game, Windows 11, RTX 4070 Ti SUPER, driver 621.14, a headless scripted run: nine minutes of running inside
    Hyrule Field as adult, the same input both times, on this branch's first version (it waited for the GPU; the rule and
    the memory check as here), with a local debug line (not part of the change) for each small mip-chained texture made
    from guest memory.
    • The fault did not happen in that run. With the change off the game made no 260 x 260 normal tile from memory at
      all (5000 textures made from memory, none of them such a tile), so none could be zero, and the run says nothing
      about the fix itself.
    • With the change on: 90 textures written back (19 MB), none left out because the CPU had written their memory, none
      failed, no crash. That version's time on the frame is in What changes.
  • A play session on this branch (the same local debug line, which also counts tiles uploaded over a drawn image), same
    PC, outside Gerudo Valley, where I had seen the sand turn into the woven pattern before; these figures are from the
    session's first 7 minutes, the start and the way there included. The ground looked right from the start, with no bare sand first and no woven pattern. In that time the game
    made 122 of the 260 x 260 normal tiles from memory, every one with real texels and none all zero, and one tile
    uploaded over a drawn image held real texels too. 78 textures were written back (16 MB, 15 ms in all, at most 1.9 ms
    for a batch, finished 1 frame after their copies), none left out, dropped or failed, no "slow present" beside a
    write-back, no crash.

Not tested

  • The reporters' places on their cards (Dodongo's Cavern on #1717, the cave on #2276). AMD and Intel cards. Linux in the game.
  • Mario Kart World: the rule also takes its small mip-chained textures the GPU draws in one frame, if it has any.
  • A tile the game copies between its drawing and the write, three frames at the least and more when the GPU is behind.
  • A guest write during the write-back, in the game: the compare and write and its undo are covered by the game-free
    test only. Both go by content, so a guest store of the value already there cannot be told from no store: zero over
    zero while the texels go in, or the texel's own value over a word that had gone in before an undo (that word goes
    back to what the check saw). The undo also goes a word at a time, so a guest store into part of a word that had gone
    in leaves the rest of that word's texels beside it. Closing that would take stopping the game's threads for the write. The write-backs at
    eviction and remap are unchanged; they go by the page generations alone.
  • A game that draws such a texture over memory it did not clear first: its texels stay out of memory, as before this change.
After some minutes of play inside one area the ground can turn into a woven or smeared pattern and stay that way until a restart: Dodongo's Cavern's floor, the sand outside Gerudo Valley, Hyrule Field. The terrain's normal tiles are all zero there. Their texels were drawn on the GPU and never reached guest memory, and the game copies that memory. Refs #1717 (and the reports folded into it: #2213, #2276, #2297). Tracking: #2508. ### What goes wrong - The game bakes each terrain chunk's normal tile on the GPU: 260 x 260, RG8 (NVN 0xd), nine levels, drawn once. - Later it moves a chunk's tiles into another block with a CPU copy of their memory, and makes new textures over the copies. On the console the drawn texels are in that memory. Here they are only in the Vulkan image, so the copy of a drawn tile is all zero, and the terrain samples a zero normal tile. - The log line `vk: upload over a drawn image: ... 260x260x1 x1 NVN 0xd ... finalized` is a second way into the same fault: the game writes a finalized tile's memory (zeros, before the slot is used again) and that memory is uploaded over the drawn image. - A map change resets the tile pool, so a tour of teleports does not show it. It takes play inside one map. ### What changes - A small texture (1 MB or less, in a pool's own memory) with a mip chain is noted when its GPU contents begin. - Two frames later, if it was drawn in that one frame only, its image is copied to a staging buffer. The copies of all the textures due in a frame are recorded with the present's uploads, as the VRAM compaction's copies are. Nothing waits for the GPU: a later frame, once the GPU is past that recording, writes the texels into memory. A first version submitted the batch on its own and waited: 3 to 8 ms of the frame per batch, and 232 ms once behind an area load's uploads. - The write uses the write-back that evictions and remaps already use, so pages the CPU wrote since the drawing keep the CPU's bytes. A texture destroyed, drawn or uploaded over between the copy and the write, or whose storage another image was drawn over, is dropped. An image with a copy outstanding is kept until the GPU is past it. - The texels go only into memory the CPU has not written since: the texture's memory is read right before the write and has to be all zero (the game clears a tile's slot before it draws the tile) or still hold the texture's own last write-back. The page generations know a CPU write only once its capture was taken, so this check does not rely on them. A texture left out this way is counted in the log line. - The game's threads run while the replay works, so the write itself is a compare and write: the texels are staged against the memory the check read, the memory is compared with it once more, and each word goes in only where it still holds that value. If a guest thread wrote a word meanwhile, the words this write put in are taken out again the same way. Either all of the texture's texels are in its memory or the memory is as the game has it. - Left out: textures drawn again in a later frame (render targets in use), ones another image whose storage overlaps theirs (from any start address) was drawn in that frame or since (that memory is the newer image's), ones the GPU only cleared (a font cache, whose glyphs come from the CPU), compressed, depth and multisampled ones, and anything `flushable()` already refuses. - The rule is in `gpu/vk/drawn_writeback.h`. `RX_VK_NO_DRAWN_WRITEBACK=1` turns the change off. - One log line for the first 12 batches and every 200th after: how many textures, the time, the totals. ### Credit and overlap - @snoozedDev's recording on #1717 pinned the cause (the zero tiles made from memory, the CPU copy) and showed that a write-back two frames after the drawing holds for 80 minutes. The two-frame rule here follows that result. Their case of a tile finalized the frame after its drawing is covered: a finalize does not stop the write-back. - @Joey-Magnus took #1717 earlier today (15:11 UTC). I have been working on this one since 2026-11-10, starting from my own play (the woven sand outside Gerudo Valley, then Dodongo's Cavern) and its logs (the `upload over a drawn image` line in three sessions with the woven ground and in none without), and I only learned of the claim this afternoon, when the fix was already built and in testing. I am not on Discord, where claims are made, so I had no way to claim it there. Sorry for the overlap, Joey: I am opened this as a draft first so the work and the numbers were in one place, not to step on yours. I am happy to compare notes, fold the best parts of both into one PR, test yours on my PC, or close this one if yours lands first. ### Testing - Game-free: `python3 -I -B tests/test_drawn_writeback.py` passes (Linux, g++): the rule's cases, the compare and write with a guest write before, during and after it and its undo, and that `vk_texture.cpp` still notes a texture when its GPU contents begin, asks the rule each frame and checks the memory before the write. `tests/test_compressed_over_rt.py` and `tests/test_astc_bc_policy.py` pass too. `tests/run_all.py` on Linux: 361 passed, 62 skipped (no Vulkan headers or .NET here), 1 failed: `test_bake_portability.py`, which needs the zstandard module this machine does not have. - In the game, Windows 11, RTX 4070 Ti SUPER, driver 621.14, a headless scripted run: nine minutes of running inside Hyrule Field as adult, the same input both times, on this branch's first version (it waited for the GPU; the rule and the memory check as here), with a local debug line (not part of the change) for each small mip-chained texture made from guest memory. - The fault did not happen in that run. With the change off the game made no 260 x 260 normal tile from memory at all (5000 textures made from memory, none of them such a tile), so none could be zero, and the run says nothing about the fix itself. - With the change on: 90 textures written back (19 MB), none left out because the CPU had written their memory, none failed, no crash. That version's time on the frame is in What changes. - A play session on this branch (the same local debug line, which also counts tiles uploaded over a drawn image), same PC, outside Gerudo Valley, where I had seen the sand turn into the woven pattern before; these figures are from the session's first 7 minutes, the start and the way there included. The ground looked right from the start, with no bare sand first and no woven pattern. In that time the game made 122 of the 260 x 260 normal tiles from memory, every one with real texels and none all zero, and one tile uploaded over a drawn image held real texels too. 78 textures were written back (16 MB, 15 ms in all, at most 1.9 ms for a batch, finished 1 frame after their copies), none left out, dropped or failed, no "slow present" beside a write-back, no crash. ### Not tested - The reporters' places on their cards (Dodongo's Cavern on #1717, the cave on #2276). AMD and Intel cards. Linux in the game. - Mario Kart World: the rule also takes its small mip-chained textures the GPU draws in one frame, if it has any. - A tile the game copies between its drawing and the write, three frames at the least and more when the GPU is behind. - A guest write during the write-back, in the game: the compare and write and its undo are covered by the game-free test only. Both go by content, so a guest store of the value already there cannot be told from no store: zero over zero while the texels go in, or the texel's own value over a word that had gone in before an undo (that word goes back to what the check saw). The undo also goes a word at a time, so a guest store into part of a word that had gone in leaves the rest of that word's texels beside it. Closing that would take stopping the game's threads for the write. The write-backs at eviction and remap are unchanged; they go by the page generations alone. - A game that draws such a texture over memory it did not clear first: its texels stay out of memory, as before this change.
Commenting is not possible because the repository is archived.
No description provided.