Skip to content

fix(agc): forget the blocks kept for the CPU when an upload restamps their units - #15457

Merged
oneandonlydean merged 2 commits into
boykopovar:mainfrom
atinseau:codex/agc-kept-blocks-reupload
Nov 3, 2026
Merged

oneandonlydean merged 2 commits into
boykopovar:mainfrom
atinseau:codex/agc-kept-blocks-reupload

Conversation

@atinseau

@atinseau atinseau commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

What

Since #14302, when a storage write-back keeps blocks of a unit for the CPU, it records them with the unit's generation. While that record holds, new GPU results leave the unit's pending-since as it was, so a CPU store made before them still wins at the next write-back. The record is meant to end at the unit's next re-upload, which gives the unit a new generation. On memory outside the write watch the generation is 0 before and after the upload, so the record never ends: every later result of the unit keeps the first pending-since, the CPU store that the upload just took counts as newer than those results, and the next write-back drops them for the CPU's bytes.

agc_driver_image_atomics gains contention checks that show it. 2,048 threads in 32 groups run image_atomic_add, then image_atomic_umax, on one texel of an 8x8 R32_UINT image in a global array outside the write watch, each check followed by a read-back dispatch, with 32- and 64-lane waves. The umax check rewrites the texel while the add's results are pending; the umax dispatch keeps the CPU's bytes and re-uploads them, and on main the read-back dispatch keeps them again, so the texel reads back its initial 5 instead of the largest stored value.

StorageTexture::upload now forgets the kept blocks of each unit it restamps, since the image takes the guest bytes there. On watched memory nothing changes: the new generation already ended the record.

Tested

macOS 27.0.1, Apple M3 Max (36 GB), MoltenVK 1.4.2 from Vulkan SDK 1.4.363.0, Release build with -DBUILD_TESTING=ON. main does not run its libraries on macOS, so both sides are main 4f41f4c37 with the macOS port (the squash of #8658, which its author upstreams in parts) and one local build fix for #12919's test (a reinterpret_cast from const void* that Clang refuses). Both sides carry the new test; the change side adds the fix.

Not tested: Linux and Windows GPUs, where the new checks run on native 32-bit image atomics through the same storage image path, and any title.

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 under Silent stubs in their library's technical debt file
  • 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
  • Fixes: none
  • Unblocks: none
  • GPUs tested: Apple M3 Max, MoltenVK 1.4.2 (Vulkan SDK 1.4.363.0), macOS 27.0.1
  • AI-assisted: yes

Context

Found while rebasing our macOS branch onto main: our local emulation of 64-bit image atomics on MoltenVK has the same contention checks in agc_driver_image_atomics64, and they started failing after #14302.

…ispatches

Each check fills a texel the CPU rewrote, lets 2,048 threads of 32 groups
run image_atomic_add or image_atomic_umax on it, and reads it back with a
second dispatch; it runs with 32- and 64-lane waves. The umax check comes
after the add check on the same texel, so its CPU rewrite lands while the
add's results are still pending and the storage image keeps the CPU's
bytes before the umax dispatch writes it again.
…their units

boykopovar#14302 records the blocks a storage write-back keeps for the CPU with the
unit's generation, and keptForCpu holds while the unit keeps that
generation: new GPU results then leave the unit's pending-since as it
was, so a CPU store made before them still wins at the next write-back.
A re-upload ends the record by giving the unit a new generation. On
memory outside the write watch the generation is 0 before and after the
upload, so the record held for good: every later result of the unit kept
the pending-since of the first, the CPU store that the upload had just
taken counted as newer than the new results, and the next write-back
kept the CPU's bytes over them.

The contention checks of agc_driver_image_atomics show it: their texel is
in a global array outside the write watch. After the image_atomic_add
check, the umax check rewrites the texel while the add's results are
pending, the umax dispatch keeps those bytes and re-uploads them, and the
read-back dispatch kept them again, so the texel read back its initial 5
instead of the largest stored value.

An upload now forgets the kept blocks of each unit it restamps, since the
image takes the guest bytes there.
@oneandonlydean oneandonlydean self-assigned this Nov 3, 2026
@RNK123

RNK123 commented Nov 3, 2026

Copy link
Copy Markdown

The new contended image-atomic test reproduces the stale value on RADV with main's production file and passes with this change.

Linux CachyOS (kernel 7.3.1-1-cachyos), GCC 16.2.1 Release, Radeon RX 9060 XT (RADV GFX1200, Mesa 26.2.5, Vulkan 1.4.354), Khronos validation layer 1.4.363. Local merge of head 858915127445f9dd183a34192d84193665f0da89 onto main 4f41f4c375f5abb487dbd184fbc150f31994d762, tree 2140479a7a68414b738eefcb86f028507eef4ded.

  • Original agc_driver_image_atomics passes 3/3 on main; the candidate's extended version passes 3/3, including its wave32 and wave64 contended add/umax checks.
  • Counter-test: on the merged candidate, retain the unchanged candidate test but replace only Texture.cpp with its byte-identical main version. It fails 3/3 on the wave32 contended umax: read-back is 5, expected 4293705069. Restoring the candidate file makes the same test pass 3/3. The failed runs stop at wave32, so they do not establish the wave64 outcome on main.
  • The RX 9060 XT and Khronos layer were confirmed, with no test skips, VUID- or SYNC-HAZARD- in the focused logs. An unrelated LSFG implicit-layer loader error appeared in both arms; the requested Khronos layer did load.
Reproduction
cmake --build build-main --target agc_driver_image_atomics_tests -j 4
export ANYPS5_REQUIRE_VULKAN=1 ANYPS5_NO_SHADER_CACHE=1
export VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation VK_LOADER_DEBUG=layer
export VK_LAYER_PATH="$HOME/.local/share/anyps5-tester/vk-layers/usr/share/vulkan/explicit_layer.d"
ctest --test-dir build-main -R '^agc_driver_image_atomics$' --repeat until-fail:3 -V

For the counter-test, the new fixture is retained while only the production file is restored to main; run the command three separate times to retain all expected failures, then restore the candidate file.

No game or console behavior was tested. AI-assisted independent hardware test run (GPT-6.1 Terra medium).

@github-actions github-actions Bot added the agc libSceAgc* label Nov 3, 2026
@TechLevelZero

Copy link
Copy Markdown
Contributor

✅ AMD GPU test: passed

PR head 8589151 merged onto main 4f41f4c · tested 2026-11-03 12:35 UTC · 16 min

GPU AMD Radeon RX 6300 (RADV NAVI24), driver Mesa 25.2.8-0ubuntu0.24.04.4, Vulkan 1.4.318
Host Ubuntu 26.04.1 LTS, kernel 7.0.0-42-generic, container ubuntu:24.04
Logs build and test logs for this run
Build ✅ passed (Release, -DANYPS5_ENABLE_SPIRV_TOOLS=ON)
Tests ✅ 2,673 passed · 8 failed (0 new, 7 also fail on main, 1 unstable on main) · 58 skipped (ANYPS5_REQUIRE_VULKAN=1)
Test setup CPU-only tests without a GPU; GPU tests (and any CPU-only test that failed or skipped without one) once on the GPU with VK_LAYER_KHRONOS_validation and ANYPS5_NO_SHADER_CACHE=1; main ran the same way
Test selection 2,739 of 3,811 tests run: the ones this change can reach, chosen by tools/select_tests.py from main's build graph
Vulkan validation ✅ no new validation IDs vs main (923 tests run on the GPU)
Slow tests (>30 s, GPU tests timed with validation layers) ⚠️ agc_driver_pipeline_journal 120.0 s, ⚠️ public_headers 69.4 s, ⚠️ agc_driver_image_sample_unnormalized 43.1 s, ⚠️ agc_driver_lazy_resolved_abi 34.0 s, ⚠️ agc_driver_sample_multisample_target 30.3 s
Unstable on main (1, these also failed in recent main runs on this machine; not counted against this PR)
agc_driver_pipeline_journal: failed in 7 of the last 9 main runs
Failing on main too (7, not counted against this PR)
agc_driver_image_table
agc_driver_native_depth_bda_clear
agc_driver_native_multisample_depth
agc_driver_submit_progress
agc_submit_progress
agc_submit_progress_no_timeline
guest_socket_options_multicast
Validated but not compared (1, not in the main baseline's validation pass)
guest_job_manager_worker_spin_scale_10
Validation IDs already present on main (4 IDs in 517 tests)
VUID-VkDeviceCreateInfo-pNext-pNext (517 tests): agc_driver, agc_driver_address_passes, agc_driver_alias_copy_label, agc_driver_alpha_to_coverage, agc_driver_array_layer_target, agc_driver_bc_block_copy, agc_driver_bda_anchor_reads, agc_driver_bda_span_reads, and 509 more
VUID-vkCmdCopyImage-srcImage-01548 (8 tests): agc_driver_depth_clear_hle, agc_driver_depth_plane_buffer_memory, agc_driver_depth_plane_partial_store, agc_driver_depth_storage_dispatch, agc_driver_depth_surface_from_storage, agc_driver_dispatch_move_depth, agc_driver_stencil_null_feedback, agc_driver_stencil_plane_memory
VUID-vkCmdCopyImage-srcImage-01551 (8 tests): agc_driver_depth_clear_hle, agc_driver_depth_plane_buffer_memory, agc_driver_depth_plane_partial_store, agc_driver_depth_storage_dispatch, agc_driver_depth_surface_from_storage, agc_driver_dispatch_move_depth, agc_driver_stencil_null_feedback, agc_driver_stencil_plane_memory
VUID-VkPipelineColorBlendStateCreateInfo-pAttachments-00605 (1 test): agc_driver_srgb8_color_target
Skipped (58)
agc_driver_bda_reads_shadowed_results: skipped, the device reports watched host memory written when it imports or reads it
agc_driver_bda_reads_shadowed_results_per_build: skipped, the device reports watched host memory written when it imports or reads it
agc_driver_copied_writer_label_order: skipped, the device reports a minStorageBufferOffsetAlignment of 4, so the dispatch binds its record at +0x1004 in place and nothing is written back; the test needs an alignment above 4
agc_driver_float16_unused_nan_payloads: skipped, the device has no native f16 conversions with RTE rounding and kept f16 denormals
agc_driver_image_table_vouched_lookups: skipped, the device reports 4 of 4 host-imported pages written after a GPU read, so a watched imported table cannot be vouched for
agc_driver_linear_target_direct_write_completion: skipped, the device does not keep host imports write-watched, so no completion stamp over the target is tracked
agc_driver_ngg_vertex_wave_reduction: skipped, s3 describes a 32-lane wave only on 32-lane subgroups, and the device has 64 lanes
agc_driver_scattered_runs: skipped, host imports are unavailable: /dev/udmabuf is not accessible
agc_driver_sparse_dma_buf_import: skipped, host imports are unavailable: /dev/udmabuf is not accessible
agc_driver_unit_shadow: skipped, the device does not import watched host memory for unit shadows
agc_driver_unit_shadow_budget: skipped, the device does not import watched host memory for unit shadows
agc_host_import_sparse: skipped, host imports are unavailable: /dev/udmabuf is not accessible
agc_idle_publish_wait: skipped, the device reports 4 of 4 host-imported pages written after a GPU read, so a forced watch of the import reads its submissions as CPU stores
agc_import_settle_initial_dcc: skipped, the device has no deferred settle of host imports: imports under 1 MiB wait for one only on Windows
agc_shadowed_key_stores: skipped, the device imports no watched host memory under a unit shadow
agc_small_tiled_shadow_label: skipped, host imports are unavailable: the test block is not imported and watched
agc_storage_clear_hand_over: skipped, the device imports no watched host memory for the clear-key hand-over
agc_unit_shadow: skipped, the device does not import watched host memory for unit shadows
agc_unit_shadow_completion_store: skipped, the device reports watched host memory written when it imports or reads it
guest_http_uri_locale: 
guest_net_ctl: skipped, the host has no IPv4 address
guest_video_out_color_space: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_hdr: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_high_frame_rate: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_output_mode: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_resolution_status: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_wqhd_detection: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
video_out_flip_aftergpu: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_backtoback: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_controls: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_cpuflip: skipped, no display or Vulkan device: DisplayW
Reproduce
git fetch origin 858915127445f9dd183a34192d84193665f0da89 4f41f4c375f5abb487dbd184fbc150f31994d762
git checkout 4f41f4c375f5abb487dbd184fbc150f31994d762 && git merge --no-ff 858915127445f9dd183a34192d84193665f0da89
git submodule update --init --recursive
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON -DANYPS5_ENABLE_SPIRV_TOOLS=ON
cmake --build build && cmake --build build --target libs
VK_DRIVER_FILES=/nonexistent/icd.json ctest --test-dir build -E '^(agc|shader|recompiler|videoout|video_out|guest_video_out|vulkan)|gpu' --timeout 120 --parallel 12 --output-on-failure
ANYPS5_REQUIRE_VULKAN=1 ANYPS5_NO_SHADER_CACHE=1 VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation \
  ctest --test-dir build -R '^(agc|shader|recompiler|videoout|video_out|guest_video_out|vulkan)|gpu' --timeout 120 --parallel 16 --output-on-failure
# failures are then re-run alone: --timeout 360 --parallel 2; new validation IDs: the test alone, twice, --parallel 1

Automated build and test on a contributor's AMD machine; it does not replace upstream CI. A new comment is posted for each new push. Full logs.

@TechLevelZero

Copy link
Copy Markdown
Contributor

✅ NVIDIA GPU test: passed

PR head 8589151 merged onto main 4f41f4c · tested 2026-11-03 12:47 UTC · 25 min

GPU NVIDIA RTX 2000 Ada Generation, driver 595.99.02, Vulkan 1.4.329
Host Ubuntu 26.04.1 LTS, kernel 7.0.0-38-generic, container ubuntu:24.04
Logs build and test logs for this run
Build ✅ passed (Release, -DANYPS5_ENABLE_SPIRV_TOOLS=ON)
Tests ✅ 2,686 passed · 5 failed (0 new, 2 also fail on main, 3 unstable on main) · 48 skipped (ANYPS5_REQUIRE_VULKAN=1)
Test setup CPU-only tests without a GPU; GPU tests (and any CPU-only test that failed or skipped without one) once on the GPU with VK_LAYER_KHRONOS_validation and ANYPS5_NO_SHADER_CACHE=1; main ran the same way
Test selection 2,739 of 3,811 tests run: the ones this change can reach, chosen by tools/select_tests.py from main's build graph
Vulkan validation ✅ no new validation IDs vs main (922 tests run on the GPU)
Slow tests (>30 s, GPU tests timed with validation layers) ⚠️ agc_driver_pipeline_journal 120.1 s
Unstable on main (3, these also failed in recent main runs on this machine; not counted against this PR)
agc_driver_pipeline_journal: failed in 4 of the last 10 main runs
agc_submit_progress: failed in 6 of the last 10 main runs
agc_submit_progress_no_timeline: failed in 5 of the last 10 main runs
Failing on main too (2, not counted against this PR)
agc_driver_image_table
guest_socket_options_multicast
Validation IDs already present on main (4 IDs in 517 tests)
VUID-VkDeviceCreateInfo-pNext-pNext (517 tests): agc_driver, agc_driver_address_passes, agc_driver_alias_copy_label, agc_driver_alpha_to_coverage, agc_driver_array_layer_target, agc_driver_bc_block_copy, agc_driver_bda_anchor_reads, agc_driver_bda_span_reads, and 509 more
VUID-vkCmdCopyImage-srcImage-01548 (9 tests): agc_driver_depth_clear_hle, agc_driver_depth_plane_buffer_memory, agc_driver_depth_plane_partial_store, agc_driver_depth_plane_sampling, agc_driver_depth_storage_dispatch, agc_driver_depth_surface_from_storage, agc_driver_dispatch_move_depth, agc_driver_stencil_null_feedback, and 1 more
VUID-vkCmdCopyImage-srcImage-01551 (9 tests): agc_driver_depth_clear_hle, agc_driver_depth_plane_buffer_memory, agc_driver_depth_plane_partial_store, agc_driver_depth_plane_sampling, agc_driver_depth_storage_dispatch, agc_driver_depth_surface_from_storage, agc_driver_dispatch_move_depth, agc_driver_stencil_null_feedback, and 1 more
VUID-VkPipelineColorBlendStateCreateInfo-pAttachments-00605 (1 test): agc_driver_srgb8_color_target
Skipped (48)
agc_driver_bda_reads_shadowed_results: skipped, the device reports watched host memory written when it imports or reads it
agc_driver_bda_reads_shadowed_results_per_build: skipped, the device reports watched host memory written when it imports or reads it
agc_driver_sample_order_grid_reference: skipped, the device does not keep depth drawn at custom sample locations at those locations, even after a transition that names them: the composite of uniform renders holds the guest's depth in 0 of 3005 sloped samples, so no path can be compared with it bit for bit
agc_driver_scattered_runs: skipped, host imports are unavailable: /dev/udmabuf is not accessible
agc_driver_sparse_dma_buf_import: skipped, host imports are unavailable: /dev/udmabuf is not accessible
agc_driver_stencil_ref_export: skipped, the device lacks VK_EXT_shader_stencil_export
agc_host_import_sparse: skipped, host imports are unavailable: /dev/udmabuf is not accessible
agc_import_settle_initial_dcc: skipped, the device has no deferred settle of host imports: imports under 1 MiB wait for one only on Windows
agc_unit_shadow_completion_store: skipped, the device reports watched host memory written when it imports or reads it
guest_http_uri_locale: 
guest_net_ctl: skipped, the host has no IPv4 address
guest_video_out_color_space: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_hdr: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_high_frame_rate: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_output_mode: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_resolution_status: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_wqhd_detection: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
video_out_flip_aftergpu: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_backtoback: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_controls: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_cpuflip: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_cpuflip_in_order: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_dcc: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_deferred: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_eop: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_eop_aftergpu: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_eop_main: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not co
Reproduce
git fetch origin 858915127445f9dd183a34192d84193665f0da89 4f41f4c375f5abb487dbd184fbc150f31994d762
git checkout 4f41f4c375f5abb487dbd184fbc150f31994d762 && git merge --no-ff 858915127445f9dd183a34192d84193665f0da89
git submodule update --init --recursive
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON -DANYPS5_ENABLE_SPIRV_TOOLS=ON
cmake --build build && cmake --build build --target libs
VK_DRIVER_FILES=/nonexistent/icd.json ctest --test-dir build -E '^(agc|shader|recompiler|videoout|video_out|guest_video_out|vulkan)|gpu' --timeout 120 --parallel 32 --output-on-failure
ANYPS5_REQUIRE_VULKAN=1 ANYPS5_NO_SHADER_CACHE=1 VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation \
  ctest --test-dir build -R '^(agc|shader|recompiler|videoout|video_out|guest_video_out|vulkan)|gpu' --timeout 120 --parallel 4 --output-on-failure
# failures are then re-run alone: --timeout 360 --parallel 2; new validation IDs: the test alone, twice, --parallel 1

Automated build and test on a contributor's NVIDIA machine; it does not replace upstream CI. A new comment is posted for each new push. Full logs.

@XairRrR XairRrR left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed — approved

StorageTexture::upload now drops the kept-for-CPU blocks of each unit it restamps. The new contended image-atomic checks pass on lavapipe with this change and fail with main's Texture.cpp.

What I ran

Head 858915127445f9dd183a34192d84193665f0da89, main 4f41f4c375f5abb487dbd184fbc150f31994d762, merged with --no-ff (tree 2222ab4d9ef92600bd34325af6b0d1a54647cb68). Machine: Debian 13 VM, 2× Xeon Gold 6136 (32 vCPU), GCC 14.2.0, Release, Vulkan through lavapipe (Mesa 26.1.6, LLVM 19).

git diff --check 4f41f4c375f5abb487dbd184fbc150f31994d762...858915127445f9dd183a34192d84193665f0da89
python3 tools/check_conventions.py --base 4f41f4c375f5abb487dbd184fbc150f31994d762
env VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation ANYPS5_NO_SHADER_CACHE=1 ANYPS5_REQUIRE_VULKAN=1 VK_LOADER_DEBUG=error,warn ctest --test-dir build -V -R '^agc_driver_image_atomics$'
# negative control
git show 4f41f4c375f5abb487dbd184fbc150f31994d762:core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp > core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
ninja -C build -j32 -l32 libs
env VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation ANYPS5_NO_SHADER_CACHE=1 ANYPS5_REQUIRE_VULKAN=1 ctest --test-dir build --output-on-failure -R '^agc_driver_image_atomics$'
git show 2222ab4d9ef92600bd34325af6b0d1a54647cb68:core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp > core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
ninja -C build -j32 -l32 libs
# 3 runs
env VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation ANYPS5_NO_SHADER_CACHE=1 ANYPS5_REQUIRE_VULKAN=1 ctest --test-dir build --output-on-failure -R '^agc_driver_image_atomics'
git status --porcelain

Results

Check Result
git diff --check clean
check_conventions.py All conventions checks passed
Configure / build / libs (script) ok
Selected tests (script, 2,146 run) 2,025 passed, 114 expected skips, 0 new failures, 0 timeouts. The 7 known failures also fail on main: agc_driver_division, the denormal-mode tests, unwrap_self_…, vector_constant_factor_tininess
Validation layer comparison (script, 770 tests) Layer loaded in 769 tests and missing in none; 0 tests with new VUID-
agc_driver_image_atomics (merged) passed in 0.56–0.63 s
agc_driver_image_atomics{,64,_bitwise,_float} ×3 (merged) 12/12 passed, each under 1 s
Worktree after restore git status --porcelain empty
Merged run, tail of -V output
[gpu] storage image 0x55a9b0469000: guest memory changed while GPU results were pending; keeping the CPU's blocks
[src/Texture.cpp:3188 upload] StorageTexture upload submit
[src/Texture.cpp:3190 upload] StorageTexture upload done
[gpu] storage image 0x55a9b0469000 destroyed with GPU results pending
[gpu] storage image 0x55a9b046e000 destroyed with GPU results pending
image atomics tests passed
1/1 Test #1224: agc_driver_image_atomics .........   Passed    0.63 sec

Negative control

I put back main's core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp, which is the PR's only production file, and kept the new test. After rebuilding, agc_driver_image_atomics failed with the same message the description quotes:

AGC graphics: image_atomic_umax contended by 2048 wave32 threads on one texel: the texel holds 5, expected 4293705069

This is lavapipe evidence for the macOS/MoltenVK and RADV results in the description and in RNK123's counter-test. After I restored the merged file and rebuilt, the test passed in three runs, and the worktree matched the merged tree again.

Findings

Blocking

None.

Non-blocking

None.

I also checked the fix against the code around it:

  • forgetKeptAtUnwatch(layer) runs in stampLayers before the unit gets its new generation (Texture.cpp#L2916-L2919).
  • stampLayers has four callers: fast clear, the import path (whole and partial), the selected CPU layers and the whole CPU upload. In each one the restamped units really take the guest bytes or the guest's clear. A record that survived the upload could therefore only be wrong.
  • On watched memory, keptForCpu (L4798-L4805) compares the recorded generation with the new one, so the record already ended there and nothing changes. On unwatched memory the generation stays 0, so before this change the record never ended. That matches the description.
  • The write-back records its kept blocks after the upload's restamp (L6603-L6606), so the new forget cannot erase a record made by the same write-back.
  • The existing debt entry in docs/dev/debt/libSceAgcDriver.md already says "A block a write-back kept for the CPU stays the CPU's until its unit is uploaded again". The change makes that statement true outside the write watch too, so the entry needs no edit. The PR adds no code comments.
  • Checklist lines are present: Depends on, Fixes, Unblocks, GPUs tested (Apple M3 Max / MoltenVK) and AI-assisted. The new checks finish well under the 30 s limit.

Not verified

  • I did not run the change on any hardware GPU. RNK123 (RADV GFX1200) and TechLevelZero (RADV NAVI24, NVIDIA RTX 2000 Ada) report runs with no new failures.
  • I ran the wave64 checks only on the merged tree. With main's file the test stops at the first wave32 umax failure, so I did not see the wave64 result on main.
  • I did not test any title.

@atinseau

atinseau commented Nov 3, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @XairRrR, @RNK123 and @TechLevelZero. The negative controls on lavapipe and RADV match what I saw on MoltenVK: with main's Texture.cpp the contended umax reads back the unit's stale value, and with the change the record of the restamped unit's kept blocks goes, so the second dispatch reads the atomic result. As @RNK123 notes, the failing arm stops at wave32, so wave64 on main stays untested there; the change passes both widths.

@SleglJan

SleglJan commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

Data point from Linux / RADV, with a second upstream test that shows this.

agc_driver_store_over_untracked fails on main 4f41f4c37 (3 runs of 3) and passes with this PR's head 8589151 merged onto it (2 runs of 2). Its five write-watch cases print "not run, the surface is not write-watched" when the surface is not watched; on a lavapipe node without a write watch all five do and the test passes on both trees. Here the four cases that leave the watch print it too; the last, "a second store over a surface outside the write watch", checks the watch before its first store, runs, and fails.

With APS5_TRACE_FLUSH=1 on main the case "a surface written before it left the write watch" ends with

[flush] image <addr>+0x40000 keeps 1 CPU-written 64 KiB blocks (first stored byte at +0x10000, 1 ranges stored, generation 0)

and the last case flushes the same line for the same image and size, then fails with "store over untracked: the first result was not stored": block 0 stays the CPU's at generation 0 and does not take the stored value, as the PR describes for memory outside the write watch. With the PR merged, the earlier case still keeps the block at its own end, and the last case's flushes of that image carry no "keeps" line.

Radeon RX 9070 XT (RADV GFX1201), Mesa 26.2.4, Linux 7.2.10 (Arch-based), GCC 16.2.1, Release build with -DBUILD_TESTING=ON; one run per side in a full ^agc ctest run, the rest alone with -V.

AI-assisted: yes (Claude Code)

@atinseau

atinseau commented Nov 3, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @SleglJan, that is a second way in. The last case of agc_driver_store_over_untracked takes the same path as the contended checks: outside the write watch the kept block's generation is 0 before and after the upload, so on main the record never ends and the block stays the CPU's over the stored result. With the change the upload forgets the kept blocks of the units it restamps, which is why the later flushes of that image carry no "keeps" line. It also explains why lavapipe nodes without a write watch pass on both trees: there the case does not run.

@atinseau

atinseau commented Nov 3, 2026

Copy link
Copy Markdown
Contributor Author

A macOS data point for agc_driver_store_over_untracked: the test passes there on both sides, and the reason shows when it can catch this fault.

Apple M3 Max, MoltenVK 1.4.2 from Vulkan SDK 1.4.363.0, macOS 27.0.1, Release, main 4f41f4c37 with the macOS port (#8658). The base side is the same tree without this PR's line in Texture.cpp. With VK_LAYER_KHRONOS_validation, ANYPS5_REQUIRE_VULKAN=1 and ANYPS5_NO_SHADER_CACHE=1, alternating the sides, the test passes 3 runs of 3 on each, with no VUID-. Every case runs: the surfaces are write-watched here, so none prints "not run".

With APS5_TRACE_FLUSH=1 the base keeps CPU-written blocks at generation 0, as in your trace. The last case, "a second store over a surface outside the write watch", gets a fresh image (0x10d570000+0x40000) that no earlier case used, so it never meets a stale record; on your machine it got the earlier case's image. The case catches the fault only when the allocator hands that address back. On this Mac only the contended image-atomic checks of this PR fail on main.

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

Labels

agc libSceAgc*

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants