Skip to content

feat(libSceAgcDriver): import scattered direct memory as a dma-buf run list and probe its coherence - #4449

Open
Mouad-Scriptz-0 wants to merge 2 commits into
boykopovar:mainfrom
Mouad-Scriptz-0:fix/dmabuf-import-coherence
Open

Mouad-Scriptz-0 wants to merge 2 commits into
boykopovar:mainfrom
Mouad-Scriptz-0:fix/dmabuf-import-coherence

Conversation

@Mouad-Scriptz-0

@Mouad-Scriptz-0 Mouad-Scriptz-0 commented Oct 12, 2026 •

Copy link
Copy Markdown

What

Linux only. The dma-buf fallback of host imports (createImport, when the host-pointer import fails) had two problems, found while chasing Minecraft's untracked textures. This is @blawesom's port of the original branch onto main's sharedBacking import structure (port/4449, authorship kept), plus the fixes from the review of 10-17.

  • Scattered backing. A guest range mapped from several physical allocations is several runs of the direct memory backing file (a 4 MiB Minecraft atlas spans 53), and the fallback took one offset for the whole range. GuestArenaSharedBackingRuns (libkernel, registered by DirectMemory) resolves a range into its runs in address order, up to udmabuf's 1024; one run keeps main's chunked and sparse paths, several runs create one udmabuf through UDMABUF_CREATE_LIST. The single resolver stays as the fallback for registrations that do not provide runs. guest_memory gains CheckDirectMemorySharedBackingRuns: three physical allocations mapped back to back resolve to their runs with the right offsets and total, a sub-range starts inside the first run, and a short run array, a range past the mapping or an empty range are refused.
  • Coherence. i915 maps a dma-buf without snooping the CPU caches: after the CPU stores to the pages and the GPU fills the import, the CPU keeps reading its stale lines. The start-up probe now stores to every page from the CPU, fills from the GPU and counts the pages the CPU still reads stale (staleAfterFill, in the [write-watch] line); a device measured incoherent does not use dma-buf imports and copies instead, which rules them out on Intel. Without a write watch the probe still runs, since that measurement is a plain CPU read after a GPU fill; it registers and collects the watch only where one exists, and the no-watch line says when nothing was measured. The mixed-watch import path (watchedDmaBuf != watchedHostPointer) also requires the coherence result.

Tested

Fedora 44 (kernel 7.2.11, GCC 16.2.1, Ninja, Release, BUILD_TESTING=ON, Intel Arc Meteor Lake, Mesa 26.2), branch on main 9f25d0b: guest_memory (with the new case), agc_dma_data_tests, agc_write_watch_coverage, agc_host_import_unmap, agc_driver_host_import_at_map and agc_driver_host_import_teardown pass. The probe on this GPU reports 4 stale after a GPU store over CPU writes for dma-buf imports, so the fallback stays off here. tools/check_conventions.py --base origin/main and git diff --check pass. Not tested: a device without a write watch (the branch the review found unmeasured), AMD or NVIDIA, Windows (no dma-buf path), and no title was run on this head; @alfalfa-star offered the Kena scene (#4449, 10-17) on the port.

Checklist

  • Based on current main; no other open PR implements the same functions
  • One topic per PR; follow-ups go in a new PR
  • No comments in code except technical debt
  • Unimplemented paths throw (NotImplemented_nid_no_patch); silent stubs are listed in TechnicalDebt
  • No notes, investigation .md files or images added to the repository (attach them to this PR)
  • New third-party code is a submodule built from source
  • Generic behaviour, not specific to one title
  • Depends on: none
  • AI-assisted: yes (Claude Code)

@Mouad-Scriptz-0
Mouad-Scriptz-0 force-pushed the fix/dmabuf-import-coherence branch from 2dbf7b7 to 371a549 Compare October 12, 2026 13:42
@github-actions github-actions Bot added needs rebase Conflicts with main, rebase needed labels Oct 12, 2026
@oneandonlydean oneandonlydean self-assigned this Oct 12, 2026
@oneandonlydean

Copy link
Copy Markdown
Collaborator

Which device takes the run-list path once the coherence probe is in? On i915 the probe turns the dma-buf fallback off, and SharedBacking on main already refuses a range whose mappings are not contiguous in the backing file (DirectMemory.cpp:496), so nothing is imported with a wrong offset today. Please keep the probe here and leave SharedBackingRuns for a PR with a device that needs it.

dmaBufImportsCoherent also stays false on the early returns of decideImportWatch (APS5_WRITE_WATCH_IMPORTS=watch or unwatch, no write watch), which turns the fallback off there without a probe.

It needs a rebase as well: docs/dev/TechnicalDebt.md was split, so the entry goes under docs/dev/debt/.

@johnmaia

Copy link
Copy Markdown
Contributor

Test report: AMD / RADV, where the run-list path is needed (re @oneandonlydean's question).

Setup

  • Linux 7.0, RX 7600 (RADV, Mesa 25.2.8).
  • Title: Epic Mickey: Rebrushed (PPSA15657), Dark Beauty Castle level.
  • Two builds of the same base: my Mickey test bed (main e7c70a2 plus Mickey fixes, including fix(agc): store a storage image whose last block row runs past its allocation #5030). B adds this PR, which merged cleanly onto that base.
  • Each build ran a 5-minute scripted play session with APS5_PROFILE_DRAW=1. Numbers are averaged from 30 to 300 s after loading the save.

Why AMD takes this path: host-pointer imports of this title's direct memory fail at vkAllocateMemory (-13). The guest ranges are stitched from many memfd mappings at non-contiguous file offsets. For example, 0x2010000000–0x2016000000 is 11 separate VMAs. amdgpu's userptr import can't span VMAs, and SharedBacking on main refuses non-contiguous runs. So these ranges are copied on the CPU instead.

A (base) B (+#4449)
In-level FPS 4.57 4.93 (+8%)
Buffer regions copied on the CPU, MiB per 10 s ([buffers] outside imports) 231,027 64,643 (−72%)
host import ... failed lines 40 0
GPU busy / idle gaps per present 106.1 / 108.7 ms 107.7 / 91.5 ms

Coherence probe on RADV: 0 stale after a GPU store over CPU writes for both host-pointer and dma-buf imports, so the fallback stays enabled here:

[write-watch] dma-buf imports keep write protection: yes (0 of 4 scratch pages written after a GPU read, 0 after a GPU write, 0 after the import, 1 seen after a CPU store, 0 stale after a GPU store over CPU writes); imported ranges stay watched

Checks

  • No visual change; the level renders as before.
  • No new errors in the log.
  • Both sessions survived 5 minutes of play.
  • I also merged this PR onto current main, resolving the GuestBufferMemory.cpp conflict against the new createBackingImport structure for a local build: full ctest 1033/1033 passed. That build doesn't boot the title for an unrelated reason, so the in-game numbers above are from the older base.

🤖 Generated with Claude Code

@darklinkworker

Copy link
Copy Markdown

A data point from a local port of this branch onto current main. Linux, RADV Phoenix (Radeon 760M), Dragon Quest III HD-2D (PPSA20612).

On this host the write-watch probe does not run: there is no [write-watch] line, and GuestMemory::WriteWatched() is false. decideImportWatch returns on that path before it can set dmaBufImportsCoherent, so the flag stays at its default and the dma-buf fallback is skipped. The log then fills with host import ... falling back to copies. In the opening scene the character was missing, at about 1.5 FPS.

For a local test only, I set dmaBufImportsCoherent true on that early return, with no probe result. The copy lines dropped to zero, and the character was visible in the opening scene. I do not have a staleAfterFill reading for this GPU, so I cannot say those imports are coherent, and I am not suggesting the fallback be forced on. It is the same early-return gap already noted on this pull request: without a write watch, the fallback is disabled and no probe runs. Running the probe on that path, as discussed, is what would let this machine decide.

This was a local build, not a rebase of the branch. A later save-load screen can still stop presenting here; I don't think that belongs on this pull request.

@oneandonlydean

Copy link
Copy Markdown
Collaborator

That is the early-return gap from 10-14: without a write watch decideImportWatch returns before the probe, dmaBufImportsCoherent keeps its default and the dma-buf fallback is skipped, which is the copy lines and the 1.5 FPS. Setting it true is a measurement, not a fix, as you say. @Mouad-Scriptz-0, the probe has to run on that path, and since main now has the sharedBacking version of the same import this needs a port, not a rebase.

blawesom added a commit to blawesom/AnyPS5 that referenced this pull request Oct 17, 2026
…n list and probe its coherence

Ports boykopovar#4449 onto main's sharedBacking import structure, per the
maintainer direction that this needs a port rather than a rebase:

- GuestArenaSharedBackingRuns resolves a guest range into the runs of
  its direct memory backing (one per physical allocation, up to
  udmabuf's 1024), registered by DirectMemory; a single run still
  takes main's chunked and sparse paths, several runs create one
  udmabuf buffer per run through UDMABUF_CREATE_LIST
- the start-up probe stores to the import from the GPU after CPU
  stores to the same pages and counts pages the CPU still reads stale
  (i915 maps dma-buf without snooping CPU caches); an incoherent
  measurement keeps the fallback off
- decideImportWatch no longer returns early when there is no write
  watch: the coherence probe still runs there (its stale measurement
  is a plain CPU read after a GPU fill), so such devices measure
  instead of skipping the dma-buf fallback
- the single shared backing resolver stays as a fallback for
  registrations that do not provide runs

Co-authored-by: Mouad-Scriptz-0 <188722908+Mouad-Scriptz-0@users.noreply.github.com>
@blawesom

Copy link
Copy Markdown
Contributor

The port, as directed — since main now carries the sharedBacking version of this import, I ported this PR's run-list semantics onto that structure rather than rebasing, and included the early-return fix from the 10-14/10-17 discussion: decideImportWatch now runs the dma-buf coherence probe even when there is no write watch (its stale measurement is a plain CPU read after a GPU fill), so such devices measure coherence instead of skipping the fallback.

What the branch carries, on top of current main:

  • GuestArenaSharedBackingRuns resolves a guest range into the runs of its direct memory backing (one per physical allocation, up to udmabuf's 1024), registered by DirectMemory; a single run keeps main's chunked and sparse paths, several runs create one udmabuf buffer per run through UDMABUF_CREATE_LIST
  • the start-up probe measures dma-buf CPU coherence: it stores to the import from the GPU after CPU stores to the same pages and counts pages the CPU still reads stale (the i915 case in the debt entry); an incoherent measurement keeps the fallback off
  • the single shared backing resolver stays as a fallback for registrations that do not provide runs (the sparse-import tests rely on it), and the mixed-watch path (watchedDmaBuf != watchedHostPointer) is ported to the runs API
  • one reconciliation to review: dmaBufImportsCoherent defaults to true (probe-not-run = main's unconditional fallback, which the sparse tests assume) and is only turned off by an incoherent measurement — the PR's original default of false would have disabled main's sparse path before any probe ran

Verified on Linux (CachyOS, kernel 7.2.9, GCC 16.2.1, Ninja, Release, BUILD_TESTING=ON, AMD RX 9070 XT / RADV): builds; conventions pass; full suite 1335/1336 — only the known main regression (agc_driver_tessellation segfault, #4727). The new runs test (CheckDirectMemorySharedBackingRuns) and the sparse/chunked import tests all pass, and the probe's coherence measurement runs end-to-end here: 0 stale after a GPU store over CPU writes (coherent, fallback available on this device).

Branch: blawesom:port/4449 (8d3627214) — happy to hand it over as the base for this PR, or open it separately if that is easier; the design is @Mouad-Scriptz-0's, carried with co-authorship.

@oneandonlydean

Copy link
Copy Markdown
Collaborator

Thanks @blawesom, that covers both asks (the port onto sharedBacking and the probe on the no-write-watch path). We're reviewing 8d3627214 now, including the dmaBufImportsCoherent default you flagged.

@Mouad-Scriptz-0, it's your design: are you happy to take blawesom's commit as the base of this PR (it keeps your co-authorship), or would you prefer it as a separate PR? Either works for us.

@oneandonlydean

Copy link
Copy Markdown
Collaborator

Review of the port (port/4449 at f1bb5cb, same tree as 8d36272), @blawesom:

The run-list port is faithful. The kernel, libc and runs code matches #4449 line for line: one run goes through UDMABUF_CREATE plus main's chunked/sparse paths, several runs through UDMABUF_CREATE_LIST (capped at 1024), and the single resolver stays as the fallback. It merges cleanly with current main. Two things need fixing before we run CI:

1. The no-write-watch probe still never runs on Linux. decideImportWatch now calls ProbeDmaBufImportWriteProtection when !GuestMemory::WriteWatched(), but that probe still starts with if (!GuestWriteWatchAvailable_nid_postfix()) { probe.failure = "the write watch"; return probe; }. On Linux WriteWatched() is GuestWriteWatchAvailable(), so the probe always fails there, dmaBufImportsCoherent keeps its default, and nothing is measured. The early return was removed from the host probe (ProbeImportWriteProtection) but not from the dma-buf one. Your 0-stale run was on RX 9070 XT, which has a write watch, so it went down the watched branch.

  • Ask: drop that early return in ProbeDmaBufImportWriteProtection, guard its GuestWriteWatchRegister/Unregister calls the way the host probe does, and log "not measured" when failure != nullptr (it currently prints "CPU-coherent (0 of 0 pages stale)" for an unmeasured device).
  • The measurement itself is a valid coherence test without a watch (CPU store through the MAP_SHARED memfd, fenced GPU fill, plain read), so this is only about letting it run.

2. The mixed-watch path ignores the coherence result. In importRunAt, the branch if (step != nullptr && watchedDmaBuf != watchedHostPointer) calls createBackingImport with no dmaBufImportsCoherent check. On a watched i915 whose dma-buf and host watch decisions differ, it imports dma-buf right after logging "dma-buf imports are not CPU-coherent and are not used". Main added that path after #4449, so the gap is new in the port.

  • Ask: add && state.dmaBufImportsCoherent to that condition.

Smaller notes:

  • A test on the no-watch branch would have caught (1); one is worth adding if it's practical. The new CheckDirectMemorySharedBackingRuns test is good (runs, pread offset, capacity), but nothing covers the CREATE_LIST import, the probe or the mixed-path gate.
  • In that test, the capacity-1 call leaks the dup'd fd when it returns true.
  • With dmaBufImportsCoherent defaulting to true, the APS5_WRITE_WATCH_IMPORTS overrides and a dma-buf probe failure on the watched branch still leave the fallback on. That's the same as main today, so it's not blocking.

On where it lands: from the code side either way works, since it fully replaces #4449's diff. That choice is @Mouad-Scriptz-0's. It's a guest-memory change, so after the fixes it gets full CI on the exact head and our game checks.

@alfalfa-star

Copy link
Copy Markdown
Contributor

A data point for the run-list idea, from our own variant, not from this PR. In #6951 (since cut down to a run memo) I had imported windows whose direct-memory backing is scattered as one UDMABUF_CREATE_LIST. When sharedBacking failed it ran by default on Linux, much like this PR's several-runs path.

Kena: Bridge of Spirits (PPSA01802), forest title scene, no input. main f8b22ed, APS5_TRACE_SECONDS=100:30, warm cache, three rotated rounds. Linux 7.2.9, RX 9070 XT, RADV Mesa 26.2.4, Release. Presents in 30 s:

build presents in 30 s
main + a run-bounds memo 234 / 243 / 244
the same + the run-list import 344 / 323 / 314
main 191 / 227 / 208

So about +36 % over the memo alone in this scene; Kena maps its pools in many small direct-memory pieces. I haven't run this PR or the port/4449 branch. Once the port is the PR's base, I can run the same scene on it.

AI-assisted: yes (Claude Code)

@oneandonlydean

Copy link
Copy Markdown
Collaborator

Thanks, that's a useful data point: +36% over the memo alone in a scene that maps its pools in many small pieces is a good argument for the run-list path. A run on the port branch, once it's this PR's base, would be welcome, ideally with the same scene and the same three rotated rounds so the numbers compare directly.

blawesom and others added 2 commits October 18, 2026 12:07
…n list and probe its coherence

Ports boykopovar#4449 onto main's sharedBacking import structure, per the
maintainer direction that this needs a port rather than a rebase:

- GuestArenaSharedBackingRuns resolves a guest range into the runs of
  its direct memory backing (one per physical allocation, up to
  udmabuf's 1024), registered by DirectMemory; a single run still
  takes main's chunked and sparse paths, several runs create one
  udmabuf buffer per run through UDMABUF_CREATE_LIST
- the start-up probe stores to the import from the GPU after CPU
  stores to the same pages and counts pages the CPU still reads stale
  (i915 maps dma-buf without snooping CPU caches); an incoherent
  measurement keeps the fallback off
- decideImportWatch no longer returns early when there is no write
  watch: the coherence probe still runs there (its stale measurement
  is a plain CPU read after a GPU fill), so such devices measure
  instead of skipping the dma-buf fallback
- the single shared backing resolver stays as a fallback for
  registrations that do not provide runs

Co-authored-by: Mouad-Scriptz-0 <188722908+Mouad-Scriptz-0@users.noreply.github.com>
… watch and gate the mixed-watch import on it

ProbeDmaBufImportWriteProtection returned before measuring when the write watch was unavailable, so on Linux the no-write-watch branch of decideImportWatch never measured and dmaBufImportsCoherent kept its default. The probe now registers and collects the watch only where it exists, as the host probe does, and that branch logs when the probe failed instead of reporting an unmeasured device as coherent.

The mixed-watch branch of importRunAt created a dma-buf import without consulting dmaBufImportsCoherent; it now requires it.

CheckDirectMemorySharedBackingRuns closes the duplicated file of the capacity-1 call when that call succeeds.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@Mouad-Scriptz-0
Mouad-Scriptz-0 force-pushed the fix/dmabuf-import-coherence branch from 371a549 to ca1d482 Compare October 18, 2026 12:22
@Mouad-Scriptz-0 Mouad-Scriptz-0 changed the title fix(libSceAgcDriver): import scattered direct memory as a dma-buf run list and probe its coherence feat(libSceAgcDriver): import scattered direct memory as a dma-buf run list and probe its coherence Oct 18, 2026
@Mouad-Scriptz-0

Copy link
Copy Markdown
Author

@blawesom's port/4449 is now this PR's head, rebased onto main 9f25d0bf5 (249ef873b, authorship kept; the only conflict was the debt file, both entries stay), with the review's fixes on top in ca1d48211:

  • ProbeDmaBufImportWriteProtection no longer returns early without a write watch; it registers, collects and unregisters the watch only where one exists, as the host probe does, so the no-watch branch of decideImportWatch measures on Linux.
  • That branch logs dma-buf coherence not measured (probe failed at %s, %d) when the probe fails, instead of reporting an unmeasured device as coherent.
  • The mixed-watch branch of importRunAt requires state.dmaBufImportsCoherent.
  • CheckDirectMemorySharedBackingRuns closes the duplicated file of the capacity-1 call when it succeeds.

Fedora 44, Intel Arc Meteor Lake, Mesa 26.2: guest_memory, agc_dma_data_tests, agc_write_watch_coverage, agc_host_import_unmap, agc_driver_host_import_at_map and agc_driver_host_import_teardown pass; the probe here reports 4 of 4 pages stale after a GPU store, so the fallback stays off on this GPU. Conventions and git diff --check pass. The description is rewritten for the port. Not covered yet: a test on the no-watch branch and coverage of the CREATE_LIST import, the probe and the mixed gate; I would take those as a follow-up unless you want them here.

@alfalfa-star, this head is the port plus those fixes, if the Kena scene with the same three rotated rounds is still on offer.

@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 18, 2026
@Pacuka

Pacuka commented Oct 18, 2026

Copy link
Copy Markdown
Contributor

AMD data point (the PR was only tested on Intel, where the fallback stays off): The Smurfs - Dreams (PPSA21607, Unreal Engine), CachyOS kernel 7.2.2, Ryzen 9 5950X, RX 6650 XT / RADV Mesa 26.2.4, main 9f25d0b + this PR + #6208 + #6501 + #6617 + #6566 (+ unrelated local rendering fixes). Same spot in the Smurfs' Village, standing still, APS5_PROFILE_DRAW=1.

On RADV the host-pointer import of shared direct memory fails (vkAllocateMemory -1000072003), and the title's scattered direct memory (e.g. 0x1e41000000+0x800000, many 64 KiB runs of the backing file) was not covered by the single-offset dma-buf fallback, so those ranges fell back to copies, and small DMA stores into them took commitDmaStore's WaitIdle path. A gdb sample of the committer showed ~38% of its time in commitDmaStore → VulkanDevice::WaitIdle.

With this PR the probe reports 0 stale after a GPU store over CPU writes, so dma-buf imports stay on, and every import succeeds (12 failures → 0):

  • presents per 10 s: 80–83 → 142–145 (~8.2 → ~14.4 fps, about +75%)
  • committer: ~70 → ~38 us per commit (~125k → ~215k commits per 10 s)
  • GPU idle per frame: ~78 → ~28 ms
  • DMA stores: 36k on the GPU / 157k on the CPU (idle) → 564k on the GPU / 22k on the CPU

The image is correct. The only flicker (a ground shadow briefly getting lighter) was there before and is unchanged.

AI-assisted (Claude Code).

@Pacuka

Pacuka commented Oct 18, 2026

Copy link
Copy Markdown
Contributor

Follow-up: without APS5_PROFILE_DRAW the same build runs at ~17 fps in the Smurfs' Village (it was ~6–7 fps this morning without this PR and the other perf PRs). The ground-shadow flicker I mentioned above is gone too in this run, so it was probably a timing effect of the slow frames, not something this PR changes.

AI-assisted (Claude Code).

@alfalfa-star

Copy link
Copy Markdown
Contributor

@Mouad-Scriptz-0 @oneandonlydean Kena on this head (ca1d482) against its base main 9f25d0b. Same setup as my 10-17 numbers: forest title scene, no input, APS5_TRACE_SECONDS=100:30, warm cache, three rotated rounds of 150 s. Linux 7.2.9, RX 9070 XT, RADV Mesa 26.2.4, Release.

build presents in 30 s frame p50 (ms) GPU busy per present (ms) host import ... failed lines
main 9f25d0b 189 / 236 / 242 154 / 126 / 119 70.8 / 66.9 / 66.9 59 / 52 / 55
+ #4449 345 / 317 / 344 86 / 94 / 85 62.3 / 68.1 / 62.7 0 / 0 / 0

That is about +51% (mean 335 against 222 presents). It matches the 344 / 323 / 314 from our own run-list variant on 10-17, so the port keeps the gain. On main, the failures are host imports of Kena's 8 MiB pool windows (0x13c2800000+0x800000 failed at vkAllocateMemory (-1000072003); falling back to copies). With the PR all of them import.

The probe on this GPU (write watch present, so the watched branch) keeps the fallback on:

[write-watch] dma-buf imports keep write protection: yes (0 of 4 scratch pages written after a GPU read, 0 after a GPU write, 0 after the import, 1 seen after a CPU store, 0 stale after a GPU store over CPU writes); imported ranges stay watched

No crash, skip or GPU reset in any of the six runs. These were perf runs without frame dumps, so they say nothing about the image. Tests on the head: the 24 guest_memory, host-import, write-watch and DMA tests pass. I didn't cover the no-watch branch: this machine has a write watch.

AI-assisted: yes (Claude Code)

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants