diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp index f4444a8083bd..2a455c681e3e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp @@ -114,6 +114,7 @@ std::shared_ptr Driver::decodeDraw(const QueueState& queue, const Su product->pixel = Graphics::DecodePixelStageInfo(queue.context, Graphics::ExportMappings(product->state), Graphics::PixelProgramSkipped(queue)); product->pixel.targetExportPacking = Graphics::ExportPackings(product->state); product->pixel.dualSourceBlend = product->state.dualSourceBlend; + if (product->pixel.ancillary) product->pixel.rasterPrimitive = Graphics::RasterPrimitiveOf(product->state); return product; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp index 090b9b10dbea..c352dc495cc6 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp @@ -68,6 +68,7 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr(value)); mix(pixel.dualSourceBlend); + mix(static_cast(pixel.rasterPrimitive)); std::lock_guard cacheLock(drawCacheMutex); ++drawEntryCounters.lookups; const auto found = drawCache.find(drawKey); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp index 1759f417ebd6..1cfdcf606a84 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp @@ -105,7 +105,7 @@ bool Driver::sameDecode(const DrawDecode& a, const DrawDecode& b) { const auto& y = b.programs[i]; if (x.binary.stage != y.binary.stage || x.binary.codeAddress != y.binary.codeAddress || x.userDataBase != y.userDataBase || x.firstUserSgpr != y.firstUserSgpr || x.userData != y.userData || x.snapshot != y.snapshot || x.codeOffset != y.codeOffset) return false; } - if (p.targetExportPacking != q.targetExportPacking || p.dualSourceBlend != q.dualSourceBlend) return false; + if (p.targetExportPacking != q.targetExportPacking || p.dualSourceBlend != q.dualSourceBlend || p.rasterPrimitive != q.rasterPrimitive) return false; return true; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index 05cb27663e88..80a7febe0791 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -113,6 +113,7 @@ ShaderStages DecodeShaderStages(const QueueState& queue); State DecodeState(const QueueState& queue); std::array ExportMappings(const State& state); std::array ExportPackings(const State& state); +ShaderRecompiler::RasterPrimitive RasterPrimitiveOf(const State& state); ColorTarget DecodeColorBuffer(const Registers& context, std::uint32_t slot); std::size_t CmaskBytes(std::uint32_t width, std::uint32_t height); std::uint32_t ColorWriteMask(const Registers& context); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index f29320922a1a..4ed9e64851e4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -782,6 +782,16 @@ std::array ExportPackings(const State& return packings; } +ShaderRecompiler::RasterPrimitive RasterPrimitiveOf(const State& state) { + if (state.rectList) return ShaderRecompiler::RasterPrimitive::Rectangle; + if (state.stages.mesh.has_value() || state.stages.tessellation.has_value()) return ShaderRecompiler::RasterPrimitive::Triangle; + switch (state.topology) { + case VK_PRIMITIVE_TOPOLOGY_POINT_LIST: return ShaderRecompiler::RasterPrimitive::Point; + case VK_PRIMITIVE_TOPOLOGY_LINE_LIST: return ShaderRecompiler::RasterPrimitive::Line; + default: return ShaderRecompiler::RasterPrimitive::Triangle; + } +} + std::array ExportMappings(const State& state) { std::array mappings{}; mappings.fill(0xe4u); diff --git a/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp b/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp index 119f8792511b..17c06cd64a40 100644 --- a/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp @@ -77,6 +77,22 @@ struct Builder { return threadBit(low, high); } + std::pair ballotWords(IrValue& predicate) { + auto& ballot = emit(IrOpcode::Ballot, IrType::U32x4, {&predicate}); + return {&emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&ballot, &constant(0u)}), &emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&ballot, &constant(1u)})}; + } + + IrValue& wordAnd(IrValue& left, IrValue& right) { + return emit(IrOpcode::BitwiseAnd32, IrType::U32, {&left, &right}); + } + + IrValue& phi(IrType type, IrBlock& predecessor, IrValue& incoming) { + auto& created = program.CreateValue(IrOpcode::Phi, type); + block->AppendInstruction(&created); + created.AddPhiOperand(&predecessor, &incoming); + return created; + } + void keep(IrValue& value) { (void)emit(IrOpcode::ReferenceU32, IrType::Void, {&value}); } @@ -162,6 +178,29 @@ int main() { return b.eliminate() == 2u && removed(first) && removed(second) && !removed(sink); }); + passed &= run("a write read through thousands of lane-local values still loses its select", [] { + Builder b; + auto& exec = b.mask(16u); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + IrValue* value = &written; + for (std::uint32_t step = 0; step < 6000u; ++step) value = &b.add(*value, step); + b.keep(b.select(exec, *value, old)); + return b.eliminate() == 1u && removed(written); + }); + + passed &= run("thousands of lane-local values ending in a wider read keep the select", [] { + Builder b; + auto& wide = b.mask(32u); + auto& exec = b.logicalAnd(wide, b.mask(16u)); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + IrValue* value = &written; + for (std::uint32_t step = 0; step < 6000u; ++step) value = &b.add(*value, step); + b.keep(b.select(wide, *value, old)); + return b.eliminate() == 0u && !removed(written); + }); + passed &= run("cross-lane reads must keep the select", [] { bool ok = true; for (const IrOpcode opcode : {IrOpcode::ReadFirstLane, IrOpcode::DppMoveU32, IrOpcode::BpermuteU32, IrOpcode::Permlane16U32, IrOpcode::SwizzleU32}) { @@ -275,6 +314,121 @@ int main() { return count == 0u && !gone; }); + const auto mergedExec = [](bool wideBranch, bool loopMerge) { + Builder b; + auto& wide = b.mask(48u); + auto& exec = b.logicalAnd(wide, b.mask(32u)); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + IrBlock& head = *b.block; + IrBlock& body = b.newBlock(); + IrBlock& merge = b.newBlock(); + head.AddBranch(&body); + head.AddBranch(&merge); + body.AddBranch(&merge); + b.block = &body; + auto& killed = b.logicalAnd(wideBranch ? wide : exec, b.mask(16u)); + auto& phi = b.program.CreateValue(IrOpcode::Phi, IrType::Bool); + merge.AppendInstruction(&phi); + phi.AddPhiOperand(&head, &exec); + phi.AddPhiOperand(&body, &killed); + if (loopMerge) { + IrBlock& exit = b.newBlock(); + merge.AddBranch(&merge); + merge.AddBranch(&exit); + phi.AddPhiOperand(&merge, &phi); + b.block = &exit; + } else { + b.block = &merge; + } + b.keep(b.select(phi, b.add(written, 2u), old)); + return std::pair{b.eliminate(), removed(written)}; + }; + + passed &= run("an exec merged by a phi whose every incoming exec implies the write's exec drops the select", [&] { + const auto [count, gone] = mergedExec(false, false); + return count == 1u && gone; + }); + + passed &= run("an exec merged by a phi with a wider incoming exec keeps the select", [&] { + const auto [count, gone] = mergedExec(true, false); + return count == 0u && !gone; + }); + + passed &= run("an exec carried around a loop whose every incoming exec implies the write's exec drops the select", [&] { + const auto [count, gone] = mergedExec(false, true); + return count == 1u && gone; + }); + + passed &= run("an exec carried around a loop with a wider incoming exec keeps the select", [&] { + const auto [count, gone] = mergedExec(true, true); + return count == 0u && !gone; + }); + + const auto mergedWords = [](bool wideBranch) { + Builder b; + auto& wide = b.mask(48u); + auto& exec = b.logicalAnd(wide, b.mask(40u)); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + auto& saved = b.emit(IrOpcode::Ballot, IrType::U32x4, {&exec}); + auto& savedLow = b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&saved, &b.constant(0u)}); + auto& savedHigh = b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&saved, &b.constant(1u)}); + IrBlock& head = *b.block; + IrBlock& body = b.newBlock(); + IrBlock& merge = b.newBlock(); + head.AddBranch(&body); + head.AddBranch(&merge); + body.AddBranch(&merge); + b.block = &body; + auto& survived = b.emit(IrOpcode::Ballot, IrType::U32x4, {&b.logicalAnd(wideBranch ? wide : exec, b.mask(24u))}); + auto& survivedLow = b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&survived, &b.constant(0u)}); + auto& survivedHigh = b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&survived, &b.constant(1u)}); + auto& low = b.program.CreateValue(IrOpcode::Phi, IrType::U32); + auto& high = b.program.CreateValue(IrOpcode::Phi, IrType::U32); + merge.AppendInstruction(&low); + merge.AppendInstruction(&high); + low.AddPhiOperand(&head, &savedLow); + low.AddPhiOperand(&body, &survivedLow); + high.AddPhiOperand(&head, &savedHigh); + high.AddPhiOperand(&body, &survivedHigh); + b.block = &merge; + b.keep(b.select(b.threadBit(low, high), b.add(written, 2u), old)); + return std::pair{b.eliminate(), removed(written)}; + }; + + passed &= run("an exec rebuilt from mask words merged by phis implies the write's exec", [&] { + const auto [count, gone] = mergedWords(false); + return count == 1u && gone; + }); + + passed &= run("an exec rebuilt from merged mask words with a wider incoming ballot keeps the select", [&] { + const auto [count, gone] = mergedWords(true); + return count == 0u && !gone; + }); + + const auto chained = [](bool wideRoot) { + Builder b; + auto& wide = b.mask(48u); + auto& exec = b.logicalAnd(wide, b.mask(32u)); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + IrValue* later = wideRoot ? &wide : &exec; + for (std::uint32_t kill = 0; kill < 24u; ++kill) later = &b.logicalAnd(*later, b.mask(31u - kill)); + b.keep(b.select(*later, b.add(written, 2u), old)); + return std::pair{b.eliminate(), removed(written)}; + }; + + passed &= run("an exec narrowed by a long chain of kills still implies the write's exec", [&] { + const auto [count, gone] = chained(false); + return count == 1u && gone; + }); + + passed &= run("a long chain of kills rooted at a wider exec keeps the select", [&] { + const auto [count, gone] = chained(true); + return count == 0u && !gone; + }); + passed &= run("a write in a loop must keep its select", [] { Builder b; auto& exec = b.mask(16u); @@ -292,9 +446,10 @@ int main() { return b.eliminate() == 0u && !removed(written); }); - passed &= run("a write read inside a later loop must keep its select", [] { + const auto laterLoop = [](bool wideRead) { Builder b; - auto& exec = b.mask(16u); + auto& wide = b.mask(48u); + auto& exec = b.logicalAnd(wide, b.mask(16u)); auto& old = b.lane(); auto& written = b.select(exec, b.add(old, 1u), old); IrBlock& head = *b.block; @@ -302,11 +457,142 @@ int main() { head.AddBranch(&loop); loop.AddBranch(&loop); b.block = &loop; - b.keep(b.select(exec, b.add(written, 2u), old)); + b.keep(b.select(wideRead ? wide : exec, b.add(written, 2u), old)); + return std::pair{b.eliminate(), removed(written)}; + }; + + passed &= run("a write read inside a later loop under its own exec loses its select", [&] { + const auto [count, gone] = laterLoop(false); + return count == 1u && gone; + }); + + passed &= run("a write read inside a later loop under a wider exec keeps its select", [&] { + const auto [count, gone] = laterLoop(true); + return count == 0u && !gone; + }); + + enum class Latch { Narrowed, Revived }; + const auto carried = [](Latch latch, bool readInside) { + Builder b; + auto& wide = b.mask(48u); + auto& exec = b.logicalAnd(wide, b.mask(40u)); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + const auto [savedLow, savedHigh] = b.ballotWords(exec); + IrBlock& entry = *b.block; + IrBlock& header = b.newBlock(); + IrBlock& exit = b.newBlock(); + entry.AddBranch(&header); + header.AddBranch(&header); + header.AddBranch(&exit); + b.block = &header; + auto& low = b.phi(IrType::U32, entry, *savedLow); + auto& high = b.phi(IrType::U32, entry, *savedHigh); + auto& value = b.phi(IrType::U32, entry, written); + auto& loopExec = b.threadBit(low, high); + auto& next = b.select(loopExec, b.add(value, 2u), value); + if (readInside) b.keep(b.select(loopExec, b.add(value, 3u), old)); + if (latch == Latch::Narrowed) { + const auto [stayLow, stayHigh] = b.ballotWords(b.mask(24u)); + low.AddPhiOperand(&header, &b.wordAnd(low, *stayLow)); + high.AddPhiOperand(&header, &b.wordAnd(high, *stayHigh)); + } else { + const auto [revivedLow, revivedHigh] = b.ballotWords(wide); + low.AddPhiOperand(&header, revivedLow); + high.AddPhiOperand(&header, revivedHigh); + } + value.AddPhiOperand(&header, &next); + b.block = &exit; + b.keep(b.select(b.threadBit(*savedLow, *savedHigh), b.add(value, 4u), old)); + return std::pair{b.eliminate(), removed(written)}; + }; + + passed &= run("a write carried through a loop whose exec is saved, narrowed and restored loses its select", [&] { + const auto [count, gone] = carried(Latch::Narrowed, false); + return count == 1u && gone; + }); + + passed &= run("a write read inside a loop under the loop's narrowed exec loses its select", [&] { + const auto [count, gone] = carried(Latch::Narrowed, true); + return count == 1u && gone; + }); + + passed &= run("a lane that becomes active again inside a loop and reads the write keeps the select", [&] { + const auto [count, gone] = carried(Latch::Revived, true); + return count == 0u && !gone; + }); + + passed &= run("a lane that becomes active again inside a loop but never reads the write lets the select go", [&] { + const auto [count, gone] = carried(Latch::Revived, false); + return count == 1u && gone; + }); + + passed &= run("an exec proven only by assuming a loop phi is forgotten once that phi turns out wider", [] { + Builder b; + auto& wide = b.mask(48u); + auto& exec = b.logicalAnd(wide, b.mask(40u)); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + const auto [savedLow, savedHigh] = b.ballotWords(exec); + IrBlock& entry = *b.block; + IrBlock& header = b.newBlock(); + IrBlock& kill = b.newBlock(); + IrBlock& revive = b.newBlock(); + IrBlock& latch = b.newBlock(); + IrBlock& exit = b.newBlock(); + entry.AddBranch(&header); + header.AddBranch(&kill); + header.AddBranch(&revive); + kill.AddBranch(&latch); + revive.AddBranch(&latch); + latch.AddBranch(&header); + latch.AddBranch(&exit); + b.block = &header; + auto& low = b.phi(IrType::U32, entry, *savedLow); + auto& high = b.phi(IrType::U32, entry, *savedHigh); + (void)b.select(b.threadBit(low, high), written, old); + b.block = &kill; + const auto [stayLow, stayHigh] = b.ballotWords(b.mask(24u)); + auto& killedLow = b.wordAnd(low, *stayLow); + auto& killedHigh = b.wordAnd(high, *stayHigh); + b.keep(b.select(b.threadBit(killedLow, killedHigh), written, old)); + b.block = &revive; + const auto [revivedLow, revivedHigh] = b.ballotWords(wide); + b.block = &latch; + auto& mergedLow = b.phi(IrType::U32, kill, killedLow); + auto& mergedHigh = b.phi(IrType::U32, kill, killedHigh); + mergedLow.AddPhiOperand(&revive, revivedLow); + mergedHigh.AddPhiOperand(&revive, revivedHigh); + low.AddPhiOperand(&latch, &mergedLow); + high.AddPhiOperand(&latch, &mergedHigh); return b.eliminate() == 0u && !removed(written); }); - const auto sample = [](IrShaderStage stage, std::uint32_t sampleFlags) { + passed &= run("a write mask computed inside an earlier loop does not let that loop's phis stand for it", [] { + Builder b; + auto& old = b.lane(); + IrBlock& entry = *b.block; + IrBlock& header = b.newBlock(); + IrBlock& exit = b.newBlock(); + entry.AddBranch(&header); + header.AddBranch(&header); + header.AddBranch(&exit); + b.block = &header; + auto& low = b.phi(IrType::U32, entry, b.constant(0u)); + auto& high = b.phi(IrType::U32, entry, b.constant(0u)); + auto& bound = b.phi(IrType::U32, entry, b.constant(64u)); + auto& exec = b.emit(IrOpcode::ULessThan32, IrType::Bool, {&b.lane(), &bound}); + const auto [stayLow, stayHigh] = b.ballotWords(exec); + low.AddPhiOperand(&header, stayLow); + high.AddPhiOperand(&header, stayHigh); + bound.AddPhiOperand(&header, &b.emit(IrOpcode::ISub32, IrType::U32, {&bound, &b.constant(8u)})); + b.block = &exit; + auto& written = b.select(exec, b.add(old, 1u), old); + b.keep(b.select(b.threadBit(low, high), b.add(written, 2u), old)); + return b.eliminate() == 0u && !removed(written); + }); + + const auto sample = [](IrShaderStage stage, std::uint32_t sampleFlags, IrOpcode opcode = IrOpcode::ImageSampleRaw) { Builder b; b.program.Resources().stage = stage; MemoryInfo memory; @@ -322,7 +608,7 @@ int main() { MemoryFlags flags{0u, 0u}; std::uint64_t bits = 0; std::memcpy(&bits, &flags, sizeof(flags)); - auto& sampled = b.emit(IrOpcode::ImageSampleRaw, IrType::U32x4, {&image, &sampler, &address}, bits); + auto& sampled = b.emit(opcode, IrType::U32x4, {&image, &sampler, &address}, bits); b.keep(b.select(exec, b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&sampled, &b.constant(0u)}), old)); b.eliminate(); return removed(written); @@ -336,6 +622,26 @@ int main() { return sample(IrShaderStage::Pixel, RdnaImageSampleFlagLevelZero) && sample(IrShaderStage::Mesh, 0u); }); + passed &= run("a pixel gather without an LOD of its own takes it from the quad and keeps the select", [&] { + return !sample(IrShaderStage::Pixel, 0u, IrOpcode::ImageGatherRaw) && !sample(IrShaderStage::Pixel, RdnaImageSampleFlagBias, IrOpcode::ImageGatherRaw); + }); + + passed &= run("a gather with an explicit LOD reads only its lane and drops the select", [&] { + return sample(IrShaderStage::Pixel, RdnaImageSampleFlagLevelZero, IrOpcode::ImageGatherRaw) && sample(IrShaderStage::Pixel, RdnaImageSampleFlagLod, IrOpcode::ImageGatherRaw) && + sample(IrShaderStage::Mesh, 0u, IrOpcode::ImageGatherRaw); + }); + + passed &= run("an image size query reads only its lane's level and drops the select", [] { + Builder b; + auto& exec = b.mask(16u); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + auto& address = b.emit(IrOpcode::MakeImageAddress, IrType::ImageAddress, {&written}); + auto& size = b.emit(IrOpcode::ImageQueryDimensions, IrType::U32x4, {&b.emit(IrOpcode::GetImageResource, IrType::ImageResource, {}), &address}); + b.keep(b.select(exec, b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&size, &b.constant(0u)}), old)); + return b.eliminate() == 1u && removed(written); + }); + const auto guarded = [](IrOpcode opcode, bool sameExec, bool asActive) { Builder b; auto& wide = b.mask(48u); @@ -350,6 +656,19 @@ int main() { case IrOpcode::LoadBufferU32: b.keep(b.select(exec, b.emit(opcode, IrType::U32, {&b.emit(IrOpcode::GetBufferResource, IrType::BufferResource, {}), &written, &b.constant(0u), &b.constant(0u), &active}), old)); break; + case IrOpcode::ImageRead: { + auto& address = b.emit(IrOpcode::MakeImageAddress, IrType::ImageAddress, {&written, &old}); + auto& image = b.emit(IrOpcode::GetImageResource, IrType::ImageResource, {}); + auto& read = b.emit(opcode, IrType::U32x4, {&image, &address, &active}); + b.keep(b.select(exec, b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&read, &b.constant(0u)}), old)); + break; + } + case IrOpcode::ImageWrite: { + auto& address = b.emit(IrOpcode::MakeImageAddress, IrType::ImageAddress, {&old, &old}); + auto& texel = b.emit(IrOpcode::CompositeConstructU32x4, IrType::U32x4, {&written, &old, &old, &old}); + b.emit(opcode, IrType::Void, {&b.emit(IrOpcode::GetImageResource, IrType::ImageResource, {}), &address, &texel, &active}); + break; + } default: b.emit(opcode, IrType::Void, {&b.emit(IrOpcode::CompositeConstructU32x4, IrType::U32x4, {&written, &old, &old, &old}), &active}); break; @@ -359,11 +678,15 @@ int main() { }; passed &= run("a store, load or export whose exec implies the write's exec drops the select", [&] { - return guarded(IrOpcode::WriteSharedU32, true, false) && guarded(IrOpcode::LoadBufferU32, true, false) && guarded(IrOpcode::SetAttribute, true, false); + return guarded(IrOpcode::WriteSharedU32, true, false) && guarded(IrOpcode::LoadBufferU32, true, false) && guarded(IrOpcode::ImageRead, true, false) && guarded(IrOpcode::SetAttribute, true, false); }); passed &= run("a store, load or export under a wider exec keeps the select", [&] { - return !guarded(IrOpcode::WriteSharedU32, false, false) && !guarded(IrOpcode::LoadBufferU32, false, false) && !guarded(IrOpcode::SetAttribute, false, false); + return !guarded(IrOpcode::WriteSharedU32, false, false) && !guarded(IrOpcode::LoadBufferU32, false, false) && !guarded(IrOpcode::ImageRead, false, false) && !guarded(IrOpcode::SetAttribute, false, false); + }); + + passed &= run("an image write under the write's exec drops the select, under a wider exec or as its exec keeps it", [&] { + return guarded(IrOpcode::ImageWrite, true, false) && !guarded(IrOpcode::ImageWrite, false, false) && !guarded(IrOpcode::ImageWrite, true, true); }); passed &= run("a value used as a store's exec keeps the select", [&] { diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index 0aeb9425e42e..374e895e43ee 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -230,6 +230,7 @@ CompiledVariant sampleVariant() { info.info.hasBitwiseXor = true; info.info.usesDma = true; info.info.usesFaultBuffer = true; + info.info.rasterPrimitive = true; info.bindings.pushDataStartDword = 2; info.bindings.memoryOffsetDword = 1; info.bindings.memoryOffsetCount = 5; diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index c1c8977ac01b..9aa0a135ee02 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -1372,13 +1372,15 @@ void verifyPixelRequestSerialization() { minimal.shader = {ShaderStage::Fragment, 0x30000u, code, 0, {}}; minimal.context.waveSize = 64; minimal.context.pixel = ShaderPixelStageInfo{}; + minimal.context.pixel->rasterPrimitive = RasterPrimitive::Rectangle; const auto encoded = serializer.Serialize(minimal); - require(requestPrefix(encoded, 8u) == "NVNQQQ8AAAA=", "new requests did not use serialization version 15"); + require(requestPrefix(encoded, 8u) == "NVNQQRAAAAA=", "new requests did not use serialization version 16"); + require(serializer.Deserialize(encoded).request.context.pixel->rasterPrimitive == RasterPrimitive::Rectangle, "the pixel primitive class did not round-trip"); constexpr std::size_t mappingOffset = 8u + 37u + 18u + 163u; - for (std::size_t bytes = 0; bytes < 17u; ++bytes) { - expectFailure([&] { static_cast(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing or dual-source flag was accepted"); + for (std::size_t bytes = 0; bytes < 18u; ++bytes) { + expectFailure([&] { static_cast(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source flag or primitive class was accepted"); } - for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQRAAAAA="}) { + for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQREAAAA="}) { expectFailure([&] { static_cast(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted"); } } @@ -1467,6 +1469,7 @@ void verifyLegacyPixelRequests() { require(pixel.targetExportMapping == std::array{}, "legacy pixel mapping no longer defaults to zero"); require(pixel.targetExportPacking == std::array{}, "a legacy request packed a color export"); require(!pixel.dualSourceBlend, "a legacy request enabled dual-source blending"); + require(pixel.rasterPrimitive == RasterPrimitive::Triangle, "a legacy request drew something other than triangles"); require(pixel.inputAddr == 2u && pixel.hasPerspectiveCenterVgpr && pixel.targetOutputMode[0] == 9u, "legacy pixel layout was misread"); require(pixel.conservativeZExport == (version >= 7u ? ConservativeZExport::GreaterThanZ : ConservativeZExport::AnyZ), "legacy conservative Z layout was misread"); require(!pixel.orderedPixelShader, "a legacy request became a primitive-ordered pixel shader"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PixelAncillaryWord.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PixelAncillaryWord.cpp new file mode 100644 index 000000000000..559a514ed707 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PixelAncillaryWord.cpp @@ -0,0 +1,306 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 16; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t Rgba32FloatInfo = 0x8738u; +constexpr std::uint32_t LinearAttrib3 = 0x09000000u; +constexpr std::uint32_t Float32AbgrExport = 9; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +constexpr std::array, 3> Triangle{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} +}}; + +alignas(256) std::array, 5> Programs{}; +std::size_t programCount = 0; + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +std::span Program(std::uint32_t ancillaryVgpr, bool wholeWord) { + Require(programCount < Programs.size(), "too many test programs"); + auto& code = Programs[programCount++]; + std::size_t at = 0; + if (wholeWord) { + code[at++] = 0x36000000u | (20u << 17u) | (ancillaryVgpr << 9u) | 0xffu; + code[at++] = 0x0000ffffu; + code[at++] = 0x2c000000u | (21u << 17u) | (ancillaryVgpr << 9u) | 0x90u; + } else { + code[at++] = 0xd5480000u | 20u; + code[at++] = (0x84u << 18u) | (0x88u << 9u) | (256u + ancillaryVgpr); + code[at++] = 0x7e000280u | (21u << 17u); + } + code[at++] = 0x7e000c00u | (20u << 17u) | (256u + 20u); + code[at++] = 0x7e000c00u | (21u << 17u) | (256u + 21u); + code[at++] = 0x7e000280u | (22u << 17u); + code[at++] = 0x7e0002f2u | (23u << 17u); + code[at++] = 0xf800180fu; + code[at++] = 0x17161514u; + code[at++] = 0xbf810000u; + return std::span(code.data(), at); +} + +struct Block { + explicit Block(std::size_t bytes) : bytes(bytes), watched(AgcDriver::GuestMemory::WriteWatched()) { +#ifdef _WIN32 + if (watched) { + data = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes)); + GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes); + } else { + data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + } +#else + if (watched) { + void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot map the color target"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + BlockBytes - 1) & ~(static_cast(BlockBytes) - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast(aligned + bytes), begin + BlockBytes - aligned); + data = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes); + } else { + data = static_cast(std::aligned_alloc(BlockBytes, bytes)); + } +#endif + Require(data != nullptr, "cannot allocate the color target"); + Require(!watched || AgcDriver::GuestMemory::Watched(Address(), bytes), "the color target is not write-watched"); + GuestAllocations::Mutation mutation; + mutation.Add(data, bytes, true, true, true); + } + ~Block() { + AgcDriver::Graphics::StorageTexture::FlushPending(Address(), bytes, nullptr, "test release"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(data); + } +#ifdef _WIN32 + if (watched) { + GuestArena::GuestArenaReset_nid_postfix(data, bytes); + GuestArena::GuestArenaRelease_nid_postfix(data, bytes); + } else { + VirtualFree(data, 0, MEM_RELEASE); + } +#else + if (watched) { + munmap(data, bytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes); + } else { + std::free(data); + } +#endif + } + Block(const Block&) = delete; + Block& operator=(const Block&) = delete; + std::uint64_t Address() const { return reinterpret_cast(data); } + std::size_t bytes; + bool watched; + std::uint8_t* data = nullptr; +}; + +AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block, std::uint32_t info) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(block.Address() >> 8u); + cx[0x31b] = 0; + cx[0x31c] = info; + cx[0x31d] = 0; + cx[0x323] = 0; + cx[0x324] = 0; + cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u); + cx[0x3b8] = LinearAttrib3; + cx[0x390] = static_cast(block.Address() >> 40u); + auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + color.exportIndex = 0; + return color; +} + +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, std::uint32_t inputAddr, ShaderRecompiler::RasterPrimitive primitive, std::span pixelCode) { + const auto target = device.Target(); + constexpr std::uint32_t waveSize = 64; + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(Triangle.data(), 16u, static_cast(Triangle.size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.inputAddr = inputAddr; + pixel.ancillary = true; + pixel.rasterPrimitive = primitive; + pixel.targetOutputMode.at(0) = Float32AbgrExport; + pixel.targetExportMapping.fill(0xe4u); + const std::vector pixelUserData(8, 0u); + const std::array pixelMemory{{{reinterpret_cast(pixelCode.data()), std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, + {waveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + + VkPipelineColorBlendAttachmentState blend{}; + blend.blendEnable = VK_FALSE; + blend.colorWriteMask = 0xf; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + state.colors = {color}; + state.color = color; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blends = {blend}; + state.blend = blend; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); +} + +std::vector ReadBack(AgcDriver::VulkanDevice& device, std::uint64_t address, std::size_t bytes) { + AgcDriver::Graphics::StorageTexture::FlushPending(address, bytes, nullptr, "test read-back"); + device.WaitIdle(); + std::vector stored(bytes); + AgcDriver::GuestMemory::Read(address, std::as_writable_bytes(std::span(stored)), 1); + return stored; +} + +void Expect(AgcDriver::VulkanDevice& device, std::uint32_t inputAddr, std::uint32_t ancillaryVgpr, ShaderRecompiler::RasterPrimitive primitive, const std::string& what) { + constexpr std::size_t bytes = std::size_t{Width} * Height * 16u; + Require(AgcDriver::Graphics::ColorTargetLayout(Width, Height, AgcDriver::Graphics::ColorTileMode::Linear, 16).Bytes() == bytes, what + ": the linear target is padded"); + Block block(bytes); + std::fill(block.data, block.data + bytes, std::uint8_t{0xff}); + const auto color = DecodeTarget(block, Rgba32FloatInfo); + Require(color.format == VK_FORMAT_R32G32B32A32_SFLOAT && color.elementBytes == 16 && color.bytes == bytes, what + ": the target decoded to the wrong format"); + Draw(device, color, inputAddr, primitive, Program(ancillaryVgpr, true)); + const auto stored = ReadBack(device, block.Address(), bytes); + const auto word = static_cast(primitive); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + std::array texel{}; + std::memcpy(texel.data(), stored.data() + (static_cast(y) * Width + x) * 16u, sizeof(texel)); + const std::array expected{static_cast(word & 0xffffu), static_cast(word >> 16u), 0.0f, 1.0f}; + Require(texel == expected, what + ": pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") holds (" + std::to_string(texel[0]) + ", " + std::to_string(texel[1]) + ", " + std::to_string(texel[2]) + ", " + std::to_string(texel[3]) + ")"); + } + } +} + +std::uint64_t PixelModule(const ShaderRecompiler::SpirvTarget& target, std::span code, ShaderRecompiler::RasterPrimitive primitive) { + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary); + pixel.ancillary = true; + pixel.rasterPrimitive = primitive; + pixel.targetOutputMode.at(0) = Float32AbgrExport; + pixel.targetExportMapping.fill(0xe4u); + const std::vector userData(8, 0u); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Fragment, reinterpret_cast(code.data()), code, 0, {}}, + {64, 0, userData, std::nullopt, pixel, std::nullopt, memory}, + target, + PixelPushLayout(0, target) + }; + return ShaderRecompiler::Recompile(request).PipelineVariantId(); +} + +void ExpectModules(const ShaderRecompiler::SpirvTarget& target, bool wholeWord, const std::string& what) { + using ShaderRecompiler::RasterPrimitive; + const auto code = Program(0u, wholeWord); + const auto triangle = PixelModule(target, code, RasterPrimitive::Triangle); + Require(PixelModule(target, code, RasterPrimitive::Triangle) == triangle, what + ": the same primitive class built a second module"); + for (const auto primitive : {RasterPrimitive::Line, RasterPrimitive::Rectangle}) { + Require((PixelModule(target, code, primitive) != triangle) == wholeWord, what + (wholeWord ? ": a line or rectangle draw reused the triangle module" : ": a line or rectangle draw built its own module")); + } +} + +void ExpectPrimitive(VkPrimitiveTopology topology, bool rectList, bool tessellated, ShaderRecompiler::RasterPrimitive expected, const std::string& what) { + AgcDriver::Graphics::State state{}; + state.topology = topology; + state.rectList = rectList; + if (tessellated) state.stages.tessellation = ShaderRecompiler::TessellationConfiguration{}; + Require(AgcDriver::Graphics::RasterPrimitiveOf(state) == expected, what + ": wrong primitive class"); +} + +} + +int main() { + try { + using ShaderRecompiler::PixelInput; + using ShaderRecompiler::PixelInputBit; + using ShaderRecompiler::RasterPrimitive; + ExpectPrimitive(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, false, false, RasterPrimitive::Triangle, "triangle strip"); + ExpectPrimitive(VK_PRIMITIVE_TOPOLOGY_LINE_LIST, false, false, RasterPrimitive::Line, "line list"); + ExpectPrimitive(VK_PRIMITIVE_TOPOLOGY_POINT_LIST, false, false, RasterPrimitive::Point, "point list"); + ExpectPrimitive(VK_PRIMITIVE_TOPOLOGY_PATCH_LIST, true, false, RasterPrimitive::Rectangle, "rect list"); + ExpectPrimitive(VK_PRIMITIVE_TOPOLOGY_PATCH_LIST, false, true, RasterPrimitive::Triangle, "tessellated patches"); + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Expect(*device, PixelInputBit(PixelInput::Ancillary), 0u, RasterPrimitive::Triangle, "ANCILLARY alone, triangle"); + Expect(*device, PixelInputBit(PixelInput::PerspectiveCenter) | PixelInputBit(PixelInput::Ancillary), 2u, RasterPrimitive::Rectangle, "ANCILLARY after the perspective center, rectangle"); + Expect(*device, PixelInputBit(PixelInput::Ancillary), 0u, RasterPrimitive::Line, "ANCILLARY alone, line"); + ExpectModules(device->Target(), true, "whole word"); + ExpectModules(device->Target(), false, "sample field only"); + std::puts("pixel ancillary word tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/tests/PixelAncillary.cpp b/core/libs/tests/PixelAncillary.cpp index b7be6fc810c8..c6fa84df1466 100644 --- a/core/libs/tests/PixelAncillary.cpp +++ b/core/libs/tests/PixelAncillary.cpp @@ -1,7 +1,9 @@ #include "IntermediateRepresentation/IrBuilder.hpp" #include "Optimization/ConstantFolder.hpp" #include "Optimization/DeadCodeEliminator.hpp" +#include "Optimization/MaskedSelectEliminator.hpp" #include "Optimization/ShaderInfoCollector.hpp" +#include "Recompiler.hpp" #include #include #include @@ -17,6 +19,7 @@ struct Row { std::uint32_t sample; std::uint32_t layer; bool frontFacing; + RasterPrimitive primitive = RasterPrimitive::Triangle; }; constexpr std::array Rows {{{0u, 0u, false}, {0u, 0u, true}, {15u, 0x1FFFu, false}, {15u, 0x1FFFu, true}, {0u, 0x1FFFu, true}, {15u, 0u, false}}}; @@ -152,6 +155,7 @@ static std::uint32_t Evaluate(const IrValue* value, const Row& row) { case StageInputKind::SampleId: return row.sample; case StageInputKind::Layer: return row.layer; case StageInputKind::FrontFacing: return row.frontFacing ? 1u : 0u; + case StageInputKind::RasterPrimitive: return static_cast(row.primitive); default: break; } break; @@ -253,6 +257,237 @@ static void MergedRefused(IrOpcode opcode, std::uint32_t offset, std::uint32_t c } Require(false); } +enum class HelperRead { Export, FullWaveExport, DataTestExport, Ballot }; + +struct HelperProgram { + IrValue* written = nullptr; + IrValue* layer = nullptr; + IrValue* exported = nullptr; +}; + +static HelperProgram HelperOnly(IrProgram& program, HelperRead read, std::uint32_t chain = 0u) { + auto& entry = Begin(program); + auto& body = program.CreateBlock(); + auto& merge = program.CreateBlock(); + program.BlockOrder().push_back(&body); + program.BlockOrder().push_back(&merge); + entry.AddBranch(&body); + entry.AddBranch(&merge); + body.AddBranch(&merge); + IrBuilder builder(program); + builder.SetInsertionPoint(entry); + IrValue& helper = builder.Emit(IrOpcode::GetBuiltin, IrType::U32, {&builder.Constant(static_cast(StageInputKind::HelperInvocation)), &builder.Constant(0u)}); + IrValue& live = builder.IEqual(helper, builder.Constant(0u)); + IrValue& ancillary = AncillaryBuiltin(builder); + IrValue& layer = builder.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&ancillary, &builder.Constant(16u), &builder.Constant(11u)}); + IrValue& target = builder.Select(live, layer, builder.Constant(0u)); + IrValue& tested = builder.LogicalAnd(live, FrontFacingBuiltin(builder)); + IrValue& written = builder.Select(read == HelperRead::DataTestExport ? tested : live, builder.Constant(SelectOther), ancillary); + builder.SetInsertionPoint(body); + IrValue& squared = builder.Emit(IrOpcode::FPMul32, IrType::F32, {&builder.BitCastF32(written), &builder.BitCastF32(written)}); + IrValue& killed = builder.Select(tested, builder.BitCastU32(squared), written); + IrValue& exec = program.CreateValue(IrOpcode::Phi, IrType::Bool); + IrValue& value = program.CreateValue(IrOpcode::Phi, IrType::U32); + merge.AppendInstruction(&exec); + merge.AppendInstruction(&value); + exec.AddPhiOperand(&entry, &live); + exec.AddPhiOperand(&body, &tested); + value.AddPhiOperand(&entry, &written); + value.AddPhiOperand(&body, &killed); + builder.SetInsertionPoint(merge); + IrValue* shaded = &value; + for (std::uint32_t step = 0; step < chain; ++step) shaded = &builder.IAdd(*shaded, helper); + IrValue& data = builder.Emit(IrOpcode::CompositeConstructU32x4, IrType::U32x4, {shaded, &target, &builder.Constant(0u), &builder.Constant(0u)}); + program.Metadata().exportInfo.push_back(ExportInfo {.kind = ExportTargetKind::Mrt, .index = 0u, .en = 0xFu, .done = true, .vm = true}); + IrValue& guard = read == HelperRead::FullWaveExport ? builder.ConstantBool(true) : exec; + static_cast(builder.Emit(IrOpcode::SetAttribute, IrType::Void, {&data, &guard}, ExportFlags {0u, 0u})); + if (read == HelperRead::Ballot) { + IrValue& ballot = builder.Emit(IrOpcode::Ballot, IrType::U32x4, {&builder.INotEqual(value, builder.Constant(0u))}); + static_cast(builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&builder.CompositeExtract(ballot, 0u)})); + } + static_cast(builder.Emit(IrOpcode::Return, IrType::Void, {})); + return HelperProgram {&written, &layer, &value}; +} +static void LowerMasked(IrProgram& program, bool compose = false) { + ConstantFolder().Fold(program); + DeadCodeEliminator().RemoveIdentities(program); + DeadCodeEliminator().Eliminate(program); + static_cast(MaskedSelectEliminator().Eliminate(program)); + DeadCodeEliminator().Eliminate(program); + if (compose && ConstantFolder().ComposePackedAncillary(program)) DeadCodeEliminator().Eliminate(program); + const ShaderPixelInputInfo pixel {}; + ShaderInfoCollector().Collect(program, ShaderStageInputInfo {nullptr, &pixel, nullptr}); +} +static void HelperLanesOnly(std::uint32_t chain = 0u) { + IrProgram program; + const HelperProgram built = HelperOnly(program, HelperRead::Export, chain); + LowerMasked(program, true); + Require(!program.Info().rasterPrimitive); + for (const IrBlock* block : program.BlockOrder()) { + for (const IrValue* inst : block->Instructions()) { + Require(inst->Opcode() != IrOpcode::GetBuiltin || static_cast(inst->Argument(0)->Resolve()->ImmediateU32()) != StageInputKind::PackedAncillary); + } + } + Require(built.written->Parent() == nullptr); + const IrValue* field = built.layer->Argument(0)->Resolve(); + Require(field->Opcode() == IrOpcode::GetBuiltin && static_cast(field->Argument(0)->Resolve()->ImmediateU32()) == StageInputKind::Layer); + const IrValue* entryValue = built.exported->Argument(0)->Resolve(); + Require(entryValue->HasImmediate() && entryValue->ImmediateU32() == SelectOther); +} +static void HelperLanesRefused(HelperRead read, std::uint32_t chain = 0u) { + IrProgram program; + static_cast(HelperOnly(program, read, chain)); + try { + LowerMasked(program); + } catch (const std::runtime_error& error) { + Require(std::string(error.what()).find("unsupported live use") != std::string::npos); + return; + } + Require(false); +} +enum class LoopRead { ExportAfter, ExportInside, UniformLoop, RevivedExportInside }; + +static IrValue& LaneBit(IrBuilder& builder, IrValue& low, IrValue& high) { + IrValue& lane = builder.Emit(IrOpcode::LaneId, IrType::U32, {}); + IrValue& word = builder.Select(builder.Emit(IrOpcode::ULessThan32, IrType::Bool, {&lane, &builder.Constant(32u)}), low, high); + IrValue& shifted = builder.Emit(IrOpcode::ShiftRightLogical32, IrType::U32, {&word, &builder.BitwiseAnd(lane, builder.Constant(31u))}); + return builder.INotEqual(builder.BitwiseAnd(shifted, builder.Constant(1u)), builder.Constant(0u)); +} +static HelperProgram HelperLoop(IrProgram& program, LoopRead read) { + auto& entry = Begin(program); + auto& header = program.CreateBlock(); + auto& exit = program.CreateBlock(); + program.BlockOrder().push_back(&header); + program.BlockOrder().push_back(&exit); + entry.AddBranch(&header); + header.AddBranch(&header); + header.AddBranch(&exit); + IrBuilder builder(program); + builder.SetInsertionPoint(entry); + IrValue& helper = builder.Emit(IrOpcode::GetBuiltin, IrType::U32, {&builder.Constant(static_cast(StageInputKind::HelperInvocation)), &builder.Constant(0u)}); + IrValue& live = builder.IEqual(helper, builder.Constant(0u)); + IrValue& ancillary = AncillaryBuiltin(builder); + IrValue& layer = builder.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&ancillary, &builder.Constant(16u), &builder.Constant(11u)}); + IrValue& target = builder.Select(live, layer, builder.Constant(0u)); + IrValue& written = builder.Select(live, builder.Constant(SelectOther), ancillary); + IrValue& frontFacing = FrontFacingBuiltin(builder); + IrValue& saved = builder.Emit(IrOpcode::Ballot, IrType::U32x4, {&live}); + IrValue& savedLow = builder.CompositeExtract(saved, 0u); + IrValue& savedHigh = builder.CompositeExtract(saved, 1u); + IrValue& low = program.CreateValue(IrOpcode::Phi, IrType::U32); + IrValue& high = program.CreateValue(IrOpcode::Phi, IrType::U32); + IrValue& value = program.CreateValue(IrOpcode::Phi, IrType::U32); + header.AppendInstruction(&low); + header.AppendInstruction(&high); + header.AppendInstruction(&value); + low.AddPhiOperand(&entry, &savedLow); + high.AddPhiOperand(&entry, &savedHigh); + value.AddPhiOperand(&entry, &written); + builder.SetInsertionPoint(header); + IrValue& loopExec = read == LoopRead::UniformLoop ? live : LaneBit(builder, low, high); + IrValue& squared = builder.Emit(IrOpcode::FPMul32, IrType::F32, {&builder.BitCastF32(value), &builder.BitCastF32(value)}); + IrValue& next = builder.Select(loopExec, builder.BitCastU32(squared), value); + IrValue& stay = builder.Emit(IrOpcode::Ballot, IrType::U32x4, {&frontFacing}); + IrValue& stayLow = builder.CompositeExtract(stay, 0u); + IrValue& stayHigh = builder.CompositeExtract(stay, 1u); + const bool revived = read == LoopRead::RevivedExportInside; + low.AddPhiOperand(&header, revived ? &stayLow : &builder.BitwiseAnd(low, stayLow)); + high.AddPhiOperand(&header, revived ? &stayHigh : &builder.BitwiseAnd(high, stayHigh)); + value.AddPhiOperand(&header, &next); + program.Metadata().exportInfo.push_back(ExportInfo {.kind = ExportTargetKind::Mrt, .index = 0u, .en = 0xFu, .done = true, .vm = true}); + const auto exportValue = [&](IrValue& guard) { + IrValue& data = builder.Emit(IrOpcode::CompositeConstructU32x4, IrType::U32x4, {&value, &target, &builder.Constant(0u), &builder.Constant(0u)}); + static_cast(builder.Emit(IrOpcode::SetAttribute, IrType::Void, {&data, &guard}, ExportFlags {0u, 0u})); + }; + if (read == LoopRead::ExportInside || revived) exportValue(loopExec); + builder.SetInsertionPoint(exit); + if (read == LoopRead::ExportAfter) exportValue(LaneBit(builder, savedLow, savedHigh)); + if (read == LoopRead::UniformLoop) exportValue(live); + static_cast(builder.Emit(IrOpcode::Return, IrType::Void, {})); + return HelperProgram {&written, &layer, &value}; +} +static void HelperLoopOnly(LoopRead read) { + IrProgram program; + const HelperProgram built = HelperLoop(program, read); + LowerMasked(program); + for (const IrBlock* block : program.BlockOrder()) { + for (const IrValue* inst : block->Instructions()) { + Require(inst->Opcode() != IrOpcode::GetBuiltin || static_cast(inst->Argument(0)->Resolve()->ImmediateU32()) != StageInputKind::PackedAncillary); + } + } + Require(built.written->Parent() == nullptr); + const IrValue* field = built.layer->Argument(0)->Resolve(); + Require(field->Opcode() == IrOpcode::GetBuiltin && static_cast(field->Argument(0)->Resolve()->ImmediateU32()) == StageInputKind::Layer); + const IrValue* entryValue = built.exported->Argument(0)->Resolve(); + Require(entryValue->HasImmediate() && entryValue->ImmediateU32() == SelectOther); +} +static void HelperLoopRefused(LoopRead read) { + IrProgram program; + static_cast(HelperLoop(program, read)); + try { + LowerMasked(program); + } catch (const std::runtime_error& error) { + Require(std::string(error.what()).find("unsupported live use") != std::string::npos); + return; + } + Require(false); +} +static bool AncillaryRemains(const IrProgram& program) { + for (const IrBlock* block : program.BlockOrder()) { + for (const IrValue* inst : block->Instructions()) { + if (inst->Opcode() == IrOpcode::GetBuiltin && static_cast(inst->Argument(0)->Resolve()->ImmediateU32()) == StageInputKind::PackedAncillary) return true; + } + } + return false; +} +static void ExportedWholeWord() { + IrProgram program; + auto& entry = Begin(program); + auto& body = program.CreateBlock(); + auto& merge = program.CreateBlock(); + program.BlockOrder().push_back(&body); + program.BlockOrder().push_back(&merge); + entry.AddBranch(&body); + entry.AddBranch(&merge); + body.AddBranch(&merge); + IrBuilder builder(program); + builder.SetInsertionPoint(entry); + IrValue& helper = builder.Emit(IrOpcode::GetBuiltin, IrType::U32, {&builder.Constant(static_cast(StageInputKind::HelperInvocation)), &builder.Constant(0u)}); + IrValue& live = builder.IEqual(helper, builder.Constant(0u)); + IrValue& ancillary = AncillaryBuiltin(builder); + IrValue& tested = builder.INotEqual(builder.Select(FrontFacingBuiltin(builder), builder.Constant(7u), builder.Constant(0u)), builder.Constant(0u)); + builder.SetInsertionPoint(body); + IrValue& written = builder.Select(live, builder.Constant(SelectOther), ancillary); + IrValue& carried = program.CreateValue(IrOpcode::Phi, IrType::U32); + merge.AppendInstruction(&carried); + carried.AddPhiOperand(&entry, &ancillary); + carried.AddPhiOperand(&body, &written); + builder.SetInsertionPoint(merge); + const auto channel = [&](float value) -> IrValue& { + IrValue& half = builder.ConstantF32(value); + return builder.Select(tested, builder.Emit(IrOpcode::PackFloat2x16Rtz, IrType::U32, {&half, &half}), carried); + }; + IrValue& data = builder.Emit(IrOpcode::CompositeConstructU32x4, IrType::U32x4, {&channel(0.25f), &channel(0.75f), &builder.Constant(0u), &builder.Constant(0u)}); + program.Metadata().exportInfo.push_back(ExportInfo {.kind = ExportTargetKind::Mrt, .index = 0u, .en = 0xFu, .done = true, .vm = true}); + static_cast(builder.Emit(IrOpcode::SetAttribute, IrType::Void, {&data, &live}, ExportFlags {0u, 0u})); + static_cast(builder.Emit(IrOpcode::Return, IrType::Void, {})); + LowerMasked(program, true); + Require(!AncillaryRemains(program) && program.Info().rasterPrimitive); + const IrValue* word = carried.Argument(0)->Resolve(); + constexpr std::array words {{{0u, 0u, false, RasterPrimitive::Triangle}, {0u, 0u, false, RasterPrimitive::Rectangle}, {0u, 0u, false, RasterPrimitive::Line}, {15u, 0x1FFFu, false, RasterPrimitive::Point}, {3u, 0x7FFu, false, RasterPrimitive::Triangle}}}; + for (const Row& row : words) { + Require(Evaluate(word, row) == (static_cast(row.primitive) | (row.sample << 8u) | (row.layer << 16u))); + } +} +static void WholeWordReplacesRefusal(HelperRead read) { + IrProgram program; + const HelperProgram built = HelperOnly(program, read); + LowerMasked(program, true); + Require(!AncillaryRemains(program) && program.Info().rasterPrimitive); + Require(built.written->Parent() != nullptr); + const Row row {9u, 0x123u, false, RasterPrimitive::Rectangle}; + Require(Evaluate(built.written->Argument(2), row) == (3u | (9u << 8u) | (0x123u << 16u))); +} int main() { Extract(IrOpcode::BitFieldUExtract, 8u, 4u, StageInputKind::SampleId, 0u); Extract(IrOpcode::BitFieldUExtract, 9u, 2u, StageInputKind::SampleId, 1u); @@ -292,4 +527,18 @@ int main() { MergedRefused(IrOpcode::BitwiseOr32, 1u, 0u, false); MergedRefused(IrOpcode::BitFieldUExtract, 12u, 4u, false); MergedRefused(IrOpcode::BitFieldUExtract, 0u, 2u, true); + HelperLanesOnly(); + HelperLanesOnly(6000u); + HelperLanesRefused(HelperRead::FullWaveExport); + HelperLanesRefused(HelperRead::DataTestExport); + HelperLanesRefused(HelperRead::Ballot); + HelperLanesRefused(HelperRead::FullWaveExport, 6000u); + HelperLoopOnly(LoopRead::ExportAfter); + HelperLoopOnly(LoopRead::ExportInside); + HelperLoopOnly(LoopRead::UniformLoop); + HelperLoopRefused(LoopRead::RevivedExportInside); + ExportedWholeWord(); + WholeWordReplacesRefusal(HelperRead::FullWaveExport); + WholeWordReplacesRefusal(HelperRead::DataTestExport); + WholeWordReplacesRefusal(HelperRead::Ballot); } diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index e5b8a2f7c649..18b580a3adb8 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -243,6 +243,14 @@ ColorExportPacking readColorExportPacking(Reader& reader) { return static_cast(value); } +RasterPrimitive readRasterPrimitive(Reader& reader) { + const auto value = reader.ReadU8(); + if (value > static_cast(RasterPrimitive::Rectangle)) { + throw std::runtime_error("RequestSerializer: invalid RasterPrimitive value"); + } + return static_cast(value); +} + void writeShaderBinary(Writer& writer, const ShaderBinary& binary) { writer.WriteU8(static_cast(binary.stage)); writer.WriteU64(binary.codeAddress); @@ -324,6 +332,7 @@ void writePixelInfo(Writer& writer, const ShaderPixelStageInfo& info) { writer.WriteU8(static_cast(value)); } writer.WriteBool(info.dualSourceBlend); + writer.WriteU8(static_cast(info.rasterPrimitive)); } ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { @@ -371,6 +380,7 @@ ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { } } if (version >= 15u) info.dualSourceBlend = reader.ReadBool(); + if (version >= 16u) info.rasterPrimitive = readRasterPrimitive(reader); if (version < 5u) { const auto input = [](PixelInput value, bool present) { return present ? PixelInputBit(value) : 0u; }; info.inputAddr = input(PixelInput::PerspectiveSample, info.hasPerspectiveCenterVgpr && inputAddrOrCenterVgpr == 2u) | input(PixelInput::PerspectiveCenter, info.hasPerspectiveCenterVgpr) | @@ -732,7 +742,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const std::string buffer; Writer writer(buffer); writer.WriteU32(0x41505335u); - writer.WriteU32(15u); + writer.WriteU32(16u); writeShaderBinary(writer, request.shader); writeGuestContext(writer, request.context); writeSpirvTarget(writer, request.target); @@ -759,7 +769,7 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const Reader reader(decoded); if (reader.ReadU32() != 0x41505335u) throw std::runtime_error("invalid recompile request signature"); const auto version = reader.ReadU32(); - if (version < 1u || version > 15u) throw std::runtime_error("unsupported recompile request serialization version"); + if (version < 1u || version > 16u) throw std::runtime_error("unsupported recompile request serialization version"); DeserializedRequest result{}; result.request.shader = readShaderBinary(reader, result.shaderCode, result.shaderHeader); result.request.context = readGuestContext(reader, result, version); diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp index aa1f183eea97..089d299cf7b6 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp @@ -35,6 +35,7 @@ struct ShaderInfo { bool bdaWrites = false; bool usesFaultBuffer = false; bool dispatchThreadLimit = false; + bool rasterPrimitive = false; bool operator==(const ShaderInfo& other) const = default; }; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp index ca9e8fd91e7b..66f8516c9c29 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp @@ -36,6 +36,7 @@ enum class StageInputKind { DispatchThreadLimit, Parameter, HelperInvocation, + RasterPrimitive, }; enum class StageOutputKind { diff --git a/core/shader/recompiler/Optimization/include/Optimization/ConstantFolder.hpp b/core/shader/recompiler/Optimization/include/Optimization/ConstantFolder.hpp index 6be55fb2319a..d2df4ed3cca0 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ConstantFolder.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ConstantFolder.hpp @@ -14,6 +14,8 @@ class ConstantFolder { void Fold(IrProgram& program, std::span blocks) const; + [[nodiscard]] bool ComposePackedAncillary(IrProgram& program) const; + private: [[nodiscard]] bool tryFoldValue(IrProgram& program, IrValue& value) const; }; diff --git a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp index a37056fc7749..78d9c71511ab 100644 --- a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp +++ b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp @@ -243,9 +243,25 @@ std::optional packedField(std::uint32_t offset, std::uint32_t count return static_cast(field - packedFields.begin()); } +bool isPackedAncillary(const IrValue& builtin) { + auto& kind = resolveArg(builtin, 0); + return isImmediate(kind, IrType::U32) && static_cast(kind.ImmediateU32()) == StageInputKind::PackedAncillary; +} + +IrValue& insertBefore(IrProgram& program, IrValue& next, IrOpcode opcode, IrValue& lhs, IrValue& rhs) { + IrValue& created = program.CreateValue(opcode, IrType::U32); + created.AddArgument(&lhs); + created.AddArgument(&rhs); + next.Parent()->InsertInstructionBefore(&next, &created); + return created; +} + +IrValue& insertBuiltin(IrProgram& program, IrBuilder& builder, IrValue& next, StageInputKind kind) { + return insertBefore(program, next, IrOpcode::GetBuiltin, builder.Constant(static_cast(kind)), builder.Constant(0u)); +} + void lowerPackedAncillary(IrProgram& program, IrBuilder& builder, IrValue& ancillary) { - auto& kind = resolveArg(ancillary, 0); - if (!isImmediate(kind, IrType::U32) || static_cast(kind.ImmediateU32()) != StageInputKind::PackedAncillary) { + if (!isPackedAncillary(ancillary)) { return; } const std::vector uses = ancillary.OperandUses(); @@ -291,21 +307,12 @@ void lowerPackedAncillary(IrProgram& program, IrBuilder& builder, IrValue& ancil std::array fields {}; const auto field = [&](std::size_t index) -> IrValue& { if (fields[index] == nullptr) { - const auto builtin = index == 0u ? StageInputKind::SampleId : StageInputKind::Layer; - IrValue& created = program.CreateValue(IrOpcode::GetBuiltin, IrType::U32); - created.AddArgument(&builder.Constant(static_cast(builtin))); - created.AddArgument(&builder.Constant(0u)); - ancillary.Parent()->InsertInstructionBefore(&ancillary, &created); - fields[index] = &created; + fields[index] = &insertBuiltin(program, builder, ancillary, index == 0u ? StageInputKind::SampleId : StageInputKind::Layer); } return *fields[index]; }; const auto emit = [&](IrOpcode opcode, IrValue& lhs, IrValue& rhs) -> IrValue& { - IrValue& created = program.CreateValue(opcode, IrType::U32); - created.AddArgument(&lhs); - created.AddArgument(&rhs); - ancillary.Parent()->InsertInstructionBefore(&ancillary, &created); - return created; + return insertBefore(program, ancillary, opcode, lhs, rhs); }; if (!forwarded.empty()) { IrValue& sample = emit(IrOpcode::ShiftLeftLogical32, field(0u), builder.Constant(8u)); @@ -755,4 +762,24 @@ void ConstantFolder::Fold(IrProgram& program) const { Fold(program, program.BlockOrder()); } +bool ConstantFolder::ComposePackedAncillary(IrProgram& program) const { + std::vector words; + for (IrBlock* block : program.BlockOrder()) { + for (IrValue* inst : block->Instructions()) { + if (inst->Opcode() == IrOpcode::GetBuiltin && inst->HasUses() && isPackedAncillary(*inst)) { + words.push_back(inst); + } + } + } + IrBuilder builder(program); + for (IrValue* word : words) { + IrValue& primitive = insertBuiltin(program, builder, *word, StageInputKind::RasterPrimitive); + IrValue& sample = insertBefore(program, *word, IrOpcode::ShiftLeftLogical32, insertBuiltin(program, builder, *word, StageInputKind::SampleId), builder.Constant(8u)); + IrValue& layer = insertBefore(program, *word, IrOpcode::ShiftLeftLogical32, insertBuiltin(program, builder, *word, StageInputKind::Layer), builder.Constant(16u)); + IrValue& low = insertBefore(program, *word, IrOpcode::BitwiseOr32, primitive, sample); + word->ReplaceAllUsesWith(&insertBefore(program, *word, IrOpcode::BitwiseOr32, low, layer)); + } + return !words.empty(); +} + } diff --git a/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp b/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp index 74f4cda6f718..e500ead2ae57 100644 --- a/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp +++ b/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp @@ -1,6 +1,7 @@ #include "Optimization/MaskedSelectEliminator.hpp" #include "Optimization/DeadCodeEliminator.hpp" #include "RdnaDecoder/RdnaInstruction.hpp" +#include #include #include #include @@ -10,7 +11,8 @@ namespace ShaderRecompiler { namespace { -constexpr std::size_t VisitLimit = 4096; +constexpr std::size_t MinimumWalkLimit = 4096; +constexpr std::size_t WalkLimitPerInstruction = 2; constexpr std::uint32_t DepthLimit = 8; bool isSelect(IrOpcode opcode) { @@ -169,6 +171,7 @@ bool isLaneLocal(IrOpcode opcode) { case IrOpcode::FPFrexpMant64: case IrOpcode::FPFrexpExp64: case IrOpcode::MakeImageAddress: + case IrOpcode::ImageQueryDimensions: return true; default: return false; @@ -202,6 +205,8 @@ bool readsOnlyWhereActive(IrOpcode opcode) { case IrOpcode::WriteSharedU32x3: case IrOpcode::WriteSharedU32x4: case IrOpcode::SetAttribute: + case IrOpcode::ImageRead: + case IrOpcode::ImageWrite: return true; default: return false; @@ -209,7 +214,7 @@ bool readsOnlyWhereActive(IrOpcode opcode) { } bool isExplicitLodSample(const IrProgram& program, const IrValue& value) { - if (value.Opcode() != IrOpcode::ImageSampleRaw) return false; + if (value.Opcode() != IrOpcode::ImageSampleRaw && value.Opcode() != IrOpcode::ImageGatherRaw) return false; if (program.Resources().stage != IrShaderStage::Pixel) return true; const auto index = value.Flags().index; if (index >= program.Resources().memoryInfo.size()) return false; @@ -266,38 +271,160 @@ bool alwaysTrue(const IrValue* value, std::uint32_t waveSize, std::uint32_t dept return bit && isImmediate(bit->low, 0xffffffffu) && isImmediate(bit->high, 0xffffffffu); } -bool implies(const IrValue* condition, const IrValue* mask, std::uint32_t waveSize, std::uint32_t depth = 0); +enum class Region : std::uint8_t { Unreached, Once, Loop }; -bool wordsImply(const IrValue* low, const IrValue* high, const IrValue* mask, std::uint32_t waveSize, std::uint32_t depth) { - if (depth > DepthLimit) return false; - if (isImmediate(low, 0u) && (waveSize == 32u || isImmediate(high, 0u))) return true; - if (low->Opcode() == IrOpcode::CompositeExtractU32x4 && isImmediate(low->Argument(1)->Resolve(), 0u)) { - const IrValue* ballot = low->Argument(0)->Resolve(); - const bool sameBallot = waveSize == 32u || (high->Opcode() == IrOpcode::CompositeExtractU32x4 && high->Argument(0)->Resolve() == ballot && isImmediate(high->Argument(1)->Resolve(), 1u)); - if (ballot->Opcode() == IrOpcode::Ballot && sameBallot && implies(ballot->Argument(0), mask, waveSize, depth + 1u)) return true; +Region regionOf(const std::vector& regions, const IrBlock* block) { + return block != nullptr && block->Id() < regions.size() ? regions[block->Id()] : Region::Unreached; +} + +bool followable(const std::vector& regions, const IrBlock* block, bool acrossLoops) { + const Region region = regionOf(regions, block); + return region == Region::Once || (acrossLoops && region == Region::Loop); +} + +struct ImplicationKey { + const IrValue* first = nullptr; + const IrValue* second = nullptr; + + bool operator==(const ImplicationKey&) const = default; +}; + +struct ImplicationKeyHash { + std::size_t operator()(const ImplicationKey& key) const { + return std::hash{}(key.first) * 31u ^ std::hash{}(key.second); + } +}; + +class Implications { +public: + Implications(const std::vector& regions, std::uint32_t waveSize, std::size_t& work) : regions(regions), waveSize(waveSize), work(work) {} + + void Reset(const IrValue* writeMask, bool acrossLoops) { + mask = writeMask; + loops = acrossLoops; + always = alwaysTrue(writeMask, waveSize); + results.clear(); + proven.clear(); + assumptionReads = 0; } - if (low->Opcode() != IrOpcode::BitwiseAnd32) return false; - if (waveSize == 32u) return wordsImply(low->Argument(0)->Resolve(), high, mask, waveSize, depth + 1u) || wordsImply(low->Argument(1)->Resolve(), high, mask, waveSize, depth + 1u); - if (high->Opcode() != IrOpcode::BitwiseAnd32) return false; - for (std::size_t lowIndex = 0; lowIndex < 2u; ++lowIndex) { - for (std::size_t highIndex = 0; highIndex < 2u; ++highIndex) { - if (wordsImply(low->Argument(lowIndex)->Resolve(), high->Argument(highIndex)->Resolve(), mask, waveSize, depth + 1u)) return true; + + bool Implies(const IrValue* condition, std::uint32_t depth = 0) { + condition = condition->Resolve(); + if (condition == mask || always) return true; + if (condition->HasImmediate()) return condition->Type() == IrType::Bool && !condition->ImmediateBool(); + const ImplicationKey key{condition, nullptr}; + if (const auto known = lookup(key)) return *known; + if (depth > ImplicationDepthLimit || work == 0) return false; + --work; + const Attempt attempt = assume(key); + bool result = false; + if (condition->Opcode() == IrOpcode::LogicalAnd) { + result = Implies(condition->Argument(0), depth + 1u) || Implies(condition->Argument(1), depth + 1u); + } else if (condition->IsPhi()) { + result = merges(*condition); + for (std::size_t index = 0; result && index < condition->ArgumentCount(); ++index) { + result = Implies(condition->Argument(index), depth + 1u); + } + } else if (const auto bit = threadBit(condition, waveSize)) { + result = wordsImply(bit->low, bit->high, depth + 1u); } + return settle(key, attempt, result); } - return false; -} -bool implies(const IrValue* condition, const IrValue* mask, std::uint32_t waveSize, std::uint32_t depth) { - condition = condition->Resolve(); - if (condition == mask || alwaysTrue(mask, waveSize)) return true; - if (condition->HasImmediate()) return condition->Type() == IrType::Bool && !condition->ImmediateBool(); - if (depth > DepthLimit) return false; - if (condition->Opcode() == IrOpcode::LogicalAnd) return implies(condition->Argument(0), mask, waveSize, depth + 1u) || implies(condition->Argument(1), mask, waveSize, depth + 1u); - const auto bit = threadBit(condition, waveSize); - return bit && wordsImply(bit->low, bit->high, mask, waveSize, depth + 1u); -} +private: + static constexpr std::uint32_t ImplicationDepthLimit = 64; + + enum class Proof : std::uint8_t { Assumed, Implied, NotImplied }; + + struct Attempt { + std::size_t proven = 0; + std::size_t assumptionReads = 0; + }; + + std::optional lookup(const ImplicationKey& key) { + const auto known = results.find(key); + if (known == results.end()) return std::nullopt; + if (known->second == Proof::Assumed) ++assumptionReads; + return known->second != Proof::NotImplied; + } + + Attempt assume(const ImplicationKey& key) { + results.emplace(key, Proof::Assumed); + return Attempt{proven.size(), assumptionReads}; + } + + bool settle(const ImplicationKey& key, const Attempt& attempt, bool implied) { + if (!implied && assumptionReads != attempt.assumptionReads) { + for (std::size_t index = attempt.proven; index < proven.size(); ++index) results.erase(proven[index]); + proven.resize(attempt.proven); + } + results[key] = implied ? Proof::Implied : Proof::NotImplied; + if (implied) proven.push_back(key); + return implied; + } -std::unordered_set blocksOutsideLoops(const IrProgram& program) { + bool merges(const IrValue& phi) const { + if (!followable(regions, phi.Parent(), loops) || phi.ArgumentCount() != phi.PhiBlockCount()) return false; + for (std::size_t index = 0; index < phi.ArgumentCount(); ++index) { + const IrValue* incoming = phi.Argument(index)->Resolve(); + if (!incoming->HasImmediate() && !followable(regions, incoming->Parent(), loops)) return false; + } + return true; + } + + static const IrValue* incomingFrom(const IrValue& phi, const IrBlock* predecessor) { + for (std::size_t index = 0; index < phi.PhiBlockCount(); ++index) { + if (phi.PhiBlock(index) == predecessor) return phi.Argument(index)->Resolve(); + } + return nullptr; + } + + bool wordsImply(const IrValue* low, const IrValue* high, std::uint32_t depth) { + if (isImmediate(low, 0u) && (waveSize == 32u || isImmediate(high, 0u))) return true; + const ImplicationKey key{low, high}; + if (const auto known = lookup(key)) return *known; + if (depth > ImplicationDepthLimit || work == 0) return false; + --work; + const Attempt attempt = assume(key); + bool result = false; + if (low->Opcode() == IrOpcode::CompositeExtractU32x4 && isImmediate(low->Argument(1)->Resolve(), 0u)) { + const IrValue* ballot = low->Argument(0)->Resolve(); + const bool sameBallot = waveSize == 32u || (high->Opcode() == IrOpcode::CompositeExtractU32x4 && high->Argument(0)->Resolve() == ballot && isImmediate(high->Argument(1)->Resolve(), 1u)); + result = ballot->Opcode() == IrOpcode::Ballot && sameBallot && Implies(ballot->Argument(0), depth + 1u); + } + if (!result && low->Opcode() == IrOpcode::BitwiseAnd32) { + if (waveSize == 32u) { + result = wordsImply(low->Argument(0)->Resolve(), high, depth + 1u) || wordsImply(low->Argument(1)->Resolve(), high, depth + 1u); + } else if (high->Opcode() == IrOpcode::BitwiseAnd32) { + for (std::size_t lowIndex = 0; !result && lowIndex < 2u; ++lowIndex) { + for (std::size_t highIndex = 0; !result && highIndex < 2u; ++highIndex) { + result = wordsImply(low->Argument(lowIndex)->Resolve(), high->Argument(highIndex)->Resolve(), depth + 1u); + } + } + } + } + if (!result && low->IsPhi() && merges(*low) && (waveSize == 32u || (high->IsPhi() && high->Parent() == low->Parent() && merges(*high)))) { + result = true; + for (std::size_t index = 0; result && index < low->PhiBlockCount(); ++index) { + const IrValue* highIncoming = waveSize == 32u ? high : incomingFrom(*high, low->PhiBlock(index)); + result = highIncoming != nullptr && wordsImply(low->Argument(index)->Resolve(), highIncoming, depth + 1u); + } + } + return settle(key, attempt, result); + } + + const std::vector& regions; + std::uint32_t waveSize; + std::size_t& work; + const IrValue* mask = nullptr; + bool loops = false; + bool always = false; + std::unordered_map results; + std::vector proven; + std::size_t assumptionReads = 0; +}; + +std::vector blockRegions(const IrProgram& program) { const auto& order = program.BlockOrder(); if (order.empty()) return {}; std::unordered_map state; @@ -330,46 +457,89 @@ std::unordered_set blocksOutsideLoops(const IrProgram& program) looping.insert(body.begin(), body.end()); } } - std::unordered_set result; + std::vector result; for (const auto& [block, visited] : state) { - if (visited == 2 && !looping.contains(block)) result.insert(block); + if (visited != 2) continue; + if (block->Id() >= result.size()) result.resize(block->Id() + 1u, Region::Unreached); + result[block->Id()] = looping.contains(block) ? Region::Loop : Region::Once; } return result; } -bool unobservedWhereMasked(const IrProgram& program, const std::unordered_set& once, const IrValue& select, const IrValue* mask) { - const auto waveSize = program.WaveSize(); - std::vector pending{&select}; - std::unordered_set visited{&select}; - while (!pending.empty()) { - const IrValue* value = pending.back(); - pending.pop_back(); - for (const IrUse& use : value->OperandUses()) { - const IrValue* user = use.user; - if (user->Parent() == nullptr || !once.contains(user->Parent())) return false; - if (isSelect(user->Opcode()) && use.operand == 1u && implies(user->Argument(0), mask, waveSize)) continue; - if (user->Opcode() == IrOpcode::LogicalAnd && use.operand < 2u && implies(user->Argument(1u - use.operand), mask, waveSize)) continue; - if (readsOnlyWhereActive(user->Opcode()) && use.operand + 1u < user->ArgumentCount() && implies(user->Argument(user->ArgumentCount() - 1u), mask, waveSize)) continue; - if (!user->IsPhi() && !isLaneLocal(user->Opcode()) && !isExplicitLodSample(program, *user)) return false; - if (visited.insert(user).second) { - if (visited.size() > VisitLimit) return false; - pending.push_back(user); +std::size_t walkLimit(const IrProgram& program) { + std::size_t instructions = 0; + for (const IrBlock* block : program.BlockOrder()) instructions += block->Instructions().size(); + return std::max(MinimumWalkLimit, WalkLimitPerInstruction * instructions); +} + +class MaskedLaneWalk { +public: + MaskedLaneWalk(const IrProgram& program, const std::vector& regions) : program(program), regions(regions), limit(walkLimit(program)), implications(regions, program.WaveSize(), work) {} + + bool UnobservedWhereMasked(const IrValue& select, const IrValue* mask) { + const bool acrossLoops = mask->HasImmediate() || regionOf(regions, mask->Parent()) == Region::Once; + work = limit; + implications.Reset(mask, acrossLoops); + startVisits(); + pending.clear(); + pending.push_back(&select); + static_cast(firstVisit(select)); + while (!pending.empty()) { + const IrValue* value = pending.back(); + pending.pop_back(); + for (const IrUse& use : value->OperandUses()) { + const IrValue* user = use.user; + if (!followable(regions, user->Parent(), acrossLoops)) return false; + if (isSelect(user->Opcode()) && use.operand == 1u && implications.Implies(user->Argument(0))) continue; + if (user->Opcode() == IrOpcode::LogicalAnd && use.operand < 2u && implications.Implies(user->Argument(1u - use.operand))) continue; + if (readsOnlyWhereActive(user->Opcode()) && use.operand + 1u < user->ArgumentCount() && implications.Implies(user->Argument(user->ArgumentCount() - 1u))) continue; + if (!user->IsPhi() && !isLaneLocal(user->Opcode()) && !isExplicitLodSample(program, *user)) return false; + if (firstVisit(*user)) { + if (work == 0) return false; + --work; + pending.push_back(user); + } } } + return true; } - return true; -} + +private: + void startVisits() { + if (++epoch != 0u) return; + std::fill(visits.begin(), visits.end(), 0u); + epoch = 1u; + } + + bool firstVisit(const IrValue& value) { + const std::uint32_t id = value.Id(); + if (id >= visits.size()) visits.resize(id + 1u, 0u); + if (visits[id] == epoch) return false; + visits[id] = epoch; + return true; + } + + const IrProgram& program; + const std::vector& regions; + std::size_t limit; + std::size_t work = 0; + Implications implications; + std::vector pending; + std::vector visits; + std::uint32_t epoch = 0; +}; } MaskedSelectEliminationStats MaskedSelectEliminator::Eliminate(IrProgram& program) const { MaskedSelectEliminationStats stats; - const auto once = blocksOutsideLoops(program); + const auto regions = blockRegions(program); + MaskedLaneWalk walk(program, regions); bool changed = true; while (changed) { changed = false; for (auto blockIt = program.BlockOrder().rbegin(); blockIt != program.BlockOrder().rend(); ++blockIt) { - const bool single = once.contains(*blockIt); + const bool single = regionOf(regions, *blockIt) == Region::Once; auto& instructions = (*blockIt)->Instructions(); auto it = instructions.end(); while (it != instructions.begin()) { @@ -377,7 +547,7 @@ MaskedSelectEliminationStats MaskedSelectEliminator::Eliminate(IrProgram& progra IrValue* inst = *it; if (!isSelect(inst->Opcode()) || inst->ArgumentCount() != 3u || !inst->HasUses()) continue; const IrValue* mask = inst->Argument(0)->Resolve(); - if (!alwaysTrue(mask, program.WaveSize()) && (!single || !unobservedWhereMasked(program, once, *inst, mask))) continue; + if (!alwaysTrue(mask, program.WaveSize()) && (!single || !walk.UnobservedWhereMasked(*inst, mask))) continue; inst->ReplaceAllUsesWith(inst->Argument(1)); inst->Invalidate(); inst->SetParent(nullptr); diff --git a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp index 672f01ef4356..a999c04a4350 100644 --- a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp +++ b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp @@ -143,6 +143,7 @@ void ValidateValueReferences(const IrProgram& program, ShaderStageInputInfo inpu } case StageInputKind::Layer: case StageInputKind::SampleId: + case StageInputKind::RasterPrimitive: if (program.Resources().stage != IrShaderStage::Pixel || component != 0u) { return Fail("typed pixel scalar input is invalid"); } @@ -386,6 +387,9 @@ void CollectBuiltinInputs(const IrProgram& program, ShaderInfo& info) { case StageInputKind::DispatchThreadLimit: info.dispatchThreadLimit = true; break; + case StageInputKind::RasterPrimitive: + info.rasterPrimitive = true; + break; case StageInputKind::PackedAncillary: case StageInputKind::Parameter: break; diff --git a/core/shader/recompiler/PipelineSpecialization.hpp b/core/shader/recompiler/PipelineSpecialization.hpp index f6ae066765ac..8ed8a0af98de 100644 --- a/core/shader/recompiler/PipelineSpecialization.hpp +++ b/core/shader/recompiler/PipelineSpecialization.hpp @@ -26,6 +26,7 @@ inline constexpr std::uint32_t PushDataOffset = 8188u; inline constexpr std::uint32_t ExportBase = 8192u; inline constexpr std::uint32_t ExportPackingBase = 8224u; inline constexpr std::uint32_t DualSourceBlend = 8232u; +inline constexpr std::uint32_t RasterPrimitive = 8233u; inline constexpr std::uint32_t HeapCountBase = 8256u; inline constexpr std::uint32_t MipCountBase = 8320u; inline constexpr std::uint32_t DescriptorIndexBase = 16384u; diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index bbaece6136bb..a1c6c1785d1f 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -212,6 +212,9 @@ IrProgram PrepareResourceProgram(const RecompileRequest& request, ShaderPreparat if (maskedSelectEliminator.Eliminate(program).removedSelects != 0u) { deadCodeEliminator.Eliminate(program); } + if (constantFolder.ComposePackedAncillary(program)) { + deadCodeEliminator.Eliminate(program); + } }; simplify(); if (stageKind == ShaderStageKind::Pixel && !translateOptions.fragmentShaderBarycentricEnabled && !HostInterpolationChecker{}.Lower(program, *inputInfo.pixel)) { @@ -689,6 +692,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil if (request.context.pixel) { for (const auto packing : request.context.pixel->targetExportPacking) moduleKey.push_back(static_cast(packing)); moduleKey.push_back(request.context.pixel->dualSourceBlend ? 1u : 0u); + if (variant.info.info.rasterPrimitive) moduleKey.push_back(static_cast(request.context.pixel->rasterPrimitive)); } result.vertexAttributes.reserve(result.vertexInputs.size()); std::array vertexClasses{}; @@ -732,6 +736,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil if (request.context.pixel) { for (std::uint32_t target = 0; target < request.context.pixel->targetExportPacking.size(); ++target) constants.push_back({PipelineSpecialization::ExportPackingBase + target, moduleKey[index++]}); constants.push_back({PipelineSpecialization::DualSourceBlend, moduleKey[index++]}); + if (variant.info.info.rasterPrimitive) constants.push_back({PipelineSpecialization::RasterPrimitive, moduleKey[index++]}); } if (!artifact.vertexInputPatches.empty()) { for (const auto& input : result.vertexInputs) { @@ -919,6 +924,7 @@ std::uint64_t snapshotHash(const RecompileRequest& request, const ResourceSnapsh for (const auto mapping : request.context.pixel->targetExportMapping) mix(mapping); for (const auto packing : request.context.pixel->targetExportPacking) mix(static_cast(packing)); mix(request.context.pixel->dualSourceBlend); + mix(static_cast(request.context.pixel->rasterPrimitive)); } if (request.context.vertex) { const auto& vertex = *request.context.vertex; diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 5688af8e8019..b3575279183d 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -113,6 +113,13 @@ enum class ColorExportPacking : std::uint8_t { Unorm10_11_11 }; +enum class RasterPrimitive : std::uint8_t { + Point, + Line, + Triangle, + Rectangle +}; + struct ShaderPixelStageInfo { std::uint32_t interpolatorCount; std::array interpolatorSettings; @@ -140,6 +147,7 @@ struct ShaderPixelStageInfo { std::array targetExportMapping; std::array targetExportPacking; bool dualSourceBlend; + RasterPrimitive rasterPrimitive = RasterPrimitive::Triangle; }; struct ShaderVertexBufferResource { diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index fe8b0889611d..834b5cba39c1 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -534,6 +534,7 @@ void encodeInfo(Writer& writer, const CompiledShaderInfo& compiled) { writer.Value(info.bdaWrites); writer.Value(info.usesFaultBuffer); writer.Value(info.dispatchThreadLimit); + writer.Value(info.rasterPrimitive); encodeLayout(writer, compiled.bindings); } @@ -641,6 +642,7 @@ void decodeInfo(Reader& reader, CompiledShaderInfo& compiled) { reader.Value(info.bdaWrites); reader.Value(info.usesFaultBuffer); reader.Value(info.dispatchThreadLimit); + reader.Value(info.rasterPrimitive); decodeLayout(reader, compiled.bindings); } diff --git a/core/shader/recompiler/ShaderDiskCache.hpp b/core/shader/recompiler/ShaderDiskCache.hpp index 6111e47c17b5..08b1d6c8d74e 100644 --- a/core/shader/recompiler/ShaderDiskCache.hpp +++ b/core/shader/recompiler/ShaderDiskCache.hpp @@ -12,7 +12,7 @@ namespace ShaderRecompiler::ShaderDiskCache { -inline constexpr std::uint32_t FormatVersion = 24; +inline constexpr std::uint32_t FormatVersion = 25; enum class LoadStatus { Loaded, diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 07f3ca94857d..36ccef0f7575 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -45,6 +45,9 @@ std::uint32_t EmitBuiltinU32(SpirvEmitterState& state, StageInputKind kind, std: if (kind == StageInputKind::DispatchThreadLimit) { return EmitShaderDataDwordLoad(state, state.program.Metadata().bindings.DispatchThreadLimitDword() + component); } + if (kind == StageInputKind::RasterPrimitive) { + return state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::RasterPrimitive, static_cast(RasterPrimitive::Triangle)); + } if (state.laneCount == 2 && (kind == StageInputKind::LocalInvocationId || kind == StageInputKind::GlobalInvocationId)) { const auto* workgroup = ShaderWorkgroupInputFor(state); if (workgroup == nullptr) { diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 3d64959512cb..74d50e410292 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -48,7 +48,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceVideoOutGetOutputStatusWithVrr](../../core/libs/prx/libSceVideoOut/Unimplemented.cpp) (libSceVideoOut, NID `gWT7X8H0bYs`) - the name hashes to the imported NID. PPSA26344 version 02.002.000 passes a VideoOut handle and an output pointer at three call sites. It compares a 32-bit value at byte 4 with 2, tests bit 0x10 at byte 16, and, when set, compares low 16-bit values at bytes 48, 50 and 52 with 48, 60 and 120. The full output layout, size, field meanings and console errors are unknown. Every call throws without reading or writing caller memory. - [getuid, geteuid, getgid, getegid and issetugid](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) report the root identity (0) and a process that never changed credentials (`issetugid` 0), as a jailbroken process has; the host user and group are not exposed to the guest. The values a title sees on a console are unverified - [sceFontSetScriptLanguage, sceFontGetScriptLanguage, sceFontSetTypographicDesign, sceFontGetTypographicDesign](../../core/libs/prx/libSceFont/src/Style.cpp) (libSceFont) - signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.cs), not confirmed for the PS5. The handle keeps the language set for each script and the feature set for each typographic design and returns them, with 0 for one never set; the console default and its valid ranges are unknown, so any value is accepted. Neither setting changes shaping or rendering yet -- [Packed pixel ancillary word](../../core/shader/recompiler/Optimization/src/ConstantFolder.cpp) (shader recompiler) - the sample index at bits 8-11 and the render target index at bits 16-28 follow #295, and their equality with the SampleId and Layer builtins is inferred, not measured. A value passed through a select, move or phi is rebuilt from those two fields with bits 0-7, 12-15 and 29-31 zero. That is exact only because every read of the word must be an extract inside one modelled field; any other read throws. The other bits are unmodelled and their RDNA 2 layout is unknown: the RDNA 3.5 ISA is reported to give prim type at bits 0-1, VRS rates at bits 2-5 and eye id at bit 7, which was not checked against RDNA 2 +- [Packed pixel ancillary word](../../core/shader/recompiler/Optimization/src/ConstantFolder.cpp) (shader recompiler) - the sample index at bits 8-11 and the render target index at bits 16-28 follow #295, and their equality with the SampleId and Layer builtins is inferred, not measured. Mesa reads both fields from those bits, with the layer 14 bits wide only from GFX12 on ([ac_nir_lower_intrinsics_to_args.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/bffd91947efc1932da99aca69fc540d6d4201b0c/src/amd/common/nir/ac_nir_lower_intrinsics_to_args.c#L342-L368)). A value passed through a select, move or phi whose every read is an extract inside one of those fields is rebuilt from the two fields alone. Any other read the masked-select pass cannot drop gets the whole word: the primitive class at bits 0-1 (0 point, 1 line, 2 triangle, 3 rectangle), which the draw sets as a pipeline specialization constant ([RasterPrimitiveOf](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp)) in the shaders that read it, plus the sample and layer fields, with every other bit zero. That layout comes from the RDNA 3 and 3.5 ISA guides (prim type 1:0, VRS rates 3:2 and 5:4, eye id 7, sample 11:8, render target index 28:16). The GCN register guides give the same prim type and sample fields and put the render target index at 26:16, so the two sources agree for layers below 2048. AMD's LLPC reads the primitive class from bits 1:0 with the same encoding ([LowerGlCompatibility.cpp](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/llpc/lowering/LowerGlCompatibility.cpp#L830-L836)) and without a GFX-level check ([getPrimType](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/lgc/lowering/LowerInOut.cpp#L5307-L5315)). Nobody has checked it on gfx1013. The VRS rates and the eye id are zero because the driver supports neither variable-rate shading nor multiview; a title that enables either would read other bits on the console. The sample field reads SampleId, which is always 0 here because the driver rejects MSAA and per-sample shading (`PA_SC_AA_CONFIG`, `PS_ITER_SAMPLE`), and the layer field reads Layer, which is 0 while the driver ignores vertex layer exports - [sceAudioPropagationSourceSetAudioPath](../../core/libs/prx/libSceAudioPropagation/Export.cpp) (libSceAudioPropagation, NID `tKSmk2JsMAA`) - `(uint64_t path, const void* data, float gain)` follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioPropagation.cs) and SAROS's call, which passes a handle from `sceAudioPropagationSourceGetAudioPath`, path data and gain 1.0. Path updates are unsupported and every call throws without reading or writing caller memory. The no-propagation model yields no path handles; path allocation, ownership, rendering and console error codes remain unknown. - [Color comparison descriptor forms](../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp): emulated color comparison supports UNORM, SNORM and float formats for 2D, 2D-array and cube views. sRGB formats, anisotropic filters and bilinear cube comparisons without `DISABLE_CUBE_WRAP` are refused; cube-edge sampling behavior and console coverage on PS5's gfx1013 remain unverified. - [sceNpEntitlementAccessGetEntitlementKey](../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp) - the three-argument signature, 16-byte key and errors `0x817D0002`/`0x817D0007` follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp); PPSA01521 passes a service label, entitlement label and output pointer. Only the no-key path is implemented: local entitlement labels carry no keys, and the output is untouched on failure. Console error precedence and key provisioning are unverified