Repository navigation
Conversation
A title's transient allocator (UE's RDG transient aliasing) gives the memory of a storage image A that still has results pending to a surface B of another format and shape. Refresh of B stored A's results first (a retile and a store through guest memory, PCIe on host-imported memory), then uploaded B from that memory, at every such hand-over; on Sword of the Sea most of the frame's storage write-backs and refresh uploads were these round trips. Where B's content does not come from guest memory, A's results now stay pending: - B's DCC keys are a clear code its format can hold: the upload is a GPU clear; - B's keys are uncompressed over host-imported memory: B takes the units from A on the device (A's results retiled into a device buffer, detiled into B), the bytes A's store would write. Only 64 KiB block units that lie wholly inside B, that B's write-back stores whole, that B holds no results of its own in and, on the device, that A's element covers whole and nothing stamped since A's generation. They are stored as before when anything reads the memory first (the flush hook, another image's refresh; a key change after the flush stores them first), dropped once B's own results cover the block (markLayersPending: B's write-back then overwrites every byte they would have stored), and a late store of them advances B's units past its stamps while A still holds the results it handed over. B's refresh proof is not broken by those units alone. With APS5_PROFILE_DRAW a "[storage] hand-over" line counts the units every 10 s. Test: agc_driver_alias_handover (B under clear keys and from memory; A never read again, read before B wrote, written again after the hand-over; unwatched and watched memory): the same guest bytes and the same texels of B with and without the hand-over (SetAliasHandOverForTests), and the expected units handed over, superseded and stored later. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013fa2MbmdFLVr8YHuVjucHE
|
Other open pull requests touch the same code. Merging one will break the other, or both implement the same thing. If this one builds on them, list them in Depends on.
|
|
Preparing a Windows native cross-test of your fd0bfc6 change, with your author retained. It applies cleanly in our newer-main native-test checkout: https://github.com/Scramblrrrr/AnyPS5/tree/codex/alias-hand-over-native-cross-test , head 83550be (parent 2da9b8f, containing our warm host-hole test). Not built, tested or staged yet. I will run both alias hand-over fixtures plus nearby storage/recorder checks after the active 007 run. The actual game still uses our older runtime-image-table integration, so this native branch is not a title-running stack. Reviewing #3813 and the full final #3777 chain as prerequisites for updating that integration; no competing implementation. AI-assisted: Codex. No title data uploaded. |
|
@Scramblrrrr thanks for cross-testing. A note so the numbers line up: the gain depends on the import mode, see the Configurations section I added to the description. With Windows' default compare-mode imports the change was neutral on our title (301/285 vs 292/292 ms). With imports kept write-watched it was 495-517 → 355-362 ms. So if your title stack runs default imports, expect the fixtures to pass and little frame-time change; a watched-import config, or Linux where the probe leaves imports watched, is where the hand-over shows. AI-assisted: yes (Claude Code) |
|
Source-only follow-up prepared on the actual 007 integration: https://github.com/Scramblrrrr/AnyPS5/tree/codex/first-light-alias-hand-over-cross-test , head 97737cb (full hash available on branch). Starting from the tested/running clip-cull head 188d801, this carries the complete merged #3777 chain (including global-idle scoping and waking queue zero), the complete merged #3813 lifetime/tests chain, then #4286 fd0bfc6. All original authors retained; the carries applied cleanly through three-way integration. No local depth-reuse bypass added. Not built, tested or staged yet. The current game remains graphics 188d801 with the fresh clip cache. Native storage lifetime, idle-writeback, alias hand-over and nearby recorder checks are next after this run. The separate newer-main native branch 83550be also remains prepared, not tested. No FPS claim for this storage stack. AI-assisted: Codex. No title data uploaded. |
|
Windows native cross-test on the actual 007 integration passed 13/13 selected tests in 9.38 s, no skips, on RTX 5070 Ti / driver 617.14. Head 97737cb, branch https://github.com/Scramblrrrr/AnyPS5/tree/codex/first-light-alias-hand-over-cross-test . Carries #4286 fd0bfc6 after the complete #3777 and #3813 chains, on our tested clip/cull integration. Original authors retained. Both alias_handover and alias_handover_memory passed, as did storage_upload_lifetime, storage_upload_failure, store_idle_results, graphics, BDA, GPU buffer formats, draw-cache reuse with GPL on/off and all three clip/cull modes. This is the retail integration branch; the separate newer-main branch 83550be remains untested. Not staged into 007 yet and no 007 FPS claim. The previous game run reached the rendered menu and then hit existing #2975 during campaign loading, so I am adding the already merged #3523 before the next run. That source-only follow-up is 903d503 and is building now. AI-assisted: Codex. No title data uploaded. |
|
Pre-review found a deterministic test failure on Linux/RADV — flagging early since full CI doesn't run automatically on PRs. Independent pre-review (CachyOS, kernel 7.2.9, GCC 16.2.1, Ninja, Release,
The failing assertion is the Everything else in the suite passes (922/923 — only this test). Since the assertion is GPU-result visibility, this may be driver/timing-sensitive (RADV here) — if it's green on your setup, it's worth a look at whether the read-back path has a race the RADV timing exposes. Reproduce with: Happy to provide the full test output or run any variant that would help narrow it down. |
|
@blawesom thanks, that's exactly the kind of check this needed. It's notable that the failing arm is Off (hand-over disabled, i.e. the old order), in watched memory, after A was read before B wrote. Together with the AI-assisted: yes (Claude Code) |
… imports The watched arm forced host imports to stay write-watched. Linux compares the imports of a device whose own reads and writes of an import resolve the write protection (the import probe), and there the forced watch reads the device's accesses as CPU stores: RADV imports host memory as a writable amdgpu userptr, which every command submission faults in writable again, so each collect after a submission stamps the whole import; lavapipe writes the import with the CPU. Results under those stamps give way to the CPU's bytes, so the Off order lost A's results on RADV (A read before B wrote, then A read again) and B's on lavapipe, in a configuration main never runs on these devices. On Linux the watched arm now follows the probe and reports "host imports are compared on this device" where it compares. Windows keeps the forced watch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013fa2MbmdFLVr8YHuVjucHE
|
@blawesom thanks, the 5/5 made this easy to pin down. The hand-over isn't involved, and main is fine in the configuration it actually runs on your card: the test's watched arm forced a configuration main never uses on RADV. Fixed in 0a82d7b. Cause. The watched arm called On amdgpu they do. RADV imports a host pointer as a userptr BO without the read-only flag (libdrm's In "A read before B wrote, Off", A's refresh sees CPU stores over the units it still holds pending (the half B doesn't cover). The drop rule gives those blocks to the CPU, which is your Fix. On Linux the watched arm now follows the probe. Where imports are compared, it prints Verified.
Could you rerun AI-assisted: yes (Claude Code) |
|
Reran on RADV with 0a82d7b as requested — the fix works here. The probe line on GFX1201:
Full suite: 923/923 passed (was 922/923 — the only failure is gone). Conventions still pass. For the record on the original failure mode: the machine reports |
|
@blawesom thanks for the rerun. The probe numbers match the mechanism: 4 of 4 pages are reported written after a GPU read, so RADV compares its imports, and the watched arm correctly reports "not tested" there. From our side the PR is ready: Windows/NVIDIA, Linux/lavapipe (fork CI) and now RADV GFX1201 all pass. AI-assisted: yes (Claude Code) |
|
Two things before this can be tested:
|
|
@oneandonlydean fair points, and the second one decides it. In the configurations
So +930 lines in AI-assisted: yes (Claude Code) |
Cherry-picked from fd0bfc6 (boykopovar#4286) and ported to current main: the watched-memory and hand-over paths drop the layers' saved originals through layerOriginal and cancel stored saves as main does, and the WatchedImportsStampPages condition main removed is gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> # Conflicts: # core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
…he FPS candidate Brings in ufc6/feedback (origin/main 760ffa6, boykopovar#6197, boykopovar#4286, boykopovar#5955) on top of the four FPS topics and boykopovar#6168. Conflict resolutions: - Execution Draw.cpp: keep the binding plan's single Regions() collection after the stage loop, adding the regions of reused or data-hit stages, and upstream's data-hit counters and cacheDrawStages signature. - Graphics Draw.cpp: recordDrawCommands takes both upstream's firstIndex and the dirty-record bound state; a stencil replay, like a face or channel split, drops the bound state. - The binding plan falls back to the full capture for stages whose plan uses image tables (ShaderMemory::UsesImageTables). Unbuilt and untested. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd supersede handed-over units before saving HostImportSpanning (boykopovar#6197) joined registered ranges whose boundary falls inside a 64 KiB unit, so with unit shadows such surfaces took the GPU-direct path that main refuses for them (agc_storage_split_writeback, agc_driver_storage_write_back_overhang). The whole-surface import now goes through surfaceImport, which keeps the per-window import, refuses a join across a unit-misaligned boundary under unit shadows and joins otherwise, so 64 KiB-aligned pages keep the spanning import. The boykopovar#4286 port superseded handed-over units after main's capture of unsaved layers, whose flush stored the alias's dropped results first (agc_driver_alias_handover); the supersede now runs before it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ersede removes The boykopovar#4286 port turned originalValid = false into clearing every layer's saved original of the source image, but layerOriginal now also holds the guest bytes a write-back outside the write watch needs. Once the supersede ran before the unsaved capture, the source's remaining pending units lost their saved bytes and its next write-back threw (agc_driver_alias_handover, agc_driver_alias_handover_memory). Only the dropped units now lose their saved bytes and stored saves. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sword of the Sea (PPSA25344) menu, with host imports write-watched: frame time 369 → 326 ms, GPU 128 → 104 ms per frame (and 495-517 → 355-362 ms with the unit shadow on). With Windows' default compare-mode imports the change is neutral; see Configurations below.
What
Unreal's render graph aliases transient resources: the memory of a storage image A that still has results pending is handed to a surface B of another format and shape in the same frame.
StorageTexture::Refreshof B stored A's results first and then uploaded B from that memory. That is a retile and a store through guest memory (PCIe on host-imported memory), then a detile back, at every hand-over. On Sword of the Sea most of the frame's storage write-backs and refresh uploads were these round trips, and the refresh waited for them.Where B's content does not come from guest memory, A's results now stay pending:
Only 64 KiB block units qualify, and only when:
What happens to them afterwards:
markLayersPending), they are dropped. B's write-back then overwrites every byte they would have stored.With
APS5_PROFILE_DRAW, a[storage] hand-overline counts the units every 10 s. There is no switch: the old order is kept wherever B's content comes from guest memory.Measured
Sword of the Sea menu, Windows 11, RTX 5070 Laptop (AMD iGPU hidden). Two builds of
maina11c8f7 plus the lab's local stack, one with and one without this commit; the libraries are otherwise the same. Rows of 180 s, alternating, with the figures per 10 s:Per 10 s with the change: 85.6 k units left pending under a refreshed surface (48.9 k of them taken on the device), 64.8 k superseded by the surface's own results, 3.7 k stored later, no fallbacks.
The picture at 1:1 does not change. The menu's dark specks grow at the same rate per frame with and without the change: 0.027 % per frame without, 0.026 % with. Interpolated to the same frame, the dark share of the sky band is 12.2 % and 12.1 % at frame 431, and 16.2 % and 16.0 % at frame 589. The top luma is 74 → 72 without the change and 75 → 71 with it. The specks are the depth fast clear that #3816 addresses, unrelated to this change.
Before this shape, an experimental build with a verify mode stored everything as before and checked every hand-over decision against what a later reader of the memory saw. It reported 0 mismatches in 4.26 M checked blocks over two 180 s runs.
Configurations
The rows above ran with host imports kept write-watched (
APS5_WRITE_WATCH_IMPORTS=watchwith a local change that lets it apply to Windows direct imports) andAPS5_NO_UNIT_SHADOW=1. The hand-over removes write-backs and refresh uploads that watched imports trigger. Measured again onmaind146ec1 vsmain+ this change, 180 s rows, alternating:mainframe msSo on Windows with default settings this change neither helps nor hurts this title; the gain applies where imports stay watched, which on Linux is the case when the import probe finds that the driver does not resolve write protection. A Linux measurement would show which applies there.
Tested
Windows 11 (26200), WinLibs GCC 15.2.0, Release, rebased onto
maind146ec1.agc_driver_alias_handover_memoryfails onmainwiththe refresh of a surface under clear keys stored the pending results of the image it aliases over its memory, and passes with this change. A 256x256 RGBA8 image A has results pending; a 128x128 RGBA16F surface B under 0000 keys over A's first two units is refreshed. Afterwards guest memory still holds the old bytes there and A stays pending. B's own results then supersede A's two units, and guest memory ends with B's results over B and A's elsewhere.agc_driver_alias_handovercovers B under clear keys and B from memory (taken on the device), each with three orders: A never read again, read before B wrote, and written again after the hand-over. Each order runs in unwatched and watched memory, with and without the hand-over (SetAliasHandOverForTests). The guest bytes and B's texels are the same both ways, and the hand-over counts are the expected ones: units handed over, superseded and stored later.ctest -R "^agc_": 424/424 pass, including bothagc_driver_alias_handovertests. Before the rebase, onmainbab7b32: 420/422 (agc_write_trackingandagc_driver_prepared_shaders_registered_workgroup_syncfailed the same way without this change).ctest -E "^agc_driver|^video_out|^guest_video", on d146ec1 plus this change): 582/583 pass.log_macrosfails there ("a short line differs"); the change does not touch it.tools/check_conventions.py --base origin/mainpasses.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