Skip to content

fix(agc): execute the DB's out-of-place depth expand - #6146

Open
EagleBearerYS wants to merge 39 commits into
boykopovar:mainfrom
EagleBearerYS:fix/agc-db-depth-expand
Open

EagleBearerYS wants to merge 39 commits into
boykopovar:mainfrom
EagleBearerYS:fix/agc-db-depth-expand

Conversation

@EagleBearerYS

@EagleBearerYS EagleBearerYS commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

What

A draw that sets DB_RENDER_CONTROL's DEPTH_COMPRESS_DISABLE with DB_Z_WRITE_BASE different from DB_Z_READ_BASE is a DB expand. The DB writes the read surface's depth plane, uncompressed and in the same swizzle, at the write base. With DB_DEPTH_CONTROL 0, DecodeState took the "always-pass depth/stencil state is rendered without a depth target" path, so the draw drew nothing and the write base kept whatever the CPU had written there.

Resident Evil Village (PPSA01556, 01.210.000) issues four such draws per frame in its first 3D scene, then lights, fogs, resolves and blends the scene from the copy. With zeros there, which is the far plane in its reverse Z, the scene renders as soft light discs on black. One of the game's draws, from a register trace at first player control:

DB_RENDER_CONTROL 0x00000060  DB_DEPTH_CONTROL 0x00000000  DB_DEPTH_VIEW 0x00000000
DB_Z_INFO 0x20000183  DB_STENCIL_INFO 0x20000181
DB_Z_READ_BASE 0x202a8a0000  DB_Z_WRITE_BASE 0x20298b0000
DB_STENCIL_READ_BASE 0x2029430000  DB_STENCIL_WRITE_BASE 0x2029430000  DB_HTILE_DATA_BASE 0x20293e0000
CB_COLOR_CONTROL 0x00cc0000  CB_TARGET_MASK 0xffffffff  CB_SHADER_MASK 0x00000000
PA_SC_AA_CONFIG 0x00108001

That's Z_32_FLOAT, one sample, SW_64KB_Z_X, 1920x2160. All other depth draws in the frame read and write at the same base.

  • Recognition: DecodeDepthExpandPass decodes the target with the draw state's own decoder (depthTargetFor) and takes exactly this shape:

    • DEPTH_COMPRESS_DISABLE set, and no bits besides the two compress-disable bits and bit 13. Without a copy bit, COPY_CENTROID and COPY_SAMPLE select nothing and are ignored, so fix(agc): accept a copy's sample selection on a draw that copies nothing #5937's 0xc0 shape is the same expand;
    • no depth, stencil or depth-bounds tests, and no colour writes by ColorWriteMask's rule, so an INVALID-format target writes nothing, as for a draw;
    • a single-sample Z_32_FLOAT or Z_16 SW_64KB_Z_X surface at the view's first slice, without mips or layer exports. Its stencil plane stays in place, is read-only, or moves, with the depth plane or alone;
    • 64 KiB-aligned bases;
    • a viewport and scissor covering the surface, by the coverage check the colour-metadata pass uses.
  • The copy: each moving plane of the live depth surface at the read base, with any pending fast clear applied, is copied into the storage image of a view of its write base and marked written: R32F or R16 UNORM SW_64KB_Z_X for the depth plane, and R8 UINT at DB_STENCIL_WRITE_BASE for the stencil plane. A slice of a depth array goes to the same slice of the write base. Sampled views of the write base use that image. Raw and CPU reads store it into guest memory in the Z swizzle, as for any written storage image.

  • In order: the expand runs only from the draw precheck, which decodes it and the colour-metadata pass as one outcome and runs either the same way: it drains the draw pipeline, takes the GPU lock, writes the packet's labels and runs the pass.

    • The precheck runs on queue 0's packet worker while the draws before it can still be queued for the draw pipeline's committer, which perf(agc): pipelined draws on by default #1202 turned on by default. Recorded ahead of them, the expand's copy read the depth from before them, its repeat check didn't see their writes, and a read surface that a queued draw creates was read from guest memory. The drain, as the colour-metadata passes have (a39261b), records the copy after every earlier draw. An indirect expand was already drained before the precheck.
    • The drain waits only when draws are queued, and at most for the draws the pipeline holds (8 by default); Resident Evil Village issues four expands per frame.
    • The state-only depth check refuses an out-of-place expand ("is run by the draw precheck, in order, and is not drawn"). A path that classifies a draw from its state alone, without running the pass, reports the expand instead of drawing or skipping it.
  • Address-based builds: they read host-imported heaps in place without storing the results pending there. So the expanded planes still pending in imported write bases are stored ahead of every address-based build (StoreDepthExpandWrites). Resident Evil Village's copy shader reads its write base through a V# right after the expand.

    • Host imports are made per registered range, so a plane can lie across two imports, or across an import and a range that is heap-mirrored or copied. A plane is stored when any existing import overlaps it (HostImportOverlaps), not only when one import holds all of it.
    • When every registered range under the plane has an import, the write-back records the plane on the GPU into each range's import, one segment per range (on main, since ea3a215). When part of it has no import, the write-back goes through the CPU after a wait for the expand.
    • The store only looks imports up: it makes and evicts none, so it can't dissolve a cached address space. A plane that no import holds stays noted while it is pending, and UploadFinish stores again after the build's own imports, so a build that imports the range itself stores the plane before reading it. When nothing is noted, that second call is a lock and a scan of an empty map.
    • These builds also skipped the results pending under their own read-only V#s. A read-only V# inside a heap the build maps whole was merged into the heap's region on the per-build path, or dropped inside a base region of the cached address space, so nothing stored what an image had left pending there, and the GPU read the import's older bytes. UploadFinish now stores the results pending under every read-only V# of an address-based build, as it already did for written ones. Pointer reads keep the exemption; for them the expand's planes are still stored ahead of every such build, as above.
  • Indirect expands: Resident Evil Village issues its expands as indirect draws.

    • DRAW_INDIRECT, DRAW_INDEX_INDIRECT and their multi forms read their count and records on the CPU, as the draw path's CPU route does.
    • They run the expand when a record draws something, and write nothing otherwise, as does a direct expand, indexed or not, of no vertices, indices or instances.
  • No depth surface at the read base yet (Resident Evil Village does this once during boot): the write base takes DB_DEPTH_CLEAR when an HTILE depth clear of the surface is pending, and otherwise the read base's guest bytes.

  • Repeats: an expand whose plane the write base's image already holds is skipped. The surface's write serial and the image's content version decide that; 7 expands in the test make 3 copies.

  • What throws: every other out-of-place expand, naming the reason: Z_24 or no depth plane, multisampled, other swizzles, mips and layer exports, partial coverage, and one into a live depth surface of the same device or from one of another extent, format or sample count. An in-place expand changes nothing modelled, as before.

  • Debt: a new "Out-of-place DB expand" entry under Unknown function info lists what is inferred and what is left:

    • the HTILE words aren't read, and an HTILE stencil clear isn't modelled;
    • the draw's geometry isn't checked, and its pixel shader never runs;
    • R32_UINT views of the write base still throw;
    • the first expand into a write base uploads bytes that the copy then overwrites;
    • a guest CPU read after a label reads the bytes from before the expand, as for any pending storage-image result;
    • a depth surface bound later at the write base without HTILE starts from the expanded plane (TakeStorage, fix(agc): start a new depth surface from the storage writes over its plane #5446); one bound with HTILE doesn't;
    • none of it is measured on the console.

    "Reused depth storage" notes the exception. A second new entry, "Pending storage-image results under the heaps of an address-based build", says what such a build stores and what it leaves to pointer reads. The "HTILE address of a depth array slice" entry goes: since fix(agc): give each slice of a depth array its own HTILE #5111 each slice has its own HTILE.

  • Not done here, with the reasons:

    • DB_RENDER_OVERRIDE's forced-dirty copies (0x28000000 depth, 0x50000000 stencil) with moving bases: public facts name the bits but not what the DB writes under them, and feat(agc): add experimental depth clear and copy recognition #1395 takes that path.
    • Raw reads of the read base after an in-place expand: a raw read (CPU, DMA, address-based copy) of a live single-sample depth surface is stale with or without an expand. That needs coherent read-back of live depth surfaces, a general feature, and a store-back on every in-place expand would cost a full-plane copy per expand.
    • Storing pending planes before a label becomes CPU-visible: that changes coherence for every pending GPU result, not only expands.
    • Decoding the guest's HTILE words in the branch with no surface: the copy reads the live surface, which the emulator renders uncompressed, and a pending HTILE clear is already applied. fix(agc): take depth fast clears from HTILE written by any kernel #5627's reader could serve it if a title clears part of an HTILE that way.
    • Running an indirect expand as an indirect dispatch: the records are read on the CPU, which waits when the GPU wrote them; no title is known to issue indirect expands often.

Tested

  • agc_driver_depth_expand_out_of_place, an execution test through PM4 using the game's register values:
    • a depth-only draw writes a gradient into a 160x96 surface; the expand copies it to another base;
    • the copy is read back through an R32F SW_64KB_Z_X image_load and through GuestMemory::Read, detiled with the driver's SW_64KB_Z_X equation.
    • On main 9f03c4f the write base holds 0 of 15,360 texels of the depth. With the change it holds 15,360 of 15,360 both ways, with one raster sample and with the game's two (PA_SC_AA_CONFIG 0x00108001).
    • It also covers:
      • indirect expands: DRAW_INDIRECT, DRAW_INDEX_INDIRECT, and a record that draws nothing;
      • a read base with no depth surface: HTILE-cleared, and plain guest bytes;
      • buffer and address-based buffer copies of the write base right after the expand;
      • a repeated expand, which copies only after a change.
  • agc_driver_depth_expand_review checks the expand beyond the game's case:
    • a surface without a stencil plane;
    • a drawn stencil plane: its contents are kept, and the read base, stencil and HTILE guest bytes are unchanged;
    • an in-place expand, and an always-pass draw with equal bases;
    • a second expand after the surface changed;
    • a GPU buffer read right after the expand;
    • one submission expanding to two write bases;
    • three expands right after a depth draw still queued for the committer, one for each way the expand finds its source: a surface that already exists, a repeat of an expand into the same write base, and a read surface that the queued draw creates. The test holds the committer with a blocking commit, as agc_driver_pipelined_fill does for fills, and puts the depth draw and the expand in one submission. On GitHub's Linux runner, with main's DrawPrecheck.cpp, each of 10 runs failed all three cases on the texture read of the write base, with the draw queued every time; with the drain, all three passed 20 of 20 (run).
  • Six more cases in agc_driver_depth_expand_out_of_place copy the write base with address-based programs right after the expand:
    • Its memory as two registered ranges, both imported (split-imported-buffer-read, split-imported-pointer-read), or one imported and the other heap-mirrored (split-mirrored-buffer-read) or copied (split-copied-buffer-read).
      • Without 103316e, on main and on this branch's base, all four fail: expanded planes stored ahead of the copies: 0 (expected 1), then 0 of 15360 texels hold the read surface's depth with both ranges imported, or 3072 of 15360 with one mirrored or copied.
      • With it, all four print 1 and 15360 of 15360.
      • On main, a temporary print in the write-back showed DB expand: GPU path, 2 segment(s), 131072 bytes with both ranges imported. With one mirrored or copied it showed DB expand: CPU path, 0 segment(s), 65536 bytes, after that range's own write-back of the other 64 KiB.
    • Every host import refused (unimported-mirrored-buffer-read, unimported-copied-buffer-read): these pass with and without 103316e, because the mirror refresh and the copy store the pending plane themselves.
  • address-based-storage-image-read in agc_driver_depth_expand_out_of_place stores a value to every texel of an image with one compute dispatch, then copies the image's bytes through a read-only V# with the address-based copy kernel. It does that twice with different values: the first copy takes the per-build path, the second the cached address space. Without bfcd183 both copies hold 0 of 15360 texels; with it, 15360 of 15360.
  • depthExpandPassTests in agc_driver_graphics check the decoder on the game's values and every refusal.
  • Completeness follow-ups (the 9 commits from f2f52d0 to a6005ba, merged at the head), each red without its change on CI's Linux configuration (lavapipe) and green with it:
    • agc_driver_graphics: the state-only refusal (with it reverted: "an out-of-place DB expand that reached the draw path was not refused"), the 0xc0 shape, an INVALID-format colour target and a non-finite clear (with the old decode: "… with color writes is not implemented"), Z_16 and Z_24, slices and layer exports, and the stencil decode.
    • agc_driver_depth_expand_review, each read through a texture and raw:
      • a Z_16 surface: 15360 of 15360 through the R16 view and raw, detiled with the 16-bit equation (before: "… of a depth plane other than Z_32_FLOAT is not implemented");
      • slice 1 of a two-slice array: 15360 of 15360, while slice 0 keeps the CPU's bytes (before: "… of a depth view with a base slice or a mip level is not implemented");
      • a stencil plane moved with the depth plane and alone: the two-value pattern matches 15360 of 15360 through the R8 view and raw (before: "… of the stencil plane is not implemented").
    • agc_driver_depth_expand_out_of_place:
      • direct-indexed: DRAW_INDEX_2 with no indices, or no instances, keeps 32768 of 32768 dwords the CPU wrote (before: 17408), and a three-index expand matches 15360 of 15360;
      • five-range-write-base (a write base across five registered ranges, as Resident Evil Village's spans five 4 MiB mappings), compute-queue-read (an ACB copy), dma-read (DMA_DATA), depth-bind-of-write-base and store-without-import-room: 15360 of 15360 in each check. The last pins the import-neutral store, which no lavapipe run can make fail: every address-based build there imports its ranges itself.
      • it also runs as agc_driver_depth_expand_out_of_place_inline, with APS5_PIPELINED_DRAWS=0.
    • Every commit builds and passes the expand, graphics, BDA, sparse-buffer, address and storage tests from the one that adds them; the expand tests pass 5 of 5 with the pipeline at its default and with it off.
  • Linux (lavapipe), with CI's flags:
    • Merged with main a86577e (13ad1f4, the head), which brings perf(agc): replace the plane copies around a depth plane's UAV clear by a depth clear #7178's depth clear and fix(agc): read a 32-bit depth-bits T# over guest memory through a float view #7346's 32-bit depth-bits debt entry. The debt file keeps both of main's entries, with this PR's note in "Reused depth storage". The merges before it: main f474fb7 (c053a55; decodeDepth takes main's DB_RENDER_CONTROL check) and main 55f82fe (24ec2d4; the debt file keeps main's plane-fill entry and drops the HTILE slice entry, as 2559452 does). On GitHub's runners (run): the ^agc tests (709, 0 failed), with the expand tests, pass 3 runs in a row on Linux, and the libc and libkernel suites pass. On Windows the only failures are agc_driver_pm4_replay_tool and agc_shader_replay_tool, which fail the same way on that runner for an unrelated branch (run).
    • At 2559452, on GitHub's runners: ctest -R '^agc' ran 604 tests, 1 failed (agc_driver_preparation_scope_settle, as on main), 41 skipped on Linux, and 598 tests, 1 failed (agc_driver_pm4_replay_tool, as on main), 466 skipped on Windows (run). The expand tests and agc_driver_graphics pass 10 runs in a row on Linux, and agc_driver_graphics on Windows, whose runner has no Vulkan driver for the others; with the sources of the previous head (d359e15) the new cases don't build (run).
    • The previous head merges main 2e5dd5a, which brought fix(agc): take depth fast clears from HTILE written by any kernel #5627, fix(agc): apply completion-proven native depth clears #5554 and fix(agc): retire fully released native depth owners #5524's changes to DepthSurface.cpp: the HTILE clear, ApplyFastClear and surface retirement, which the expand uses. The only conflict was one include block, where main dropped a duplicate GuestMemory.hpp include next to the two this PR adds.
    • At the head, on GitHub's Linux runner: ctest -R '^agc' ran 603 tests, 1 failed (agc_driver_preparation_scope_settle), 41 skipped (run). On main 2e5dd5a alone: 601 tests, the same one failed, 41 skipped (run). The two extra tests are this PR's. The two expand tests and seven others around the code the merge changed passed 10 of 10 at the head (run).
    • On GitHub's Linux runner, agc_driver_depth_expand_out_of_place fails with 103316e's code reverted (run), and again with bfcd183's reverted (run). With the code, the two expand tests pass 3 of 3, and in the second run so do agc_driver_graphics, agc_driver_bda_device, agc_driver_bda_span_reads and agc_sparse_buffer_holes. The libc and libkernel suites pass in every build.
    • Every commit builds, and the three tests pass at each commit from the one that adds them.
    • Before the merge of main 8856f18, in WSL: ctest -R '^agc' at 6596a1d ran 558 tests, 0 failed, 40 skipped (the same 40 as on main), and the two expand tests passed 10 of 10. Merged into main d92a8da: 579 of 579 in two of three runs (41 skipped), the third failing only agc_driver_preparation_scope_settle, which also fails on main. The full suite there: 1195 of 1201; the other 6 are 5 network tests that need IPv6, which the build VM lacks, and that flake.
    • At the first fix commit, the game's case also passed under the validation layers with synchronization validation, with no SYNC hazards.
  • Windows: the full Build workflow on my fork at the head, 2559452 (run): MinGW build, then ctest: 1,293 of 1,293 pass, 488 skipped, all expected by check_skips.py.
  • Windows: the full Build workflow on my fork at 7809e9d merged into main 8856f18, the tree of the head's merge commit (run): MinGW build, then ctest: 1267 tests, 0 failed, 475 skipped, all expected by check_skips.py. agc_driver_graphics passed. The workflow carried the runner-image CA-bundle fix (CI only, no source change). The head itself built with MinGW in the sweep's Windows job (run), where ctest -R '^agc' ran 597 tests: 465 skipped, and 1 failed, agc_driver_pm4_replay_tool, which fails the same way on main 2e5dd5a in that job (the tool exits 0xc0000139 there). Windows runners have no Vulkan driver, so the expand execution tests are skip stubs there and ran on a GPU separately (next item).
  • Windows on an NVIDIA GeForce RTX 3070 Laptop GPU (driver 617.42), Windows 11 10.0.26300: a MinGW (GCC 15.2.0) Release build, ANYPS5_REQUIRE_VULKAN=1 with only the NVIDIA driver visible, ctest -R '^agc_driver_depth_expand_(out_of_place|review)$' --repeat until-fail:5.
    • At the head, 2559452: ctest -R '^agc_driver_depth_expand_(out_of_place|out_of_place_inline|review)$|^agc_driver_graphics$' --repeat until-fail:5 passes all four 5 of 5. Every expand case holds 15360 of 15360 in every run: the Z_16 read base within one 16-bit step, and its 16-bit raw read and R16 UNORM texture read; the stencil-only and depth-and-stencil expands (7696 texels of 0x5a and 7664 of 0xa5); slice 1 of a depth array, with slice 0 keeping the CPU's bytes; and the queued-draw, two-write-base, in-place and always-pass cases.
    • On b7b1ca2's tree (67ab316, identical to it), before the merge of main 8856f18 and the drain, merged with fix(agc): bind a read-only V# over the cached address space through the region that holds it #5815's follow-up for one combined run, both pass 5 of 5. The four split cases print expanded planes stored ahead of the copies: 1 (expected 1) and 15360 of 15360, and the address-based storage-image read prints 15360 of 15360 texels hold it for both copies, through the cached address space and without it.
    • The tolerance change (a4137dc) fixes a failure this PR had on that GPU. At e252c03, agc_driver_depth_expand_review failed 3 of 3 with second expand: second read base: 15358 of 15360 texels hold the drawn depth.
      • That line is the test's own check that its depth draw wrote the gradient, before any expand. The expand's copies matched.
      • The two far-corner texels of the second gradient (offset 0.5), (158, 95) and (159, 95), came back 4.05e-6 and 4.11e-6 low: 68 and 69 ulps at 0.875. The test allowed a fixed 4e-6, which lavapipe always met exactly.
      • Both expand tests now accept half the gradient's step in x (0.5 / 65536, about 7.6e-6). A read of the wrong texel is still a full step off.
  • Resident Evil Village (PPSA01556, 01.210.000) on an NVIDIA GeForce RTX 3070 Laptop GPU (driver 617.42), Windows 11 10.0.26300, Ryzen 9 5900HS, 15.4 GB:
  • python3 tools/check_conventions.py --base origin/main: passed.

Checklist

Overlap: #6188 (draws compiled ahead of queue 0) rewrites most of DrawPrecheck.cpp, including precheckDraw, where this PR runs the DB expand and the colour-metadata pass as one outcome. Whichever merges second carries the other's change: in #6188's design, a DepthExpand outcome next to MetadataPass, decoded by DecodeDepthExpandPass and run by the executor after the drain. Without one, #6188's ahead classification gets the state-only refusal, so a merge that forgets the expand fails loudly instead of dropping it.

#5925 (resolving ahead) guards only the colour-metadata branch of precheckDraw with resolvingAhead(). Here both passes share one block, so DrawPrecheck.cpp conflicts there, and the guard belongs on that block's first line, where it covers both.

#1395 (DB_RENDER_OVERRIDE depth copies) decodes other bits, so the two decodes don't overlap. If both merge, a draw with both takes the expand, and #1395's copy could write through this PR's plane views and pending notes so that its result reaches guest memory the same way.

#6682 (publish unit-shadow results before an address-based build maps its heaps) also adds a line at the end of UploadFinish in GuestBufferMemory.cpp. Whichever merges second keeps both, with this PR's StoreDepthExpandWrites first.

A draw with DB_RENDER_CONTROL compress-disable bits and DB_Z_WRITE_BASE
!= DB_Z_READ_BASE is a DB expand: it writes the read surface's depth
plane, uncompressed and in the same swizzle, at the write base. It was
drawn without a depth target, so the write base kept what the CPU wrote
there. Resident Evil Village (PPSA01556) lights, fogs and blurs its
first 3D scene from that copy, and zero depth is the far plane in its
reverse Z.

DecodeDepthExpandPass recognises the expand of a single-sample
Z_32_FLOAT SW_64KB_Z_X surface with no tests and no color writes over
the whole surface, and the draw precheck runs it instead of the draw.
The depth plane of the live depth surface at the read base is copied
into the storage image of an R32F SW_64KB_Z_X view of the write base,
which the next flush of that range stores to guest memory. Every other
out-of-place expand throws; an in-place expand still changes nothing
modeled, and a read-only plane is not moved.
Draw a depth gradient into a 160x96 Z_32_FLOAT SW_64KB_Z_X surface with
a stencil plane, then submit Resident Evil Village's (PPSA01556) DB
expand: DB_RENDER_CONTROL 0x60, no depth or stencil tests, no color
writes and DB_Z_WRITE_BASE at another address. The write base must then
hold the read surface's depth, both sampled as an R32F SW_64KB_Z_X
texture and read raw after a flush. The expand runs with one raster
sample and with the game's two.

On main the expand draws nothing, so the write base keeps the zeros the
CPU wrote.
Resident Evil Village (PPSA01556) issues its out-of-place DB expand as
DRAW_INDIRECT or DRAW_INDEX_INDIRECT, at boot and four times a frame at
first control, and the draw precheck threw "indirect DB expands are
unsupported" about 18 s into boot.

The CP takes an indirect draw's counts from its records, so the precheck
now reads them as the draw path's CPU route does: the queue's deferred
labels are recorded first, the count (for the count-address forms) and
the records go through the flush hook, which waits for the recorded
work that writes them, and the expand runs when a record has a nonzero
vertex or index count and instance count. Otherwise it writes nothing.
The pass covers the surface whatever the records hold, as for a direct
expand, whose geometry is not checked either; every refusal of the
expand's decode stays.
During Resident Evil Village's (PPSA01556) boot the DB expand comes
before any depth surface exists at its read base, and the expand threw
"without a depth surface at the read base is not implemented".

The DB writes what each tile of the read surface holds. Without a live
depth surface there, a pending HTILE depth clear of the surface (the
HTILE kernel NoteHtileDepthClear records before the surface exists) makes
every tile read as DB_DEPTH_CLEAR, so the write base's storage image is
cleared to it. Otherwise the tiles are taken as expanded, whose bytes the
DB writes out as they are: the storage image of the read base's R32F
SW_64KB_Z_X view, which holds its guest bytes, is copied into the write
base's, same size and swizzle. The pending clear is only looked at, not
taken: the expand changes nothing at the read base.

The HTILE words in guest memory are not read, so an HTILE the CPU or a
DMA fill cleared before any surface existed is taken as expanded. The
expand still throws into a live depth surface and from a surface of
another extent, format or sample count.
Resident Evil Village (PPSA01556) reads the expand's write base right
after it with a compute shader, raw dwords through a V#, and the guest
memory there was all zero: the expanded plane was left pending in the
write base's storage image until the next flush of that range.

A program that is address-based (a flat or global access, or a V# the
resource tracker cannot follow) maps the registered heaps whole, and its
build reads the imported ones in place without storing the results
pending in them. With the cached address space it does not even reach
the per-region flush that APS5_BDA_FLUSH=1 enables. A buffer read of the
write base by such a program saw the CPU's bytes.

The expand now stores the written image at once when the write base is
host-imported. The store is recorded into the import, without a wait,
ahead of anything recorded later. A write base that is not imported
keeps the plane pending: nothing binds such memory in place, and the
copy and mirror paths store pending results before they read.
Extend agc_driver_depth_expand_out_of_place with the variants of
Resident Evil Village's (PPSA01556) DB expand that its boot reaches:

- The expand issued as DRAW_INDIRECT and as DRAW_INDEX_INDIRECT, its
  record in guest memory behind SET_BASE, must write the read surface's
  depth at the write base like the direct expand. An empty record (no
  vertices, then no instances) must leave the write base as the CPU
  wrote it.
- An expand before any depth surface exists at the read base must write
  DB_DEPTH_CLEAR when the title cleared the surface's HTILE (RADV's
  HTILE kernel), and the read base's guest bytes, byte for byte, when
  the HTILE says expanded.
- A compute copy of the write base through a V#, raw dwords, right
  after the expand, as the title's copy shader reads it, must hold the
  depth detiled with the SW_64KB_Z_X equation, both from a plain program
  and from an address-based one (a global load added).

A case name on the command line runs that case alone.

At 305166f the indirect cases throw "indirect DB expands are
unsupported", the surfaceless ones "without a depth surface at the read
base is not implemented", and the address-based copy reads 0 in all
15360 texels. The plain copy already passes.
An always-pass depth draw with equal bases, with or without
compress-disable bits, a draw without depth or stencil planes and a
read-only stencil plane are not taken for an expand. A surface without a
stencil plane or without HTILE is expanded depth-only; a stencil plane
moving with the depth in place, Z_16 and Z_24 with and without stencil,
and the SW_64KB_Z, SW_4KB_Z_X, SW_64KB_R_X and SW_VAR_Z_X modes throw.
agc_driver_depth_expand_review expands a surface without a stencil plane,
keeps a drawn stencil plane and the read base, stencil and HTILE guest
bytes unchanged, leaves an in-place expand and an always-pass depth draw
with equal bases to the normal draw path, expands again after the read
surface changed, reads the write base through GPU buffer loads right
after the expand, and expands one surface to two write bases in one
submission.
Each out-of-place DB expand copied the whole depth plane twice (depth
image to a staging buffer, then into the write base's R32F image, 2 x
15.8 MiB at Resident Evil Village's 1920x2160) behind three global
barriers, and then stored it, even when nothing had changed since the
previous expand into the same write base. Village expands four times a
frame at first control.

The expand now remembers, per write base, the live depth surface it
copied with that surface's write serial, and the write base's storage
image with its content version after the write. When all four are
unchanged it records nothing. Draws that bind the surface, fast clears
and stores of the surface's storage views bump the serial; uploads of
the image (after a CPU write to the write base) and writes to it bump
the version. All of them precede the expand in queue order, so the
image still holds what the copy would write. Expands without a depth
surface at the read base always write.

DepthExpandCounts() reports the expands run and skipped. A new
repeated-expand case of agc_driver_depth_expand_out_of_place makes seven
expands into one write base. It requires three copies: the first, one
after a second depth draw and one after a CPU write to the write base.
Sampled, raw and address-based reads of the write base in between must
not force a copy. With the skip disabled, all seven copy.
166fa21 stored every expand's plane into a host-imported write base at
once, because an address-based build reads the imported heaps in place
without storing the results pending in them. That write-back is a
retile of the whole plane into guest memory, 15.9 MiB at Resident Evil
Village's 1920x2160: on lavapipe it took each expand from about 3 ms to
35-50 ms, whether or not anything read the plane as a buffer.

The plane now stays pending, as it did before 166fa21, and the expand
notes its write base. Before an address-based build binds the imported
heaps, GuestBufferMemory::UploadFinish calls StoreDepthExpandWrites,
which stores the noted planes still pending in host-imported write bases
and forgets the notes. The store is recorded into the import ahead of
the build's work, so the program reads the expanded plane. Every other
reader stores the plane before it reads, as for any written storage
image: an in-place bind or GPU copy of the range, a mirror refresh, and
the CPU through the flush hook. Several expands before the next
address-based build cost one store, and none if no such build follows.

DepthExpandCounts() also reports these stores. The buffer-read cases of
agc_driver_depth_expand_out_of_place now require none at the expand,
one ahead of the address-based copy, and none for the plain copy, which
stores the range it binds itself.
A depth view with DB_DEPTH_VIEW.STENCIL_READ_ONLY on an out-of-place
expand of the depth plane threw "of a depth view with a base slice, a
mip level or a read-only plane". The stencil plane of such a view
already counts as not moving, and the expand copies only the depth
plane, so the read-only bit changes nothing it does.

The refusal now covers the rest of the view, a base slice or a mip
level, and says so. A read-only depth plane never reaches it: such a
plane does not move, so the draw is no expand of it.

agc_driver_graphics takes the game's expand with a read-only stencil
view, and refuses it with a mip level as well.
…only

The refusal of an expand into a live depth surface used DepthSurfaceAt,
which scans the depth surfaces of every Vulkan device, while the read
surface is looked up for the expand's own device. A surface of another
device at the write base refused a valid expand.

It now asks planeSurface for the expand's device, as the read side
does, under the surfaces' lock.
PM-019's review of the out-of-place DB expand found four limits the debt
entry did not state:

- the whole plane is written whenever the viewport and scissor cover the
  surface, so a partial-coverage expand would overwrite texels the
  console leaves alone;
- the expand's pixel shader never runs, so any memory it writes is lost;
- a 32-bit integer (depth-bits) view of the write base still throws, by
  an older rule;
- a depth surface later bound at the write base starts from its own
  image, and the next expand into it throws "into a live depth surface".

The entry also gives the cost of an expand's first use of a write base,
which uploads guest bytes the copy then overwrites (35-47 ms at
1920x2160 on lavapipe, against about 3 ms later). It also states that a
read-only stencil view is taken, and names agc_driver_depth_expand_review
among the tests.
…face.cpp

Other open changes add includes at the top of DepthSurface.cpp and lines at the end of ClearDepthSurfaces. The expand's two includes now follow TextureTiling.hpp, and its two teardown erases run right after the surfaces are erased.
The expand tests compared every texel of the drawn depth gradient with its
expected value within 4e-6. On an NVIDIA GeForce RTX 3070 Laptop GPU the
review test's second gradient, offset by 0.5, came back 4.05e-6 and 4.11e-6
low at its two far-corner texels (68 and 69 ulps at 0.875), so the test
failed there while lavapipe matched exactly.

Both tests now accept an error of half the gradient's step in x
(0.5 / 65536). A read of the wrong texel is still one full step off.
…port serves

Resident Evil Village fills its host import budget, so the DB expand's write base may be served to address-based builds by a heap mirror or a per-build copy instead of an import. Two cases of agc_driver_depth_expand_out_of_place refuse every host import (SetHostImportBudget(1)), copy the write base once with an address-based program before the expand, so the address space and its mirror already exist, and copy it again right after the expand. In write-watched memory the build serves the write base through a heap mirror; outside the write watch it copies it. Both copies must hold the read surface's depth.

Both cases pass at e1a94e7: the mirror refresh and the copy store the pending plane through the flush hook before they read it.
StoreDepthExpandWrites stores the expanded planes still pending ahead
of an address-based build, whose build reads the imported heaps in
place without storing the results pending in them. It stored a plane
only when one host import held the whole plane.

Imports are made per registered range: adjacent ranges share one import
only when they fill an 8 MiB aligned window, or once a buffer bound
across them joins them. A write base across the edge of two registered
ranges imported apart, or of an imported range and one that is
heap-mirrored or copied, was therefore not stored. An address-based
build then read the imported part in place, so copies of the write
base right after the expand held the guest bytes: zeros in the tests,
the symptom of Resident Evil Village's copy shader at its write base.

The plane is now stored when any import overlaps it
(HostImportOverlaps). The storage image's write-back records it into
each range's import when every registered range under it has one, and
goes through the CPU, after a wait for the expand, only when part of
it has no import. A plane one import holds is still recorded into that
import, and a plane no import touches still stays pending for the
mirror refresh or the copy that reads it, so both cost what they did.
Four cases of agc_driver_depth_expand_out_of_place register the write
base's memory as two adjacent 16 MiB ranges, with the plane across
their edge, and copy the write base with address-based programs right
after the expand:

- both ranges imported, one buffer copy inside each range;
- both ranges imported, one copy of the whole plane through global
  loads, with no V# for the write base;
- the first range imported and the second heap-mirrored, or copied (an
  import budget that admits the first range only), one buffer copy
  inside each range.

Each copy must hold the read surface's depth, and the plane must be
stored once ahead of the copies. A single buffer copy across the edge
is not among them: binding it joins the two ranges into one import and
stores the plane it overlaps.

At 7283aa6 no plane is stored ahead of the copies. With both ranges
imported the copies hold 0 of 15360 texels; with the second range
mirrored or copied they hold 3072, the second range's 64 KiB block,
which its mirror refresh or copy stores.



Each split case first runs an address-based copy with no import budget, so
the ranges that earlier cases left registered are imported before the case
sets its budget. Without it, on Windows a range left by the heap-mirrored
case was imported inside the copied case, took the budget, and kept the
case's first range from being imported.
…ad-only V#s

An address-based build binds the registered heaps it maps whole in place
and does not store the storage-image results pending in them. A read-only
V# inside such a heap was merged into the heap's region on the per-build
path, or dropped inside a base region of the cached address space, so
nothing stored the results pending under it and the GPU read the import's
older bytes. Written V#s were already stored through their write ranges.

UploadFinish now stores the results pending under every read-only V# of
an address-based build, as it does for written ones. Pointer reads keep
the exemption; the out-of-place DB expand still stores its planes ahead of
every such build for them. The debt text says both.
The address-based-storage-image-read case stores a value to every texel
of an image with one compute dispatch, then copies the image's bytes with
the address-based copy kernel through a read-only V#. It does so twice
with different values: the first copy takes the per-build path, the
second goes through the cached address space. Each copy must hold what
was stored; before the fix both held 0 of 15360 texels.
… pending plane

The out-of-place expand's debt entry said that every reader of the write
base stores the pending plane first. The driver does not see a guest CPU
read after a label, which reads the bytes from before the expand, as the
pending storage-image results entry already says.
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 15, 2026
Since 8fec47a (boykopovar#5111), decodeDepth places a slice's HTILE one slice
of pipe-aligned metadata per slice after DB_HTILE_DATA_BASE, as addrlib
addresses it. The entry still said that every slice shares slice 0's
HTILE and that the per-slice stride is not derived.
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 16, 2026
@oneandonlydean oneandonlydean self-assigned this Oct 17, 2026
EagleBearerYS added a commit to EagleBearerYS/AnyPS5 that referenced this pull request Oct 17, 2026
@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 17, 2026
Main adds the depth and stencil plane fill entry to the debt file next
to the HTILE array slice entry this branch drops, which main's
decodeDepth no longer needs. Keep main's new entry.
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 17, 2026
@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 18, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agc libSceAgc* build docs needs rebase Conflicts with main, rebase needed unblocks title Names a title ID on the Unblocks line

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants