shader: validate the IR once, at SPIR-V emission

This commit is contained in:
nmzik committed 2026-10-30 10:12:37 +01:00
1 parent 216bd28bf6
commit 4919fdb6e6
7 files changed
+47 -18

No files matched your search

@@ -219,7 +219,7 @@ std::vector<uint32_t> EmitProgram(IR::Program& program, ShaderStageInputInfo inp
!program.shader_info_complete) { !program.shader_info_complete) {
Fail(program, "SPIR-V emitter requires a fully planned native shader program"); Fail(program, "SPIR-V emitter requires a fully planned native shader program");
} }
IR::ValidateProgram(program, true); IR::ValidateProgram(program);
program.bindings = {.push_data_start_dword = push_data_start_dword}; program.bindings = {.push_data_start_dword = push_data_start_dword};
EmitterState state(program, input_info, profile); EmitterState state(program, input_info, profile);
if (emit_debug_info) { if (emit_debug_info) {
@@ -1446,7 +1446,6 @@ IR::Program TranslateProgram(const Decoder::Program& decoded, const CFG::Graph&
} }
translator.AddBranchCondition(cfg, cfg_block, *block); translator.AddBranchCondition(cfg, cfg_block, *block);
} }
IR::ValidateProgram(result, false);
return result; return result;
} }
@@ -244,7 +244,7 @@ bool HasShaderMemoryWrites(const Program& program) {
return false; return false;
} }
void ValidateProgram(const Program& program, bool require_ssa) { void ValidateProgram(const Program& program) {
if (program.blocks.size() != program.block_storage.size()) { if (program.blocks.size() != program.block_storage.size()) {
return Fail("value IR block storage is inconsistent"); return Fail("value IR block storage is inconsistent");
} }
@@ -400,7 +400,7 @@ void ValidateProgram(const Program& program, bool require_ssa) {
} else { } else {
saw_non_phi = true; saw_non_phi = true;
} }
if (require_ssa && IsRegisterStatePseudo(inst.GetOpcode())) { if (IsRegisterStatePseudo(inst.GetOpcode())) {
return Fail(fmt::format("register-state pseudo {} survived SSA rewrite", return Fail(fmt::format("register-state pseudo {} survived SSA rewrite",
ValueOpcodeName(inst.GetOpcode()))); ValueOpcodeName(inst.GetOpcode())));
} }
+1 -1
View File
@@ -751,7 +751,7 @@ uint32_t StorageBufferElementBits(const Program& program, const MemoryInfo& memo
std::string ProgramToString(const Program& program); std::string ProgramToString(const Program& program);
bool HasShaderMemoryWrites(const Program& program); bool HasShaderMemoryWrites(const Program& program);
void ValidateProgram(const Program& program, bool require_ssa); void ValidateProgram(const Program& program);
bool EquivalentValue(const ResourcePlan& program, Value left, Value right); bool EquivalentValue(const ResourcePlan& program, Value left, Value right);
bool EquivalentSelectorDomain(const ResourcePlan& program, bool EquivalentSelectorDomain(const ResourcePlan& program,
const DescriptorSource::IndirectDescriptor& left, const DescriptorSource::IndirectDescriptor& left,
+3 -3
View File
@@ -319,7 +319,7 @@ void TestInvariantIndirectImageMaterialization(bool nested) {
fixture->PlanAndTrack(); fixture->PlanAndTrack();
auto plan = ExtractResourcePlan(fixture->program); auto plan = ExtractResourcePlan(fixture->program);
EliminateDeadCode(fixture->program.blocks); EliminateDeadCode(fixture->program.blocks);
ValidateProgram(fixture->program, true); ValidateProgram(fixture->program);
const auto &source = *plan.descriptor_sources[plan.info.images[0].source].indirect_descriptor; const auto &source = *plan.descriptor_sources[plan.info.images[0].source].indirect_descriptor;
Check(!source.selector && fixture->program.info.buffers.size() == (nested ? 3u : 2u) && Check(!source.selector && fixture->program.info.buffers.size() == (nested ? 3u : 2u) &&
std::ranges::any_of(*fixture->block, [](const Inst &inst) { std::ranges::any_of(*fixture->block, [](const Inst &inst) {
@@ -7589,7 +7589,7 @@ ResourcePlan ConditionalSamplerPlan(bool diamond, bool reverse, bool reverse_phi
Check(sampler_words[0].ResolveInstruction()->GetOpcode() == ValueOpcode::Phi, Check(sampler_words[0].ResolveInstruction()->GetOpcode() == ValueOpcode::Phi,
"host descriptor selection changed the GPU Phi"); "host descriptor selection changed the GPU Phi");
EliminateDeadCode(fixture.program.blocks); EliminateDeadCode(fixture.program.blocks);
ValidateProgram(fixture.program, true); ValidateProgram(fixture.program);
return ExtractResourcePlan(fixture.program); return ExtractResourcePlan(fixture.program);
} }
@@ -7855,7 +7855,7 @@ void TestFiniteImageWaterfall() {
fixture->PlanAndTrack(); fixture->PlanAndTrack();
const auto plan = ExtractResourcePlan(fixture->program); const auto plan = ExtractResourcePlan(fixture->program);
EliminateDeadCode(fixture->program.blocks); EliminateDeadCode(fixture->program.blocks);
ValidateProgram(fixture->program, true); ValidateProgram(fixture->program);
const auto& finite = plan.descriptor_sources[plan.info.images[0].source].indirect_descriptor; const auto& finite = plan.descriptor_sources[plan.info.images[0].source].indirect_descriptor;
Check(finite && finite->sources.size() == 3u, Check(finite && finite->sources.size() == 3u,
"waterfall image selection mixed descriptor words or lost a candidate"); "waterfall image selection mixed descriptor words or lost a candidate");
+3 -3
View File
@@ -788,7 +788,7 @@ void TestCompleteWaveReduction() {
fixture.BlockAt().terminator.kind = Terminator::Branch; fixture.BlockAt().terminator.kind = Terminator::Branch;
fixture.BlockAt().terminator.true_block = &fixture.BlockAt(1); fixture.BlockAt().terminator.true_block = &fixture.BlockAt(1);
} }
ValidateProgram(fixture.program, true); ValidateProgram(fixture.program);
// The pipeline folds full-EXEC predicates to true before eliminating reads. // The pipeline folds full-EXEC predicates to true before eliminating reads.
const uint32_t wave_size = test == Case::Wave32 || rows ? 32u : 64u; const uint32_t wave_size = test == Case::Wave32 || rows ? 32u : 64u;
ConstantPropagationPass(fixture.program.blocks, wave_size); ConstantPropagationPass(fixture.program.blocks, wave_size);
@@ -812,7 +812,7 @@ void TestCompleteWaveReduction() {
} }
Check(updates == 2 && preserved.Instruction()->Arg(0).Instruction()->HasUses(), Check(updates == 2 && preserved.Instruction()->Arg(0).Instruction()->HasUses(),
"wave reduction destroyed an independently consumed prefix or kept its dead suffix"); "wave reduction destroyed an independently consumed prefix or kept its dead suffix");
ValidateProgram(fixture.program, true); ValidateProgram(fixture.program);
} else { } else {
Check(use.Instruction()->Arg(0).Resolve() == result, Check(use.Instruction()->Arg(0).Resolve() == result,
"rejected wave reduction proof changed its original result"); "rejected wave reduction proof changed its original result");
@@ -873,7 +873,7 @@ void TestControlFlowValueSurvivesReadLaneFolding() {
Check(entry_info.condition == Value(true), Check(entry_info.condition == Value(true),
"folded branch condition did not survive identity removal"); "folded branch condition did not survive identity removal");
ValidateProgram(fixture.program, true); ValidateProgram(fixture.program);
} }
void TestDeadPhiCyclesAndPlanningRoots() { void TestDeadPhiCyclesAndPlanningRoots() {
+37 -7
View File
@@ -12778,7 +12778,7 @@ void TestTypedEntryStateIsMinimal() {
IR::RewriteToSsa(values.blocks); IR::RewriteToSsa(values.blocks);
IR::RemoveIdentities(values); IR::RemoveIdentities(values);
IR::EliminateDeadCode(values.blocks); IR::EliminateDeadCode(values.blocks);
IR::ValidateProgram(values, true); IR::ValidateProgram(values);
}; };
check(32u); check(32u);
@@ -12799,13 +12799,42 @@ void TestFinalSsaRejectsRegisterStatePseudos() {
} else { } else {
ir.Emit(ValueOpcode::ReferenceU32, {ir.GetScalarReg(ScalarReg{0})}); ir.Emit(ValueOpcode::ReferenceU32, {ir.GetScalarReg(ScalarReg{0})});
} }
ExpectFatal([&] { ValidateProgram(program, true); }, ExpectFatal([&] { ValidateProgram(program); },
"final SSA accepted a register-state pseudo operation"); "final SSA accepted a register-state pseudo operation");
}; };
check_rejected(false); check_rejected(false);
check_rejected(true); check_rejected(true);
} }
void TestEmitterValidatesFinalProgram() {
using namespace ShaderRecompiler;
const uint32_t shader[] = {
EncodeMubuf0(0x1c, 0, false), // buffer_store_dword v0
EncodeMubuf1(0, 12, 0),
0xbf810000u,
};
const auto options = MakeCompileOptions(ShaderType::Compute);
auto result = RecompileForTest(shader, options);
// Only emission validates, so a definition moved below its same-block user must be fatal.
bool moved = false;
for (auto *block : result.program.blocks) {
auto &list = block->Instructions();
for (auto user = list.begin(); !moved && user != list.end(); ++user) {
for (size_t arg = 0; !moved && arg < user->NumArgs(); ++arg) {
const auto *def = user->Arg(arg).TryInstruction();
if (def == nullptr || def->Parent() != block) continue;
const auto where = std::ranges::find_if(
list, [&](const IR::Inst &inst) { return &inst == def; });
list.splice(std::next(user), list, where);
moved = true;
}
}
}
Check(moved, "emitter fixture has no same-block definition");
ExpectFatal([&] { Spirv::EmitProgram(result.program, options.input_info); },
"SPIR-V emission accepted a use before its definition");
}
#endif #endif
void TestSsaRewriteSealsFilledBlocks() { void TestSsaRewriteSealsFilledBlocks() {
@@ -12888,7 +12917,7 @@ void TestValuePhiValidation() {
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS #if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
const auto expect_invalid = [](const Program &program, const char *text) { const auto expect_invalid = [](const Program &program, const char *text) {
ExpectFatal([&] { ValidateProgram(program, true); }, text); ExpectFatal([&] { ValidateProgram(program); }, text);
}; };
#endif #endif
const auto validate = [&](InvalidPhi invalid) { const auto validate = [&](InvalidPhi invalid) {
@@ -12940,7 +12969,7 @@ void TestValuePhiValidation() {
} }
if (invalid == InvalidPhi::None) { if (invalid == InvalidPhi::None) {
ValidateProgram(program, true); ValidateProgram(program);
} }
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS #if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
else { else {
@@ -13306,7 +13335,7 @@ void TestLaneMaskProjection() {
gated_raw_consumer.Resolve().TryInstruction()->Arg(0).Resolve() == gated_mask.Resolve(), gated_raw_consumer.Resolve().TryInstruction()->Arg(0).Resolve() == gated_mask.Resolve(),
"mask projection changed a raw scalar mask consumer"); "mask projection changed a raw scalar mask consumer");
RemoveIdentities(program); RemoveIdentities(program);
ValidateProgram(program, true); ValidateProgram(program);
} }
} }
@@ -13376,7 +13405,7 @@ void TestLateConstantPropagation() {
"late constants did not reach Phi, nested mask patterns or new logical nodes"); "late constants did not reach Phi, nested mask patterns or new logical nodes");
RemoveIdentities(program); RemoveIdentities(program);
Check(exit->condition == Value(true), "late constant lost its control-flow use"); Check(exit->condition == Value(true), "late constant lost its control-flow use");
ValidateProgram(program, true); ValidateProgram(program);
} }
} }
@@ -13586,7 +13615,7 @@ void TestNativeWideValueValidation() {
} }
}; };
const auto check_rejected = [](const Program &program, const char *text) { const auto check_rejected = [](const Program &program, const char *text) {
ExpectFatal([&] { ValidateProgram(program, true); }, text); ExpectFatal([&] { ValidateProgram(program); }, text);
}; };
{ {
@@ -17404,6 +17433,7 @@ int main() {
TestTypedEntryStateIsMinimal(); TestTypedEntryStateIsMinimal();
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS #if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
TestFinalSsaRejectsRegisterStatePseudos(); TestFinalSsaRejectsRegisterStatePseudos();
TestEmitterValidatesFinalProgram();
#endif #endif
TestSsaRewriteSealsFilledBlocks(); TestSsaRewriteSealsFilledBlocks();
TestValuePhiValidation(); TestValuePhiValidation();