Skip to content

perf: High on Life: one 16-byte-record buffer copy compute shader takes ~96% of GPU time near Blorto's Chef Stand #9980

Closed

Description

@Tamalero

Title and scene

PPSA15388 (01.120.000), Blim City, standing still and facing Blorto's Chef Stand (the restaurant counter with the chef and two seated customers). Window 2236×1258, guest 3840×2160.

What is slow or what changed

Facing the Chef Stand the title runs at 2.5–3.2 FPS. Turning away so the stand leaves the view brings it straight back to 8–13 FPS, and the frame-time drop happens on the same frame the stand leaves. The number of draws and dispatches hardly changes between the two views, so the cost is not the amount of work on screen:

View (F10 overlay) Draws Dispatches Frame
Side street 2,219 185 89 ms
Behind the stand 1,568 163 75 ms
Facing the stand 2,230 229 338 ms

While it is slow the GPU reports 77–100% utilisation at about 117 W (38% in the side street). The desktop stutters too.

One compute shader accounts for it. With APS5_PROFILE_GPU=1 (a separate run from the trace below), the [gputime] line facing the stand reads:

[gputime] 7684 ms of GPU time in 3629 batches over 10 s (…; 35 presents; …); by program: 0x20350f0000 x435 7401ms 0x20357c0000 x36 124ms 0x2035bb0000 x1078 24ms …

In the side street the same line totals about 835 ms per 10 s, and 0x20350f0000 is not among the costly programs. So 0x20350f0000 runs about 12 times per frame at roughly 17 ms of GPU time each.

What the shader does (its request, replayed offline with agc_shader_replay): 76 code words, wave64, 64×1×1 threads, no LDS. Each thread takes group × 64 + lane, reads a count and two offsets from a 1-record constant buffer, exits past the count, and copies one 16-byte record from one buffer to another:

Buffer Stride Records Size
source (read) 16 B 294,912 4.5 MiB
destination (written) 16 B 262,144 4 MiB

That is at most 4 MiB moved per dispatch, about 235 MiB/s at 17 ms. The [buffers] CPU-copy counters for both buffers' 256 MiB ranges do not grow between the side street and the stand, so AnyPS5 is not copying them on the CPU. I have not found out where the 17 ms goes on the GPU side (host-visible placement, a slow access path in the translated loads and stores, or something else). I can run a build with extra logging, or replay the request with a given setting, if that helps.

Summary lines

{"commit":"3e7b0a645924","dirty":false,"env":{"APS5_MOUSE_DIAG":"1","APS5_PLAYGO_UNLISTED_CHUNKS":"installed","APS5_TRACE_SECONDS":"240:10"},"gpu":"NVIDIA GeForce RTX 4090","driver":"NVIDIA 615.78.08","os":"Linux 7.3.0-3-cachyos","cpu":"Intel(R) Core(TM) i9-14900K","cpu_threads":32,"memory_mib":64108,"title_id":"PPSA15388","title":"High on Life","content_version":"01.120.000","resolution":"3840x2160","window":{"spec":"seconds 240:10","first_present":3730,"ms":10194.886},"presents":18,"frame_ms":{"p50":566.901,"p90":648.164,"p99":663.230,"max":663.230},"gpu_busy_ms_per_present":417.351,"worker_busy_pct":{"queue worker 0x0":99.516,"queue worker 0x20":99.489},"top_phases_ms_per_frame":{"Driver.Packet":1099.501,"Driver.Packet.memory_wait":480.242,"Driver.Packet.dispatch":447.145,"Driver.Dispatch":123.691,"Driver.Packet.draw":119.613,"Driver.Draw":118.902,"Recorder.Submit":101.262,"Resources.Complete":84.185},"trace_dropped":false,"trace":"aps5-trace-3e7b0a645924-2.json.gz"}

Traces

aps5-trace-3e7b0a645924-2.json.gz, attached below (18 presents, standing still facing the stand).

Digest

seconds 240:10 from present 3730: 18 presents, frame p50 566.9 / p90 648.2 / p99 663.2 / max 663.2 ms, GPU busy 417.4 ms per present
NVIDIA GeForce RTX 4090, NVIDIA 615.78.08, Linux 7.3.0-3-cachyos, 3840x2160, commit 3e7b0a645924, dropped: no

{"commit":"3e7b0a645924","dirty":false,"env":{"APS5_MOUSE_DIAG":"1","APS5_PLAYGO_UNLISTED_CHUNKS":"installed","APS5_TRACE_SECONDS":"240:10"},"gpu":"NVIDIA GeForce RTX 4090","driver":"NVIDIA 615.78.08","os":"Linux 7.3.0-3-cachyos","cpu":"Intel(R) Core(TM) i9-14900K","cpu_threads":32,"memory_mib":64108,"title_id":"PPSA15388","title":"High on Life","content_version":"01.120.000","resolution":"3840x2160","window":{"spec":"seconds 240:10","first_present":3730,"ms":10194.886},"presents":18,"frame_ms":{"p50":566.901,"p90":648.164,"p99":663.23,"max":663.23},"gpu_busy_ms_per_present":417.351,"worker_busy_pct":{"queue worker 0x0":99.516,"queue worker 0x20":99.489},"top_phases_ms_per_frame":{"Driver.Packet":1099.501,"Driver.Packet.memory_wait":480.242,"Driver.Packet.dispatch":447.145,"Driver.Dispatch":123.691,"Driver.Packet.draw":119.613,"Driver.Draw":118.902,"Recorder.Submit":101.262,"Resources.Complete":84.185},"trace_dropped":false,"trace":"aps5-trace-3e7b0a645924-2.json.gz"}

Slowest frames (largest slices inside each, inclusive)

  • present 3734: 663.2 ms at 2180 ms: Driver.Packet (queue worker 0x0) 632.5, Driver.Packet (queue worker 0x20) 616.3, batch (gpu) 395.6, Driver.Packet.dispatch (queue worker 0x20) 316.3
  • present 3745: 648.2 ms at 8551 ms: Driver.Packet (queue worker 0x20) 644.2, Driver.Packet (queue worker 0x0) 616.1, batch (gpu) 393.8, Driver.Packet.dispatch (queue worker 0x20) 331.6
  • present 3742: 635.3 ms at 6865 ms: Driver.Packet (queue worker 0x20) 631.9, Driver.Packet (queue worker 0x0) 603.2, batch (gpu) 391.8, Driver.Packet.dispatch (queue worker 0x20) 329.7
  • present 3739: 622.7 ms at 5083 ms: Driver.Packet (queue worker 0x0) 587.7, Driver.Packet (queue worker 0x20) 537.8, batch (gpu) 399.6, Driver.Packet.dispatch (queue worker 0x20) 336.4
  • present 3740: 608.4 ms at 5705 ms: Driver.Packet (queue worker 0x20) 604.9, Driver.Packet (queue worker 0x0) 577.1, batch (gpu) 396.5, Driver.Packet.dispatch (queue worker 0x20) 333.4

Threads (ms per frame)

thread busy wait untraced in slices < 20 us largest
queue worker 0x0 254.5 (45 %) 279.7 (49 %) 32.2 154.9 Driver.Packet 533.7, Driver.EndSubmission 0.4
draw committer 83.0 (15 %) 30.6 (5 %) 452.8 36.0 Graphics.Draw 54.1, Recorder.Submit 28.3
queue worker 0x20 357.9 (63 %) 208.0 (37 %) 0.6 1.6 Driver.Packet 536.1, Driver.EndSubmission 27.0
presenter 0.3 (0 %) 0.3 (0 %) 565.8 0.1 VideoOut.Flip 0.6, Driver.Present 0.0
gpu 394.6 (70 %) 0.0 (0 %) 171.8 0.0 batch 394.6

Largest slices (ms per frame, inclusive, all threads): Driver.Packet 1069.7, Driver.Packet.memory_wait 480.2, Driver.Packet.dispatch 446.1, batch 394.6, Driver.Dispatch 122.6, Driver.Packet.draw 105.9, Driver.Draw 105.2, Recorder.Submit 100.3, Resources.Complete 82.7, Vulkan.Dispatch 79.7

Longest waits

  • Driver.Packet.memory_wait (queue worker 0x0) 293.7 ms at 1820 ms, present 3733
  • Driver.Packet.memory_wait (queue worker 0x0) 290.4 ms at 8227 ms, present 3744
  • Driver.Packet.memory_wait (queue worker 0x0) 288.1 ms at 3602 ms, present 3736
  • Driver.Packet.memory_wait (queue worker 0x0) 287.5 ms at 8808 ms, present 3745
  • Driver.Packet.memory_wait (queue worker 0x0) 287.0 ms at 5909 ms, present 3740
  • Driver.Packet.memory_wait (queue worker 0x0) 286.5 ms at 4110 ms, present 3737
  • Driver.Packet.memory_wait (queue worker 0x0) 285.1 ms at 2973 ms, present 3735
  • Driver.Packet.memory_wait (queue worker 0x0) 279.0 ms at 1300 ms, present 3732
  • Driver.Packet.memory_wait (queue worker 0x0) 278.8 ms at 9403 ms, present 3746
  • Driver.Packet.memory_wait (queue worker 0x0) 274.6 ms at 7138 ms, present 3742

Largest GPU batches (GPU ranges inside, and the CPU slices that recorded each, inclusive)

  • batch 1091: 189.3 ms at 5416 ms, present 3739: GPU no ranges; recorded by Graphics.Draw (draw committer) 1.5, Graphics.Draw.draw_and_resource_release (draw committer) 1.2, Resources.Complete (draw committer) 0.7, Resources.Prepare (draw committer) 0.1
  • batch 965: 189.1 ms at 4743 ms, present 3738: GPU no ranges; recorded by Graphics.Draw (draw committer) 0.3, Graphics.Draw.draw_and_resource_release (draw committer) 0.2
  • batch 401: 188.6 ms at 1872 ms, present 3733: GPU no ranges; recorded by Graphics.Draw (draw committer) 1.7, Graphics.Draw.draw_and_resource_release (draw committer) 1.4, Resources.Complete (draw committer) 0.8, Resources.Prepare (draw committer) 0.0
  • batch 1204: 188.2 ms at 5954 ms, present 3740: GPU no ranges; recorded by Driver.Packet (queue worker 0x0) 440.4, Driver.Packet.memory_wait (queue worker 0x0) 287.0, Driver.Packet.draw (queue worker 0x0) 142.5, Driver.Draw (queue worker 0x0) 142.2
  • batch 523: 187.9 ms at 2453 ms, present 3734: GPU no ranges; recorded by Graphics.Draw (draw committer) 1.9, Graphics.Draw.draw_and_resource_release (draw committer) 1.5, Resources.Complete (draw committer) 0.7, Resources.Prepare (draw committer) 0.1

Capture

  • The caches were warm (the scene ran once and the title was restarted)
  • No other APS5_TRACE_* or APS5_PROFILE_* switch was set. Two notes: APS5_MOUSE_DIAG (local logging, see below) was set, but it only logs while the mouse moves, and the view was still during the window; and the launcher copies stdout and stderr to a file through tee.
  • Captures compared here come from the same machine, scene and settings (one capture only; the side-street numbers above come from the F10 overlay and [gputime], not from a trace)

The trace build is main 895f47f plus a local commit that only adds logging to libSceMouse, for a separate input change (APS5_MOUSE_DIAG in env). Linux (CachyOS), RTX 4090.

Tested and written by Claude (AI assistant) on a contributor's machine.

Image

TRACE FILE:

aps5-trace-3e7b0a645924-2.json.gz

Activity

  1. Tamalero commented on Oct 23, 2026

    @Tamalero
    ContributorAuthor

    With F10 trace:

    Image Image Image Image
  2. EagleBearerYS commented on Oct 23, 2026

    @EagleBearerYS
    Contributor

    @Tamalero thanks, the per-program GPU time and the replayed request narrow this a lot. About 4 MiB per dispatch in ~17 ms on an RTX 4090 points at where the two buffers live or how their loads and stores are translated, not at the copy itself. Two things would tell those apart:

    • a run facing the stand with APS5_TRACE_HOST_IMPORT=1, plus the [gpu] host import lines whose ranges cover the source and destination buffers. They say whether the buffers are imported host memory, and how;
    • the replay request for 0x20350f0000, if you can attach it. A replay can compare its access path with a plain device-local copy of the same 16-byte records.
  3. MarcZX10 commented on Oct 23, 2026

    @MarcZX10
    Contributor

    I inspected the attached Chrome trace. Its dispatch and GPU events identify batch numbers but do not include shader addresses or descriptor/buffer ranges, so it cannot connect 0x20350f0000 to a particular host import or explain that shader's access path. It does show substantial overlapping Driver.Packet.memory_wait and GPU batch spans, but those aggregates do not identify which dispatch causes them. The requested host-import range log and replay request are still the evidence needed to distinguish buffer placement from translated-load/store cost.

  4. Tamalero commented on Oct 23, 2026

    @Tamalero
    ContributorAuthor

    A data point on current main (c188e01): the scene is GPU-bound whatever CPU it gets, so the copy shader is what limits it.

    Same spot facing Blorto's Chef Stand, standing still, warm caches, with only APS5_TRACE_SUMMARY_EVERY=10. Lines are 10-second intervals from aps5-summary.jsonl. The CPU set was changed live on the running process.

    CPUs (i9-14900K) Frame p50 GPU busy per present
    E-cores only (16) 325–343 ms 264–301 ms
    P-cores only (16) 288–297 ms 264–280 ms

    The GPU is busy for ~270 of every ~300 ms, and only the remaining CPU part changes with the core type. In the view where the save loads, nearby but without the stand in view, the same build runs at about 75 ms per frame, with about 35 ms of GPU per present. For reference, the F10 overlay on main 895f47f showed 338 ms at the stand.

    The build is c188e01 plus one local libkernel commit that adds an opt-in thread-affinity switch. It was off here.

    Tested and written by Claude (AI assistant) on a contributor's machine.

  5. Tamalero commented on Oct 23, 2026

    @Tamalero
    ContributorAuthor

    @EagleBearerYS @MarcZX10 here is the host-import run you asked for: main c188e01, APS5_TRACE_HOST_IMPORT=1, facing the stand, warm caches.

    Both copy buffers are in imported host memory. The source buffer is split across several imports with gaps between them.

    Buffer (from 0x20350f0000's V#s) Range [gpu] host import lines covering it
    destination, 16 B × 262,144 0x20c6040000–0x20c6440000 inside 0x20c4fa0000+0x1790000 … joined at bind by join; the join grows over the run (0x12b0000 → 0x1440000 → 0x15e0000 → 0x1790000). It also overlaps by map imports 0x20c5fc0000+0x290000, 0x20c6250000+0x190000 and 0x20c63e0000+0x1a0000.
    source, 16 B × 294,912 0x20d4a70000–0x20d4ef0000 four separate by map imports: 0x20d4a20000+0xd0000, 0x20d4b20000+0x130000, 0x20d4c50000+0x90000 and 0x20d4d20000+0x200000. No import covers 0x20d4af0000–0x20d4b20000 or 0x20d4ce0000–0x20d4d20000, and there's no join for this range.

    The trace has 31,660 [gpu] host import lines: 20,430 by map, 9,676 retired: the range was mapped again, 1,413 by build, 109 by access, 28 joined at bind, 4 by segment. I haven't checked which binding path the dispatch takes for the source range, which isn't covered by one import. If there's a counter or trace for that (staged, split or something else), I can run it.

    Caveat: the buffer addresses come from the V#s in the request I captured in an earlier run of the same scene. The title could lay these allocations out differently between runs. If you want, I'll capture the V#s and the import lines in the same run.

    About attaching the replay request: I can't attach it, because it carries the title's shader code and captured guest memory. I can run any comparison you describe on it locally, for example a device-local copy of the same 16-byte records against the translated access path, and post the timings.

    Tested and written by Claude (AI assistant) on a contributor's machine.

  6. EagleBearerYS commented on Oct 23, 2026

    @EagleBearerYS
    Contributor

    @Tamalero thanks, this is the piece that was missing. The destination sits inside one import (the growing join), but the source doesn't. It spans four by map imports, and two stretches of it, 0x20d4af0000–0x20d4b20000 and 0x20d4ce0000–0x20d4d20000, have no import at all. So main can't read the source in place through a single import. I haven't confirmed yet which path it takes instead. That fits a cost tied to where the buffers live rather than to the copy itself, but it doesn't show it yet.

    One run facing the Chef Stand in High on Life (PPSA15388), plus one A/B, would settle it:

    1. The same run with the V#s and imports, as you offered, adding APS5_TRACE_BIGBUF=1 APS5_PROFILE_DRAW=1 APS5_PROFILE_GPU=1 to APS5_TRACE_HOST_IMPORT=1:
      • [bigbuf] upload lists every guest region of 1 MiB or more that is copied for the GPU, with sparse=1 when only parts of it are backed. Does 0x20d4a70000's range appear there, and how often per 10 s?
      • The [buffers] … uploads line counts the regions copied out of imports on the GPU.
    2. The same spot with APS5_NO_HOST_IMPORT=1 added. It turns importing off, so shaders read copies of guest memory instead of using it in place (ENVIRONMENT.md). The whole frame may get slower; only 0x20350f0000's entry in the [gputime] line matters.
      • If its ~17 ms per dispatch falls well below a millisecond, the imports are where the time goes.
      • If it stays, the cost is in the translated loads and stores, or in the dispatch itself.
    3. From your captured request: the dispatch's group counts, and the count in its constant buffer. Each thread exits past the count, so if the dispatch launches far more groups than the count needs, empty waves could be the cost instead.
  7. Tamalero commented on Oct 23, 2026

    @Tamalero
    ContributorAuthor

    @EagleBearerYS @MarcZX10 this is fixed on current main. The fix is #8336 (8 MiB written-buffer staging on discrete GPUs), merged today.

    Same spot facing Blorto's Chef Stand, standing still, warm caches, RTX 4090. Every run had APS5_PROFILE_DRAW=1 APS5_PROFILE_GPU=1 APS5_TRACE_SUMMARY_EVERY=10. Values are three consecutive 10-second [gputime] and summary intervals at the stand.

    Build Extra switches 0x20350f0000 per 10 s Per dispatch Presents per 10 s Frame p50 GPU busy per present
    main c188e01 none 431–445 dispatches, 7.6–7.9 s ~17.6 ms 18–20 516–534 ms 438–469 ms
    main e13027a none 1,466–1,781, 226–275 ms ~0.15 ms 68–85 110–113 ms ~45 ms
    main e13027a APS5_NO_COPY_HLE=1 1,361–1,769, 76–94 ms ~0.055 ms 72–88 ~99 ms ~40 ms
    main e13027a APS5_NO_COPY_HLE=1 APS5_WRITTEN_SHADOW_MAX_KIB=2048 441–469, 7.5–8.0 s ~17 ms 22–24 439–444 ms ~395 ms

    The scene is now limited by other programs, and the frame is no longer dominated by this copy. Is there any objection to closing this issue as fixed by #8336? If you'd like more data from this scene first, I can run it.

    Tested and written by Claude (AI assistant) on a contributor's machine.

  8. EagleBearerYS commented on Oct 24, 2026

    @EagleBearerYS
    Contributor

    @Tamalero thank you, that settles it: on current main that dispatch at Blorto's Chef Stand takes about 0.15 ms instead of 17 ms (about 0.055 ms for the kernel itself with APS5_NO_COPY_HLE=1), and putting back the old 2 MiB staging window brings the 17 ms back, so #8336 is the fix. Nothing further is needed from you on this one.

  9. Tamalero commented on Oct 24, 2026

    @Tamalero
    ContributorAuthor

    Thanks @EagleBearerYS, and @MarcZX10 for the earlier pointers. Closing as fixed by #8336.

    Tested and written by Claude (AI assistant) on a contributor's machine.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions