Skip to content

Respect MemoryAccessVolatileMask on OpLoad to prevent forwarding#2

Open
hughperkins wants to merge 3 commits intomainfrom
hp/volatile-load-no-forward
Open

Respect MemoryAccessVolatileMask on OpLoad to prevent forwarding#2
hughperkins wants to merge 3 commits intomainfrom
hp/volatile-load-no-forward

Conversation

@hughperkins
Copy link
Copy Markdown
Collaborator

Problem

SPIRV-Cross has an expression forwarding optimization that inlines loaded values at each use site rather than materializing them into temporaries. For example, given:

%val = OpLoad %int %ptr
OpStore %dst1 %val
OpStore %dst2 %val

Forwarding produces:

*dst1 = *ptr;
*dst2 = *ptr;

rather than:

int val = *ptr;
*dst1 = val;
*dst2 = val;

This is done intentionally to produce cleaner, more readable generated code with fewer temporary variables. When a value is used only once, forwarding is always safe — it's the same single read, just without naming it. When a value is used multiple times, the duplicated dereferences look like extra memory I/O, but for normal storage buffer and uniform loads the downstream shader compiler (Metal, GLSL, etc.) can see that the pointer target hasn't been modified between uses and will CSE (common subexpression eliminate) them back into a single read. So in practice, forwarding doesn't cost extra I/O in the normal case.

However, for PhysicalStorageBuffer pointers (raw device T* in Metal, buffer_reference in GLSL), the downstream compiler cannot easily prove the pointer target is stable — especially when stores to the same address space happen between uses. If the OpLoad carries the Volatile memory access flag, the SPIR-V is explicitly stating that the memory location may change unpredictably. Duplicating the read by forwarding can then produce incorrect results: the two use sites may observe different values from what should be a single load.

Fix

When processing an OpLoad, SPIRV-Cross already decides whether to forward the expression. This patch adds a check: if the load carries MemoryAccessVolatileMask, forwarding is disabled, forcing the value into a temporary. This ensures the memory is read exactly once, matching SPIR-V semantics.

Test plan

  • Added MSL regression test (volatile-phys-buf-load-no-forward.asm.msl24.comp): volatile load from a PhysicalStorageBuffer pointer used in two stores; reference output shows a temporary rather than duplicated dereferences
  • Added Vulkan GLSL regression test (volatile-phys-buf-load-no-forward.nocompat.vk.asm.comp): same shader, verifying the GLSL backend also materializes a temporary
  • Both tests fail (output differs from reference) without the fix and pass with it
  • Ran full shaders-msl-no-opt and shaders-no-opt test suites; no regressions

What the test does

The test is a minimal SPIR-V compute shader that:

  1. Loads two raw GPU addresses (u64) from push constants — one for a source pointer, one for a destination pointer
  2. Does a single OpLoad with Volatile|Aligned from the source pointer, producing one value
  3. Stores that value to two locations: the destination pointer, and the destination pointer + 4 bytes

The reference output checks that SPIRV-Cross emits one read into a temporary and reuses it:

int _22 = *(reinterpret_cast<device int*>(registers.addr));  // one read
*_21 = _22;                                                   // reuse
*(reinterpret_cast<device int*>(...)) = _22;                  // reuse

Without the fix, SPIRV-Cross forwards the load and emits two reads:

*_21 = *_18;                                                  // read 1
*(reinterpret_cast<device int*>(...)) = *_18;                 // read 2

The test is a source-level regression test that verifies the shape of the generated code — it does not execute the shader at runtime.

hughperkins and others added 3 commits March 3, 2026 12:44
When an OpLoad carries the Volatile memory access flag, the loaded
value must not be forwarded (inlined) at each use site. Without this,
SPIRV-Cross re-evaluates the pointer dereference expression at every
use, which produces wrong results when the pointed-to memory is
modified between the original load and subsequent uses (e.g. an
insertion sort shifting elements in the same array accessed via
PhysicalStorageBuffer pointers).

Made-with: Cursor
Add MSL and Vulkan GLSL test cases that verify an OpLoad with
Volatile|Aligned from a PhysicalStorageBuffer pointer is not
forwarded. Without the previous commit, the loaded value would be
re-dereferenced at each use site.

Made-with: Cursor
Co-authored-by: Hans-Kristian Arntzen <post@arntzen-software.no>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants