Preserve update payload when normalizing prepare backedges
Validated against origin/next 2bdbcf5a488574d22df6930f0392f95f7fba33b8.
The residual fix is generic XIR CFG normalization, with no renderer-specific
conditions, additional SSA spill repair, or floating-point changes.
Cause and invariant
For a Loop with prepare P and distinct update U, an executable edge B -> P skips U’s payload. Both boundary-branch normalization and the batch continuation planner classified prepare as a continue target, then redirected such edges to U. That changes the executed store sequence when U is not transparent:
original: define -> U(payload) -> P skip -> P
incorrect: define -> U(payload) -> P skip -> U(payload) -> P
factored: define -> U(payload) -> C -> P skip -> C -> P
C is a payload-free continuation trampoline. The existing update-region factoring retains entry into executable U while separating iteration completion. It must run before either normalization family, after prepare subdivision has established the actual backedge target.
Do not factor every ordinary payloadful update: preserving a unique latch’s update role prevents unrelated selection-entry repair from cloning its payload. For an otherwise canonical U ending in Branch(P), factoring is required only if P has an executable predecessor other than U or the loop owner’s entry. Forwarding F -> P remains a bypass; declarative block-role uses do not count. This condition is reevaluated in every post round, including newly recovered loops and generated exit dispatches.
Baselines and permanent regressions
The frozen 6e58928d84604bf0d976f0d6b00f44006b21ceb5 SDK rejected an actual
native-Vulkan zero-BSDF module with 77 non-dominating stores. Checkpoints first
located the violation in normalize_loop_boundary_conditional_branches:
dispatch -> prepare was changed to dispatch -> payloadful update.
After fetching, unmodified latest-next already accepts that complete module; its newer ownership fixes take a different structural route. It is not correct to claim this residual patch was needed to make that original module verify. However, both minimal counterexamples still fail on unmodified latest-next:
Six blocks: a load in the non-skip arm feeds a store in U. Output verification fails when skip is redirected into U. The new
update_bypassunit covers both mutation modes, three allocation orders, and uint/float3 payloads.Seven blocks: an additional exit generates a state dispatch. Earlier reg2mem can conceal the dominance violation, but the skip arm now reads stale or uninitialized spill state and executes an extra output store. The existing scalar compiler-test interpreter compares complete effect sequences; selectors 0/1/2 must respectively execute U, skip U, or break. Coverage includes raw/structured prepare and Branch/Continue entry into U. This is not a CPU shader or reference renderer.
The existing no-cloning control remains and now covers both mutation modes, requiring an ordinary unique latch to retain its update role.
The permanent additions were built and run red before changing the pass. An initial unconditional factoring candidate failed the existing no-cloning test; it was replaced by the exact executable-bypass condition above, not by weakening the test.
Validation and reproduction
The isolated SDK was rebuilt with its own compatible core/AST/XIR libraries and all 32 threads. Dependency sources were reused only after checking clean status and exact pinned gitlink identities. Temporary dependency symlinks are not part of this patch.
cmake --build build --parallel 32 --target $(
ctest --test-dir build --show-only=json-v1 -L unit_xir | jq -r '.tests[].name'
)
ctest --test-dir build -L unit_xir --output-on-failure --parallel 32
Result: 71/71 host XIR tests passed, including 72 assertions in the new six-block unit, the expanded effect-sequence suite, and the two-mode no-cloning control. Complete original-module replay also passes with zero invalid, irreducible, unstructured, iteration-limit, or verifier errors.
Retained evidence directory:
/var/tmp/psycles-holdout-CXlJR7/restructure-capture-Dd85c6.
Full build/test logs are candidate-2-full-build.log and
candidate-2-full-ctest.log; original-module output is
candidate-2-original.result.txt. Unmodified latest libraries are archived in
latest-unmodified-libs/, with a SHA256SUMS file. The original valid interchange
is restructure-0.input.xir, not an extracted sibling kernel.
Exact host replay used the independently compiled tool linked to the isolated
latest SDK (library resolution checked with ldd):
/var/tmp/psycles-holdout-CXlJR7/latest-replay/build/latest_replay \
/var/tmp/psycles-holdout-CXlJR7/restructure-capture-Dd85c6/restructure-0.input.xir \
/var/tmp/psycles-holdout-CXlJR7/restructure-capture-Dd85c6/NEW_REPLAY_PREFIX
SHA-256 proof identities:
Artifact |
SHA-256 |
|---|---|
Frozen original valid interchange |
|
Unmodified latest |
|
Validated candidate |
|
Candidate |
|
Unmodified latest and the candidate both produce 6,833 debug block lines and 153,909 instruction lines for the complete capture. This shows no aggregate IR growth on this input; it is not a renderer performance measurement.
Actual renderer/HIP/fallback/strict native Vulkan validation after compatible integration remains pending. No backend success for this patch is inferred from host XIR verification.