Skip to content

perf(agc): store write-back changes sixteen bytes at a time - #2481

Closed
johnmaia wants to merge 1 commit into
boykopovar:mainfrom
johnmaia:perf/agc-write-back-sse2
Closed

johnmaia wants to merge 1 commit into
boykopovar:mainfrom
johnmaia:perf/agc-write-back-sse2

Conversation

@johnmaia

@johnmaia johnmaia commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

What

GuestMemory::WriteChanged stores only the bytes the GPU changed in a mirrored buffer, so that CPU writes to the bytes around them survive. It found those bytes one at a time inside every changed 256-byte block and stored each run with memcpy. When changed and unchanged bytes interleave (for example, only the low bytes of floats change), every run is a few bytes long, so each costs a byte loop and a memcpy call.

Each 16-byte chunk is now compared with SSE2:

  • no byte changed: skipped;
  • every byte changed: stored whole (_mm_storeu_si128);
  • some changed: stored with MASKMOVDQU (_mm_maskmoveu_si128), which writes only the bytes its mask selects.

An SFENCE after the loop orders the non-temporal masked stores before the stores that follow. A tail shorter than 16 bytes still goes byte by byte. Bytes the GPU did not change are never written, as before, and the reported changed range is the same.

Tested

Linux (Ubuntu 24.04, GCC 13.3), i3-12100, RX 7600 (RADV).

  • New testWriteChangedStoresExactlyTheChangedRuns (agc_driver_pm4_tests): 1037 bytes of random runs (changed, unchanged, and changed with some bytes left equal) over guest bytes that differ from both copies. Checks that exactly the changed bytes are stored and that the 13-byte tail is handled.

  • Full ctest: 553/554. The failure is guest_video_out_resolution_status, which needs a Vulkan-capable SDL display and fails over SSH (offscreen driver) without this change too. tools/check_conventions.py and tools/check_test_registration.py pass.

  • Microbenchmark, 1 MiB, best of 30, one core:

    pattern before after
    low 2 bytes of every 4 change 0.89 ms 0.20 ms
    every byte changes 0.09 ms 0.06 ms
    1 byte in 64 changes 0.46 ms 0.21 ms
    half of the 4 KiB pages change 0.06 ms 0.05 ms
  • Disney Epic Mickey: Rebrushed (PPSA15657, UE4), first level, on its testing stack (Disney Epic Mickey: Rebrushed (PPSA15657) on Linux #2255) with APS5_NO_HOST_IMPORT=1, so the level's compute buffers are mirrored:

    • Before: WriteChanged took 43% of the GPU submission thread, the bottleneck thread here (the GPU was ~1.4% busy), at about 1.0 ms per MiB written back.
    • After: about 0.26 ms per MiB and 17% of that thread.
    • Average FPS over the same stretch of the level (5-15 min after Continue, 300 samples each): 4.60 before, 6.92 after.
  • Windows (MinGW) is covered by CI only; SSE2 is part of the x86-64 baseline.

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

🤖 Generated with Claude Code

GuestMemory::WriteChanged stores only the bytes the GPU changed, so that CPU writes to the bytes
around them survive. It found them a byte at a time inside every changed 256-byte block and
stored each run with memcpy. Where changed and unchanged bytes interleave (the low bytes of
floats), every run is a few bytes long, and that loop took 43% of the GPU submission thread in
Disney Epic Mickey: Rebrushed's first level (0.9-1.0 ms per MiB).

Each 16-byte chunk is now compared with SSE2. A chunk with no change is skipped, a chunk that
changed entirely is stored whole, and a mixed one is stored with MASKMOVDQU, which writes only the
bytes selected by the mask. An SFENCE after the loop orders those non-temporal stores before the
stores that follow. Bytes the GPU did not change are still never written.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0134RWCyiyydJBbfKz8g9Bwe
@github-actions github-actions Bot added the agc libSceAgc* label Oct 9, 2026
@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown

Thank you for your interest in the project! We are sorry, but we cannot accept more PRs while we work through the existing backlog. This PR has been automatically closed and labeled "intake paused". This is not a judgment of the quality of your changes.

@github-actions github-actions Bot added the intake paused Closed while PR intake is limited to authors with at least 10 PRs merged into main label Oct 9, 2026
@github-actions github-actions Bot closed this Oct 9, 2026
@johnmaia

Copy link
Copy Markdown
Contributor Author

Windows run: Windows 11 (26200), i7-13700K, MinGW-w64 GCC 15.2.0 (WinLibs r7), Release. agc_driver_pm4 and agc_driver_pm4_failure pass through ctest, including testWriteChangedStoresExactlyTheChangedRuns.

@johnmaia

Copy link
Copy Markdown
Contributor Author

Reopened as #3137 (rebased onto current main; the description now says the Epic Mickey numbers were measured over the intro movie stretch, not level rendering).

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

Labels

agc libSceAgc* intake paused Closed while PR intake is limited to authors with at least 10 PRs merged into main

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant