fix(shader): keep runtime image mode switches inside continue targets

An image with several runtime modes is emitted as a switch whose default
and disabled modes end in OpUnreachable. In a loop latch that exit breaks
the continue construct, and spirv-val rejected the shader. Inside a
continue target those cases now join the merge with an undefined result.

Fixes #1921

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alex 2026-10-08 22:54:23 -04:00
parent f145139676
commit 33603bf8a9
4 changed files with 37 additions and 8 deletions

View File

@ -419,6 +419,14 @@ void verifyBindlessTable() {
require(wholeCapture->snapshot.images[wholeRoot].dwords == heap[0] && wholeCapture->snapshot.images[wholeDirect].dwords == heap[1] && wholeCapture->snapshot.images[wholeDirect + 1u].dwords == heap[2] && wholeCapture->snapshot.images[wholeDirect + 2u].dwords == heap[3], "bindless: mode T slots are wrong");
whole.context.memory = wholeMemory.Regions();
require(!Recompile(whole, *wholeCapture)->spirv.empty(), "bindless: mode T did not compile");
std::vector<std::uint32_t> latchCode(wholeCode.begin(), wholeCode.begin() + 12);
latchCode.insert(latchCode.end(), {0xbea80380u, 0xf09c0f08u, 0x00450000u, 0x80288128u, 0xbf0a8428u, 0xbf85fffbu, 0xe0700000u, 0x80070000u, 0xbf810000u});
auto latch = makeRequest(latchCode);
AgcDriver::ShaderMemory latchMemory({});
const auto latchCapture = latchMemory.Capture(latch);
const auto latchRegions = latchMemory.Regions();
latch.context.memory = latchRegions;
require(!Recompile(latch, *latchCapture)->spirv.empty(), "bindless: a sample in a loop latch did not compile");
// A table wider than the slots without a material pattern is rejected.
fillSrt(100u);
@ -1402,6 +1410,8 @@ void verifyUnnormalizedSamplers() {
for (const std::uint32_t mimg : {0xf0c00f08u, 0xf11c0108u, 0xf09c0f18u}) {
static_cast<void>(recompile(program(mimg), imageData(normalized, mimg == 0xf09c0f18u ? TypeCube : Type2D, Format8888UNorm, mimg == 0xf09c0f18u ? 5u : 0u)));
}
const std::vector<std::uint32_t> latchSample{0xbe900380u, 0x7e020280u, 0x7e040280u, 0x7e060280u, 0x7e080280u, 0x7e0a0280u, 0x7e0c0280u, 0xf09c0f28u, 0x00400801u, 0x80108110u, 0xbf0a8410u, 0xbf85fffbu, 0xe0700000u, 0x80030800u, 0xbf810000u};
static_cast<void>(recompile(latchSample, imageData(normalized, Type2DArray, Format8888UNorm, 5u)));
const std::array<std::uint32_t, 5> pixelCode{0xf0800f08u, 0x00400801u, 0xf800180fu, 0x0b0a0908u, 0xbf810000u};
const auto pixelData = userData(unnormalized);

View File

@ -116,6 +116,7 @@ struct SpirvEmitterState {
std::uint32_t bdaStopValue = 0;
// False for programs with workgroup barriers: faulting BDA accesses then continue (see BdaInvocationsMayStop).
bool bdaStopsInvocations = true;
bool continueTarget = false;
// Execution scope of the barriers that keep one guest wave's LDS accesses in program order across
// host invocations (see WaveLdsScope); 0 when none are emitted.
std::uint32_t waveLdsScope = 0;

View File

@ -811,11 +811,13 @@ void EmitControlFlow(SpirvValueEmitContext& context, StructuredFunctionState& fu
context.Fail("structured control flow block has no terminator metadata");
}
const bool stops = state.bdaStopsInvocations;
state.bdaStopsInvocations = stops && !IsContinueTarget(program, info->id);
state.continueTarget = IsContinueTarget(program, info->id);
state.bdaStopsInvocations = stops && !state.continueTarget;
EmitStructuredBlock(context, functionState, block);
functionState.blockExitLabels.emplace(block, state.currentLabel);
EmitStructuredTerminator(context, program, *info);
state.bdaStopsInvocations = stops;
state.continueTarget = false;
}
PatchStructuredPhis(context, functionState);
}

View File

@ -1568,17 +1568,23 @@ void EmitImage(SpirvValueEmitContext& ctx, const IrValue& inst) {
const bool returnsValue = inst.Opcode() != IrOpcode::ImageWrite;
const auto resultId = returnsValue ? ctx.Result(inst) : 0u;
ctx.definitions.erase(&inst);
const bool exits = !state.continueTarget;
const auto merge = state.module.AllocateId();
const auto invalid = state.module.AllocateId();
const auto invalid = exits ? state.module.AllocateId() : merge;
const auto undefined = returnsValue && !exits ? state.module.AllocateId() : 0u;
std::vector<std::uint32_t> labels(modes.size());
std::vector<std::uint32_t> words{spv::OpSwitch, selector, invalid};
for (std::uint32_t index = 0u; index < modes.size(); ++index) {
labels[index] = state.module.AllocateId();
words.insert(words.end(), {index, labels[index]});
}
std::vector<std::uint32_t> incoming;
if (undefined != 0u) {
state.module.AddFunction(spv::OpUndef, TypeId(state, inst.Type()), undefined);
incoming.insert(incoming.end(), {undefined, state.currentLabel});
}
state.module.AddFunction(spv::OpSelectionMerge, merge, spv::SelectionControlMaskNone);
state.module.AddFunction(words);
std::vector<std::uint32_t> incoming;
for (std::uint32_t index = 0u; index < modes.size(); ++index) {
EmitLabel(state, labels[index]);
std::uint32_t modeMerge = 0u;
@ -1586,18 +1592,26 @@ void EmitImage(SpirvValueEmitContext& ctx, const IrValue& inst) {
const auto enabled = state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::ImageModeBase + memory.resource * PipelineSpecialization::ImageModeStride + index, 1u);
const auto active = Binary(state, spv::OpINotEqual, TypeBool(state), enabled, ConstantU32(state, 0u));
const auto activeLabel = state.module.AllocateId();
const auto inactiveLabel = state.module.AllocateId();
modeMerge = state.module.AllocateId();
const auto inactiveLabel = exits ? state.module.AllocateId() : modeMerge;
state.module.AddFunction(spv::OpSelectionMerge, modeMerge, spv::SelectionControlMaskNone);
state.module.AddFunction(spv::OpBranchConditional, active, activeLabel, inactiveLabel);
EmitLabel(state, inactiveLabel);
state.module.AddFunction(spv::OpUnreachable);
if (exits) {
EmitLabel(state, inactiveLabel);
state.module.AddFunction(spv::OpUnreachable);
}
EmitLabel(state, activeLabel);
}
emitMode(modes[index]);
if (modeMerge != 0u) {
const auto activeEnd = state.currentLabel;
state.module.AddFunction(spv::OpBranch, modeMerge);
EmitLabel(state, modeMerge);
if (undefined != 0u) {
const auto joined = state.module.AllocateId();
state.module.AddFunction(spv::OpPhi, TypeId(state, inst.Type()), joined, ctx.Def(&inst), activeEnd, undefined, labels[index]);
ctx.definitions.insert_or_assign(&inst, joined);
}
}
if (returnsValue) incoming.insert(incoming.end(), {ctx.Def(&inst), state.currentLabel});
ctx.definitions.erase(&inst);
@ -1605,8 +1619,10 @@ void EmitImage(SpirvValueEmitContext& ctx, const IrValue& inst) {
}
state.runtimeImage = nullptr;
state.runtimeImageMetadata = 0u;
EmitLabel(state, invalid);
state.module.AddFunction(spv::OpUnreachable);
if (exits) {
EmitLabel(state, invalid);
state.module.AddFunction(spv::OpUnreachable);
}
EmitLabel(state, merge);
if (returnsValue) {
std::vector<std::uint32_t> phi{spv::OpPhi, TypeId(state, inst.Type()), resultId};