Skip to content

Commit 3fc0cd4

Browse files
hypermoistclaude
authored andcommitted
fix(shader): isolate terminating paths from loop continuations (open PR boykopovar#2311)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent acd26a7 commit 3fc0cd4

8 files changed

Lines changed: 165 additions & 3 deletions

File tree

‎core/libs/prx/libSceAgcDriver/CMakeLists.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -953,6 +953,7 @@ target_include_directories(agc_driver_prepared_shaders_tests PRIVATE
953953
$<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>
954954
)
955955

956+
target_include_directories(agc_driver_runtime_images_tests PRIVATE ${PROJECT_SOURCE_DIR}/3rdparty/SPIRV-Headers/include)
956957
if(BUILD_TESTING)
957958
set_target_properties(agc_shader_mode_register_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
958959
add_test(NAME agc_shader_mode_register COMMAND agc_shader_mode_register_tests)

‎core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,45 @@ void testStructExtractsAreNotShuffled() {
220220
if ((specialized[cursor] & 0xffffu) == spv::OpVectorShuffle) shuffled = true;
221221
}
222222
check(shuffled == vectorSource, vectorSource ? "extracts of one vector were not folded into a shuffle" : "extracts of a struct were folded into a vector shuffle");
223+
void testLoopContinuePatchingAndEmissionRanges() {
224+
SpirvModule module;
225+
check(!module.FunctionHasTerminationSince(0u), "an empty emission range contains a termination");
226+
const auto merge = module.AllocateId();
227+
const auto oldContinue = module.AllocateId();
228+
const auto newContinue = module.AllocateId();
229+
module.AddFunction(spv::OpNop);
230+
const auto loop = module.AddLoopMerge(merge, oldContinue);
231+
const auto body = module.FunctionInstructionOffset();
232+
module.AddFunction(spv::OpBranch, oldContinue);
233+
check(!module.FunctionHasTerminationSince(body), "an ordinary branch was classified as a termination");
234+
module.AddFunction(spv::OpUnreachable);
235+
check(module.FunctionHasTerminationSince(body), "the emitted unreachable path was not detected");
236+
check(!module.FunctionHasTerminationSince(module.FunctionInstructionOffset()), "an empty tail includes an earlier termination");
237+
for (std::uint32_t i = 0u; i < 40000u; ++i) module.AddFunction(spv::OpNop);
238+
module.Type(spv::OpTypeVoid);
239+
module.AddName(newContinue, "patched_continue");
240+
module.PatchLoopContinue(loop, newContinue);
241+
const auto words = module.Finalize();
242+
for (std::size_t offset = 5u; offset < words.size(); offset += words[offset] >> spv::WordCountShift) {
243+
if ((words[offset] & spv::OpCodeMask) == spv::OpLoopMerge) {
244+
check(words[offset + 1u] == merge && words[offset + 2u] == newContinue, "loop patch moved during function vector growth or declaration insertion");
245+
}
246+
}
247+
const auto refuses = [&](auto&& action, const char* what) {
248+
try { action(); check(false, what); } catch (const std::runtime_error&) {}
249+
};
250+
refuses([&] { module.PatchLoopContinue(0u, newContinue); }, "a non-loop instruction was patched as a loop merge");
251+
refuses([&] { module.PatchLoopContinue(loop, 0u); }, "a zero continue id was accepted");
252+
refuses([&] { module.PatchLoopContinue(module.FunctionInstructionOffset(), newContinue); }, "a patch beyond the instruction range was accepted");
253+
refuses([&] { static_cast<void>(module.FunctionHasTerminationSince(loop + 1u)); }, "a range inside an instruction was accepted");
254+
refuses([&] { static_cast<void>(module.FunctionHasTerminationSince(module.FunctionInstructionOffset() + 1u)); }, "a range past the function end was accepted");
255+
SpirvModule truncated;
256+
truncated.EmitFunctionInstruction({spv::OpLoopMerge});
257+
refuses([&] { truncated.PatchLoopContinue(0u, newContinue); }, "a truncated loop merge was patched");
258+
for (const auto opcode : {spv::OpReturn, spv::OpReturnValue, spv::OpKill, spv::OpTerminateInvocation}) {
259+
SpirvModule terminated;
260+
terminated.AddFunction(opcode);
261+
check(terminated.FunctionHasTerminationSince(0u), "a terminating opcode was missed");
223262
}
224263
}
225264

@@ -235,6 +274,7 @@ int main() {
235274
testModulesAreIndependent();
236275
testIdsStayUniqueBeyondCacheCapacity();
237276
testEmittedWordsAreStable();
277+
testLoopContinuePatchingAndEmissionRanges();
238278
if (failures != 0) {
239279
std::fprintf(stderr, "%d check(s) failed\n", failures);
240280
return 1;

‎core/libs/prx/libSceAgcDriver/tests/Structurizer.cpp‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,19 @@ void requireOriginalDominanceSets(ControlFlowGraph graph, const char* name) {
210210

211211
int main() {
212212
try {
213+
for (const auto& edges : {std::vector<std::vector<std::uint32_t>>{{1}, {2, 3}, {1}, {}},
214+
std::vector<std::vector<std::uint32_t>>{{1}, {2}, {1, 3}, {}}}) {
215+
auto graph = makeGraph(edges);
216+
const auto original = graph;
217+
Structurizer{}.Structurize(graph);
218+
for (const auto& source : original.blocks) {
219+
const auto copies = std::count_if(graph.blocks.begin(), graph.blocks.end(), [&](const BasicBlock& block) {
220+
return block.instructionBegin == source.instructionBegin && block.instructionEnd == source.instructionEnd;
221+
});
222+
if (copies != 1) throw std::runtime_error("loop normalization changed a guest instruction range");
223+
}
224+
requireStructuredBranches(graph, "dedicated loop continuation");
225+
}
213226
auto nested = makeGraph({{1}, {2}, {5, 3}, {5, 4}, {7}, {6, 7}, {}, {1}});
214227
Structurizer{}.Structurize(nested);
215228
requireExactPostDominators(nested, "nested selections");

‎core/libs/prx/libSceAgcDriver/tests/execution/RuntimeImages.cpp‎

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
2+
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
3+
#include "prx/libc/include/GuestAllocations.hpp"
24
#include "Recompiler.hpp"
35
#include "VulkanTestDevice.hpp"
46
#include <algorithm>
57
#include <array>
68
#include <bit>
79
#include <iostream>
10+
#include <memory>
11+
#include <set>
12+
#include <spirv/unified1/spirv.hpp>
813

914
namespace {
1015

@@ -14,6 +19,61 @@ alignas(256) std::array<std::array<std::uint32_t, 64>, 2> textures{};
1419
alignas(256) std::array<std::uint32_t, 64> output{};
1520
constexpr std::array<std::uint32_t, 10> code{0x7e3c02ffu, 0x3fa00000u, 0x7e3e02ffu, 0x3f000000u, 0xf09c0108u, 0x00610a1eu, 0x34060082u, 0xe0701000u, 0x80000a03u, 0xbf810000u};
1621

22+
void RunLoop(AgcDriver::VulkanDevice& device, std::uint32_t waveSize) {
23+
alignas(256) static std::array<std::uint32_t, 256> texels{};
24+
constexpr std::array<std::uint32_t, 12> loopCode{
25+
0xbe8c0381u, 0x7e040280u, 0x7e020280u, 0xbf0a840cu, 0xbf840006u,
26+
0x4a04040cu, 0xf0201108u, 0x00010200u, 0x4a020281u, 0x800c810cu, 0xbf82fff8u, 0xbf810000u,
27+
};
28+
GuestAllocations::Mutation().Add(texels.data(), sizeof(texels), true, true, true);
29+
const auto deregister = [](std::uint32_t* pointer) { GuestAllocations::Mutation().Remove(pointer); };
30+
const std::unique_ptr<std::uint32_t, decltype(deregister)> registration(texels.data(), deregister);
31+
texels.fill(0xdeadbeefu);
32+
const auto imageAddress = reinterpret_cast<std::uintptr_t>(texels.data());
33+
std::array<std::uint32_t, 16> userData{};
34+
const std::array<std::uint32_t, 8> image{
35+
static_cast<std::uint32_t>(imageAddress >> 8u), static_cast<std::uint32_t>(imageAddress >> 40u) | (20u << 20u) | (3u << 30u),
36+
15u | (3u << 14u), 0x90000facu, 0u, 0u, 0u, 0u,
37+
};
38+
std::copy(image.begin(), image.end(), userData.begin() + 4u);
39+
const ShaderRecompiler::ShaderComputeStageInfo compute{{64u, 1u, 1u}, 0u, {false, false, false}, false, 1u};
40+
ShaderRecompiler::RecompileRequest request{
41+
{ShaderRecompiler::ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(loopCode.data()), loopCode, 0u, {}},
42+
{waveSize, 0u, userData, compute, std::nullopt, std::nullopt, {}}, device.Target(), {0u, 0u, 0u, 128u},
43+
};
44+
request.useCache = false;
45+
const auto shader = ShaderRecompiler::Recompile(request);
46+
std::set<std::uint32_t> continuations;
47+
const auto& words = shader.spirv;
48+
for (std::size_t offset = 5u; offset < words.size(); offset += words[offset] >> 16u) {
49+
if ((words[offset] & 0xffffu) == spv::OpLoopMerge) continuations.insert(words[offset + 2u]);
50+
}
51+
Require(!continuations.empty(), "runtime image loop lost its loop construct");
52+
std::uint32_t label = 0u;
53+
for (std::size_t offset = 5u; offset < words.size(); offset += words[offset] >> 16u) {
54+
const auto opcode = words[offset] & 0xffffu;
55+
if (opcode == spv::OpLabel) label = words[offset + 1u];
56+
if (continuations.contains(label)) {
57+
Require(opcode != spv::OpUnreachable && opcode != spv::OpReturn && opcode != spv::OpReturnValue &&
58+
opcode != spv::OpKill && opcode != spv::OpTerminateInvocation,
59+
"runtime image loop continuation includes a terminating path: label=" +
60+
std::to_string(label) + " opcode=" + std::to_string(opcode));
61+
}
62+
}
63+
device.Dispatch(shader, 1u, 1u, 1u);
64+
device.WaitIdle();
65+
AgcDriver::Graphics::StorageTexture::FlushPending(imageAddress, sizeof(texels), nullptr, "test");
66+
device.WaitIdle();
67+
constexpr std::array expected{1u, 3u, 6u, 0xdeadbeefu};
68+
for (std::uint32_t row = 0u; row < expected.size(); ++row) {
69+
for (std::uint32_t lane = 0u; lane < 64u; ++lane) {
70+
Require(texels[row * 64u + lane] == expected[row], "runtime image loop did not preserve all three stores and exit: wave=" +
71+
std::to_string(waveSize) + " row=" + std::to_string(row) + " lane=" + std::to_string(lane) + " actual=" +
72+
std::to_string(texels[row * 64u + lane]) + " expected=" + std::to_string(expected[row]));
73+
}
74+
}
75+
}
76+
1777
void Run(AgcDriver::VulkanDevice& device) {
1878
ShaderRecompiler::CompiledShaderArtifact first;
1979
const std::array formats{20u, 21u, 22u, 34u};
@@ -50,6 +110,8 @@ int main() {
50110
try {
51111
const auto device = OpenVulkanTestDevice();
52112
if (!device) return VulkanTestSkipped;
113+
RunLoop(*device, 32u);
114+
RunLoop(*device, 64u);
53115
Run(*device);
54116
std::cout << "runtime image and sampler tests passed\n";
55117
return 0;

‎core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ struct SpirvDeferredPhiPatch {
205205

206206
struct StructuredFunctionState {
207207
std::unordered_map<const IrBlock*, std::uint32_t> blockExitLabels;
208+
std::unordered_map<const IrBlock*, std::vector<std::size_t>> loopMergeOffsets;
208209
std::vector<SpirvDeferredPhiPatch> deferredPhis;
209210
};
210211

‎core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvModule.hpp‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ class SpirvModule {
5757
void AddFunction(std::span<const std::uint32_t> words);
5858
void BeginHelperFunction();
5959
void EndHelperFunction();
60+
[[nodiscard]] std::size_t AddLoopMerge(std::uint32_t merge, std::uint32_t continuation);
61+
void PatchLoopContinue(std::size_t offset, std::uint32_t continuation);
62+
[[nodiscard]] std::size_t FunctionInstructionOffset() const { return functionInstructions.size(); }
63+
[[nodiscard]] bool FunctionHasTerminationSince(std::size_t offset) const;
6064
[[nodiscard]] SpirvDeferredPhi AddDeferredPhi(std::uint32_t type, std::uint32_t result, std::size_t incomingCount);
6165
void PatchDeferredPhi(SpirvDeferredPhi phi, std::size_t incoming, std::uint32_t value, std::uint32_t parent);
6266

‎core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp‎

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -185,15 +185,15 @@ std::uint32_t EmitBranchCondition(SpirvValueEmitContext& ctx, const BlockInfo& i
185185
return result;
186186
}
187187

188-
void EmitStructuredTerminator(SpirvValueEmitContext& ctx, const IrProgram& program, const BlockInfo& info) {
188+
void EmitStructuredTerminator(SpirvValueEmitContext& ctx, StructuredFunctionState& functionState, const IrProgram& program, const BlockInfo& info) {
189189
auto& state = ctx.state;
190190
const Terminator& term = info.terminator;
191191
const auto emitMerge = [&]() {
192192
if (term.loopHeader) {
193193
const IrBlock* merge = TargetBlock(program, term.mergeBlock);
194194
const IrBlock* cont = TargetBlock(program, term.continueBlock);
195195
if (merge != nullptr && cont != nullptr) {
196-
state.module.AddFunction(spv::OpLoopMerge, ctx.Label(merge), ctx.Label(cont), spv::LoopControlMaskNone);
196+
functionState.loopMergeOffsets[cont].push_back(state.module.AddLoopMerge(ctx.Label(merge), ctx.Label(cont)));
197197
}
198198
} else if (term.kind == TerminatorKind::ConditionalBranch && term.mergeBlock != InvalidControlFlowId) {
199199
if (const IrBlock* merge = TargetBlock(program, term.mergeBlock); merge != nullptr) {
@@ -832,9 +832,17 @@ void EmitControlFlow(SpirvValueEmitContext& context, StructuredFunctionState& fu
832832
const bool stops = state.bdaStopsInvocations;
833833
state.continueTarget = IsContinueTarget(program, info->id);
834834
state.bdaStopsInvocations = stops && !state.continueTarget;
835+
const auto instructionOffset = state.module.FunctionInstructionOffset();
835836
EmitStructuredBlock(context, functionState, block);
837+
if (const auto merges = functionState.loopMergeOffsets.find(block);
838+
merges != functionState.loopMergeOffsets.end() && state.module.FunctionHasTerminationSince(instructionOffset)) {
839+
const auto continuation = state.module.AllocateId();
840+
for (const auto offset : merges->second) state.module.PatchLoopContinue(offset, continuation);
841+
state.module.AddFunction(spv::OpBranch, continuation);
842+
EmitLabel(state, continuation);
843+
}
836844
functionState.blockExitLabels.emplace(block, state.currentLabel);
837-
EmitStructuredTerminator(context, program, *info);
845+
EmitStructuredTerminator(context, functionState, program, *info);
838846
state.bdaStopsInvocations = stops;
839847
state.continueTarget = false;
840848
}

‎core/shader/recompiler/SpirvBackend/src/SpirvModule.cpp‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,39 @@ void SpirvModule::appendString(std::vector<std::uint32_t>& words, const std::str
230230
}
231231
}
232232

233+
std::size_t SpirvModule::AddLoopMerge(std::uint32_t merge, std::uint32_t continuation) {
234+
const auto offset = functionInstructions.size();
235+
AddFunction(spv::OpLoopMerge, merge, continuation, spv::LoopControlMaskNone);
236+
return offset;
237+
}
238+
239+
void SpirvModule::PatchLoopContinue(std::size_t offset, std::uint32_t continuation) {
240+
if (offset > functionInstructions.size() || functionInstructions.size() - offset < 4u ||
241+
functionInstructions[offset] != ((4u << spv::WordCountShift) | spv::OpLoopMerge) || continuation == 0u) {
242+
throw std::runtime_error("SpirvModule::PatchLoopContinue received an invalid loop merge or a zero id");
243+
}
244+
functionInstructions[offset + 2u] = continuation;
245+
}
246+
247+
bool SpirvModule::FunctionHasTerminationSince(std::size_t offset) const {
248+
if (offset > functionInstructions.size()) throw std::runtime_error("invalid function instruction offset");
249+
bool boundary = offset == functionInstructions.size();
250+
bool terminates = false;
251+
for (std::size_t cursor = 0u; cursor < functionInstructions.size();) {
252+
const auto count = functionInstructions[cursor] >> spv::WordCountShift;
253+
if (count == 0u || count > functionInstructions.size() - cursor) throw std::runtime_error("invalid function instruction length");
254+
boundary |= cursor == offset;
255+
const auto opcode = functionInstructions[cursor] & spv::OpCodeMask;
256+
if (cursor >= offset) {
257+
terminates |= opcode == spv::OpUnreachable || opcode == spv::OpReturn || opcode == spv::OpReturnValue ||
258+
opcode == spv::OpKill || opcode == spv::OpTerminateInvocation;
259+
}
260+
cursor += count;
261+
}
262+
if (!boundary) throw std::runtime_error("function instruction offset is not an instruction boundary");
263+
return terminates;
264+
}
265+
233266
SpirvDeferredPhi SpirvModule::AddDeferredPhi(std::uint32_t type, std::uint32_t result, std::size_t incomingCount) {
234267
std::vector<std::uint32_t> words = {spv::OpPhi, type, result};
235268
words.resize(words.size() + incomingCount * 2u);

0 commit comments

Comments
 (0)