From a69f5e0371a4183cc515d52ebb9cc3836e280a63 Mon Sep 17 00:00:00 2001 From: Niko Date: Tue, 13 Oct 2026 02:19:59 +0200 Subject: [PATCH] test(recompiler): compare stored resource plans field by field The plan tests compared a decoded plan with the original only through the codec itself (re-encoding must give the same bytes) and through materialization. Neither catches a field that EncodePlan and DecodePlan both leave out: the bytes still agree, and the walk does not read every field. Leaving userDataBase, the plan's sRGB formats, the value ids or several MemoryInfo fields (secondaryOffset, dataDwords, dataFormat, planningOnly, idxen) out of both sides passed every test. The hand-built plan also never materializes (its uniform values cannot be evaluated), so its 1000 runtimes all failed with the same error, and no plan held an immediate wider than 32 bits. samePlan compares every field of two plans without the codec: the value graph by storage index (opcode, type, id, flags, register, immediate, arguments, phi blocks), the block ids, the roots, and the plain data through the structures' own equality. The round trip now requires it for the 14 fixtures and the hand-built plan, which sets every MemoryInfo field, all indirect table fields, a second buffer, the whole uniform fill and a 64-bit immediate to values other than their defaults. The second process also builds each fixture's plan with its own front end and requires it to equal the plan it used, so a stored plan must match what the front end builds in another process, not only its own re-encoding. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/tests/ShaderDiskCache.cpp | 140 ++++++++++++++++-- 1 file changed, 126 insertions(+), 14 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index 848a268bf..abc30222b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -1275,18 +1275,58 @@ IrResourcePlan handBuiltPlan() { memory.imageDimension = RdnaImageDimension::Dim2D; memory.gpuDescriptor = true; memory.flushDenormals = true; - plan.memoryInfo = {memory, MemoryInfo{}}; + MemoryInfo every; + every.kind = ResourceKind::Buffer; + every.resource = 2u; + every.sampler = 3u; + every.offset = 0x14u; + every.secondaryOffset = 0x28u; + every.dmask = 0x5u; + every.dataDwords = 3u; + every.dataBits = 16u; + every.componentIndex = 1u; + every.componentCount = 2u; + every.dataFormat = 10u; + every.numberFormat = 7u; + every.imageSampleFlags = 0x21u; + every.imageDimension = RdnaImageDimension::Dim3D; + every.imageAddressComponents = 3u; + every.addressIsFull = true; + every.dataSigned = true; + every.typed = true; + every.formatted = true; + every.d16 = true; + every.imageHasMip = true; + every.imageR128 = true; + every.imagePacked = true; + every.imageByElements = 2u; + every.idxen = true; + every.offen = true; + every.planningOnly = true; + every.coherent = true; + every.gpuDescriptor = true; + every.flushDenormals = true; + plan.memoryInfo = {memory, every, MemoryInfo{}}; DescriptorSource buffer; buffer.dwordCount = 4u; buffer.dwords = {&readConst(1), &readConst(2), &phi, &pointer}; DescriptorSource table; table.dwordCount = 8u; table.dwords[0] = &readConst(3); - table.indirectImage = DescriptorSource::IndirectImage{1u, 0u, 16u, 4u, 2u, 32u, true}; - plan.descriptorSources = {buffer, table}; - plan.controlFlow = {{&condition, {1u, 2u}, {0u}, {}}, {nullptr, {2u}, {}, {0u}}, {nullptr, {}, {}, {}}}; - plan.materializationSources = {0u}; - plan.srtReads = {{&readConst(4), 0u}, {&pointer, 1u}}; + table.indirectImage = DescriptorSource::IndirectImage{3u, 2u, 16u, 4u, 2u, 32u, true}; + DescriptorSource heap; + heap.dwordCount = 4u; + heap.dwords[1] = &readConst(6); + DescriptorSource material; + material.dwordCount = 4u; + material.dwords[2] = &readConst(7); + auto& wide = make(IrOpcode::Void, IrType::U64); + wide.SetImmediateU64(0x8000000000000001ull); + material.dwords[3] = &wide; + plan.descriptorSources = {buffer, table, heap, material}; + plan.controlFlow = {{&condition, {1u, 2u}, {0u}, {}}, {nullptr, {2u}, {3u}, {0u}}, {nullptr, {}, {}, {}}}; + plan.materializationSources = {0u, 2u}; + plan.srtReads = {{&readConst(4), 2u}, {&pointer, 1u}}; plan.guardedSrtSlots = {1u}; plan.srtGuardOffset = 6u; plan.cleanFlatSlots = {0u, 1u}; @@ -1294,14 +1334,14 @@ IrResourcePlan handBuiltPlan() { plan.requiresSpecializationMemory = true; plan.srtPlanComplete = true; plan.resourceTrackingComplete = true; - plan.info.buffers = {BufferResource{}}; + plan.info.buffers = {BufferResource{}, BufferResource{2u, 0x40u, 0x100u, 0u, true, true, true, true, true, 0x7u, true, 4u}}; ImageResource image{}; image.source = 1u; image.resourceClass = ImageResourceClass::Sampled; image.dimension = RdnaImageDimension::Dim2D; plan.info.images = {image}; ResourceMaterializer::PrepareImageModes(plan.info); - plan.uniformFill.fill = {UniformFillKind::Buffer, 0u, {4u, 0u, 0u}, 2u, 0u}; + plan.uniformFill.fill = {UniformFillKind::Buffer, 1u, {4u, 8u, 12u}, 2u, 0x3f800000u}; auto& fillValue = readConst(5); plan.uniformFill.values = {&fillValue, &fillValue, nullptr, nullptr}; return plan; @@ -1313,6 +1353,71 @@ std::vector encodedPlan(const IrResourcePlan& plan) { return bytes; } +bool samePlan(const IrResourcePlan& left, const IrResourcePlan& right) { + if (left.valueStorage.size() != right.valueStorage.size() || left.blockStorage.size() != right.blockStorage.size()) return false; + std::map leftValues; + std::map rightValues; + std::map leftBlocks; + std::map rightBlocks; + for (std::size_t index = 0; index < left.valueStorage.size(); ++index) { + leftValues.emplace(left.valueStorage[index].get(), index); + rightValues.emplace(right.valueStorage[index].get(), index); + } + for (std::size_t index = 0; index < left.blockStorage.size(); ++index) { + leftBlocks.emplace(left.blockStorage[index].get(), index); + rightBlocks.emplace(right.blockStorage[index].get(), index); + } + const auto sameValue = [&](const IrValue* a, const IrValue* b) { + if (a == nullptr || b == nullptr) return a == b; + const auto foundA = leftValues.find(a); + const auto foundB = rightValues.find(b); + return foundA != leftValues.end() && foundB != rightValues.end() && foundA->second == foundB->second; + }; + const auto sameBlock = [&](const IrBlock* a, const IrBlock* b) { + const auto foundA = leftBlocks.find(a); + const auto foundB = rightBlocks.find(b); + return foundA != leftBlocks.end() && foundB != rightBlocks.end() && foundA->second == foundB->second; + }; + for (std::size_t index = 0; index < left.blockStorage.size(); ++index) { + if (left.blockStorage[index]->Id() != right.blockStorage[index]->Id()) return false; + } + for (std::size_t index = 0; index < left.valueStorage.size(); ++index) { + const auto& a = *left.valueStorage[index]; + const auto& b = *right.valueStorage[index]; + if (a.Opcode() != b.Opcode() || a.Type() != b.Type() || a.Id() != b.Id() || a.Flags() != b.Flags() || !(a.Register() == b.Register()) || a.HasImmediate() != b.HasImmediate() || (a.HasImmediate() && a.ImmediateU64() != b.ImmediateU64())) return false; + if (a.ArgumentCount() != b.ArgumentCount() || a.IsPhi() != b.IsPhi() || a.PhiBlockCount() != b.PhiBlockCount()) return false; + for (std::size_t operand = 0; operand < a.ArgumentCount(); ++operand) { + if (!sameValue(a.Argument(operand), b.Argument(operand))) return false; + } + for (std::size_t operand = 0; operand < a.PhiBlockCount(); ++operand) { + if (!sameBlock(a.PhiBlock(operand), b.PhiBlock(operand))) return false; + } + } + if (left.stage != right.stage || left.shaderHash != right.shaderHash || left.userDataBase != right.userDataBase || left.userDataCount != right.userDataCount || left.srgbDecodeFormats != right.srgbDecodeFormats) return false; + if (left.memoryInfo != right.memoryInfo || left.descriptorSources.size() != right.descriptorSources.size() || left.controlFlow.size() != right.controlFlow.size() || left.srtReads.size() != right.srtReads.size()) return false; + for (std::size_t index = 0; index < left.descriptorSources.size(); ++index) { + const auto& a = left.descriptorSources[index]; + const auto& b = right.descriptorSources[index]; + if (a.dwordCount != b.dwordCount || a.indirectImage != b.indirectImage) return false; + for (std::size_t dword = 0; dword < a.dwords.size(); ++dword) { + if (!sameValue(a.dwords[dword], b.dwords[dword])) return false; + } + } + for (std::size_t index = 0; index < left.controlFlow.size(); ++index) { + const auto& a = left.controlFlow[index]; + const auto& b = right.controlFlow[index]; + if (!sameValue(a.condition, b.condition) || a.successors != b.successors || a.sources != b.sources || a.images != b.images) return false; + } + for (std::size_t index = 0; index < left.srtReads.size(); ++index) { + if (!sameValue(left.srtReads[index].value, right.srtReads[index].value) || left.srtReads[index].flatOffset != right.srtReads[index].flatOffset) return false; + } + for (std::size_t index = 0; index < left.uniformFill.values.size(); ++index) { + if (!sameValue(left.uniformFill.values[index], right.uniformFill.values[index])) return false; + } + return left.materializationSources == right.materializationSources && left.guardedSrtSlots == right.guardedSrtSlots && left.srtGuardOffset == right.srtGuardOffset && left.cleanFlatSlots == right.cleanFlatSlots && left.pureFlatSlots == right.pureFlatSlots && + left.requiresSpecializationMemory == right.requiresSpecializationMemory && left.srtPlanComplete == right.srtPlanComplete && left.resourceTrackingComplete == right.resourceTrackingComplete && left.info == right.info && left.uniformFill.fill == right.uniformFill.fill; +} + struct MaterializedPlan { bool ok = false; std::string error; @@ -1448,10 +1553,10 @@ void verifyMalformedPlans() { }; rejects("a branch with one successor", [](IrResourcePlan& plan) { plan.controlFlow[0].successors.pop_back(); }); rejects("a successor past the blocks", [](IrResourcePlan& plan) { plan.controlFlow[1].successors[0] = 3u; }); - rejects("a block source past the sources", [](IrResourcePlan& plan) { plan.controlFlow[0].sources[0] = 2u; }); + rejects("a block source past the sources", [](IrResourcePlan& plan) { plan.controlFlow[0].sources[0] = 4u; }); rejects("a block image past the images", [](IrResourcePlan& plan) { plan.controlFlow[1].images[0] = 1u; }); - rejects("a materialized source past the sources", [](IrResourcePlan& plan) { plan.materializationSources.push_back(2u); }); - rejects("a buffer past the sources", [](IrResourcePlan& plan) { plan.info.buffers[0].source = 2u; }); + rejects("a materialized source past the sources", [](IrResourcePlan& plan) { plan.materializationSources.push_back(4u); }); + rejects("a buffer past the sources", [](IrResourcePlan& plan) { plan.info.buffers[0].source = 4u; }); rejects("an image past the sources", [](IrResourcePlan& plan) { plan.info.images[0].source = 5u; }); rejects("a descriptor of nine dwords", [](IrResourcePlan& plan) { plan.descriptorSources[1].dwordCount = 9u; }); rejects("a uniform fill of five words", [](IrResourcePlan& plan) { plan.uniformFill.fill.words = 5u; }); @@ -1483,7 +1588,7 @@ void verifyPlanRoundTrip() { const auto& phi = *handDecoded.descriptorSources[0].dwords[2]; require(phi.IsPhi() && phi.PhiBlockCount() == 2u && phi.PhiBlock(0)->Id() == 3u && phi.PhiBlock(1)->Id() == 7u && phi.Argument(1)->ImmediateU32() == 0u, "the decoded phi lost its blocks or operands"); require(handDecoded.uniformFill.values[0] == handDecoded.uniformFill.values[1] && handDecoded.uniformFill.values[0] != nullptr && handDecoded.uniformFill.values[2] == nullptr, "the decoded uniform fill lost its shared or null values"); - require(handDecoded.descriptorSources[1].indirectImage == handBuilt.descriptorSources[1].indirectImage && handDecoded.memoryInfo == handBuilt.memoryInfo && handDecoded.info == handBuilt.info, "the decoded plan lost plain data"); + require(samePlan(handBuilt, handDecoded), "the decoded hand-built plan differs from the plan in a field or a value"); PlanFixture handFixture; handFixture.userData = {static_cast(PlanSrtAddress), static_cast(PlanSrtAddress >> 32u), 0u, 0u}; std::vector handSrt(16, 0u); @@ -1504,6 +1609,7 @@ void verifyPlanRoundTrip() { IrResourcePlan decoded; require(ShaderDiskCache::DecodePlan(bytes, decoded), fixture.name + ": the plan does not decode"); require(encodedPlan(decoded) == bytes, fixture.name + ": the plan changed in a round trip"); + require(samePlan(*plan, decoded), fixture.name + ": the decoded plan differs from the plan in a field or a value"); require(decoded.valueStorage.size() == plan->valueStorage.size() && decoded.blockStorage.size() == plan->blockStorage.size(), fixture.name + ": the decoded plan holds another graph"); const auto succeeded = verifyRoundTripRuntimes(*plan, decoded, fixture, 0x5eedu + static_cast(index), fixture.name); require(succeeded != 0u, fixture.name + ": no runtime materialized"); @@ -1595,7 +1701,7 @@ void appendSnapshot(std::vector& out, const ResourceCapture& capture) appendWords(out, reads); } -std::vector> preparedOutputs(std::vector& fixtures) { +std::vector> preparedOutputs(std::vector& fixtures, std::vector>* plans = nullptr) { std::vector> outputs; for (auto& fixture : fixtures) { const auto request = fixture.Request(true); @@ -1608,6 +1714,7 @@ std::vector> preparedOutputs(std::vector& fi ShaderDiskCache::EncodeResult(*result, bytes); appendSnapshot(bytes, *capture); require(ShaderDiskCache::EncodePlan(*capture->plan, bytes), fixture.name + ": the captured plan does not encode"); + if (plans != nullptr) plans->push_back(capture->plan); outputs.push_back(std::move(bytes)); } return outputs; @@ -1627,7 +1734,8 @@ int runPlanLoadingProcess(std::string_view mode) { if (mode == "--load-plan-switch") setEnvironment("APS5_TRACE_BINDLESS", "1"); auto fixtures = planFixtures(); const auto count = fixtures.size(); - const auto outputs = preparedOutputs(fixtures); + std::vector> plans; + const auto outputs = preparedOutputs(fixtures, &plans); ShaderDiskCache::Flush(); const auto totals = ShaderDiskCache::Totals(); const auto builds = FrontEndBuilds(); @@ -1659,6 +1767,10 @@ int runPlanLoadingProcess(std::string_view mode) { } else { std::cout << "a damaged plan entry was rejected, rebuilt and stored again with the same output\n"; } + for (std::size_t index = 0; index < count; ++index) { + const auto fresh = GetResourcePlan(fixtures[index].Request(false)); + require(samePlan(*plans[index], *fresh), fixtures[index].name + ": the plan this process used differs from the plan its front end builds"); + } ShaderDiskCache::Flush(); return 0; }