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 4ddd3e462af..36e7c1a1198 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp @@ -36,6 +36,7 @@ std::shared_ptr Driver::decodeDraw(const QueueState& queue, const Su product->pixel.dualSourceBlend = product->state.dualSourceBlend; product->pixel.alphaToCoverage = product->state.alphaToCoverage; if (product->pixel.ancillary) product->pixel.rasterPrimitive = Graphics::RasterPrimitiveOf(product->state); + if (product->pixel.ancillary) product->pixel.layerBase = Graphics::PixelLayerBase(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 7827b8b336f..acc3aebd81f 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp @@ -61,7 +61,7 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr(pixel.conservativeZExport)); mix(pixel.orderedPixelShader); for (const auto value : pixel.targetOutputMode) mix(value); 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 8f63e4ac7cd..3213c68749c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp @@ -98,14 +98,14 @@ bool Driver::sameDecode(const DrawDecode& a, const DrawDecode& b) { if (std::memcmp(&s.viewport, &t.viewport, sizeof(VkViewport)) != 0 || std::memcmp(&s.scissor, &t.scissor, sizeof(VkRect2D)) != 0) return false; const auto& p = a.pixel; const auto& q = b.pixel; - if (p.interpolatorCount != q.interpolatorCount || p.interpolatorSettings != q.interpolatorSettings || p.wave32 != q.wave32 || p.inputAddr != q.inputAddr || p.hasPerspectiveCenterVgpr != q.hasPerspectiveCenterVgpr || p.perspectiveCentroid != q.perspectiveCentroid || p.posX != q.posX || p.posY != q.posY || p.posZ != q.posZ || p.posW != q.posW || p.frontFace != q.frontFace || p.ancillary != q.ancillary || p.sampleShading != q.sampleShading || p.noPerspective != q.noPerspective || p.linearCentroid != q.linearCentroid || p.pixelKillEnable != q.pixelKillEnable || p.depthExportEnable != q.depthExportEnable || p.sampleMaskExportEnable != q.sampleMaskExportEnable || p.earlyZ != q.earlyZ || p.executeOnNoop != q.executeOnNoop || p.conservativeZExport != q.conservativeZExport || p.orderedPixelShader != q.orderedPixelShader || p.targetOutputMode != q.targetOutputMode || p.targetExportMapping != q.targetExportMapping) return false; + if (p.interpolatorCount != q.interpolatorCount || p.interpolatorSettings != q.interpolatorSettings || p.wave32 != q.wave32 || p.inputAddr != q.inputAddr || p.hasPerspectiveCenterVgpr != q.hasPerspectiveCenterVgpr || p.perspectiveCentroid != q.perspectiveCentroid || p.posX != q.posX || p.posY != q.posY || p.posZ != q.posZ || p.posW != q.posW || p.frontFace != q.frontFace || p.frontFaceAllBits != q.frontFaceAllBits || p.ancillary != q.ancillary || p.sampleShading != q.sampleShading || p.noPerspective != q.noPerspective || p.linearCentroid != q.linearCentroid || p.pixelKillEnable != q.pixelKillEnable || p.depthExportEnable != q.depthExportEnable || p.sampleMaskExportEnable != q.sampleMaskExportEnable || p.earlyZ != q.earlyZ || p.executeOnNoop != q.executeOnNoop || p.conservativeZExport != q.conservativeZExport || p.orderedPixelShader != q.orderedPixelShader || p.targetOutputMode != q.targetOutputMode || p.targetExportMapping != q.targetExportMapping) return false; if (a.roles != b.roles || a.programs.size() != b.programs.size()) return false; for (std::size_t i = 0; i < a.programs.size(); ++i) { const auto& x = a.programs[i]; 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 || p.alphaToCoverage != q.alphaToCoverage || p.rasterPrimitive != q.rasterPrimitive) return false; + if (p.targetExportPacking != q.targetExportPacking || p.dualSourceBlend != q.dualSourceBlend || p.alphaToCoverage != q.alphaToCoverage || p.rasterPrimitive != q.rasterPrimitive || p.layerExported != q.layerExported || p.layerBase != q.layerBase) 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 88728cb5beb..0e91d1d6f76 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -149,6 +149,9 @@ std::array SampleLocations(const State& state); std::array ExportMappings(const State& state); std::array ExportPackings(const State& state); ShaderRecompiler::RasterPrimitive RasterPrimitiveOf(const State& state); +// First slice the pixel ancillary layer field adds to the exported layer: the color view's slice, 0 for +// layered draws (the vertex stage exports the layer) and for draws without a color target. +std::uint32_t PixelLayerBase(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); @@ -206,9 +209,9 @@ struct DrawKeyRange { inline constexpr std::array DrawKeyRegisters{{ {RegisterBank::Context, 0x000, 1}, {RegisterBank::Context, 0x002, 1}, {RegisterBank::Context, 0x005, 1}, {RegisterBank::Context, 0x007, 7}, {RegisterBank::Context, 0x010, 6}, {RegisterBank::Context, 0x01a, 5}, {RegisterBank::Context, 0x080, 4}, {RegisterBank::Context, 0x08c, 4}, {RegisterBank::Context, 0x090, 2}, {RegisterBank::Context, 0x094, 2}, {RegisterBank::Context, 0x0b4, 2}, {RegisterBank::Context, 0x105, 4}, {RegisterBank::Context, 0x10b, 3}, {RegisterBank::Context, 0x10f, 6}, - // SPI_PS_INPUT_CNTL_0..31, SPI_PS_INPUT_ENA/ADDR, SPI_PS_IN_CONTROL, SPI_SHADER_POS/Z/COL_FORMAT, + // SPI_PS_INPUT_CNTL_0..31, SPI_PS_INPUT_ENA/ADDR, SPI_PS_IN_CONTROL .. SPI_BARYC_CNTL, SPI_SHADER_POS/Z/COL_FORMAT, // CB_BLEND0..7_CONTROL, GE_MAX_OUTPUT_PER_SUBGROUP. - {RegisterBank::Context, 0x191, 32}, {RegisterBank::Context, 0x1b3, 2}, {RegisterBank::Context, 0x1b6, 1}, {RegisterBank::Context, 0x1c3, 3}, {RegisterBank::Context, 0x1e0, 8}, {RegisterBank::Context, 0x1ff, 1}, + {RegisterBank::Context, 0x191, 32}, {RegisterBank::Context, 0x1b3, 2}, {RegisterBank::Context, 0x1b6, 3}, {RegisterBank::Context, 0x1c3, 3}, {RegisterBank::Context, 0x1e0, 8}, {RegisterBank::Context, 0x1ff, 1}, // DB_DEPTH_CONTROL .. PA_CL_VS_OUT_CNTL, PA_SC_MODE_CNTL_0/1, VGT_GS_MODE, VGT_GS_VERT_ITEMSIZE, // PA_SU_VTX_CNTL, PA_SC_AA_CONFIG, the sample locations, the sample masks, // PA_SC_CONSERVATIVE_RASTERIZATION_CNTL. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp index ae684e041ef..4d2bf30bbdd 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp @@ -22,6 +22,7 @@ constexpr std::uint32_t spiPsInputCntl0 = 0x191; constexpr std::uint32_t spiPsInputEna = 0x1B3; constexpr std::uint32_t spiPsInputAddr = 0x1B4; constexpr std::uint32_t spiPsInControl = 0x1B6; +constexpr std::uint32_t spiBarycCntl = 0x1B8; constexpr std::uint32_t dbShaderControl = 0x203; constexpr std::uint32_t spiShaderColFormat = 0x1C5; constexpr std::uint32_t defaultPixelInputs = 0x2u; @@ -134,6 +135,7 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con .posZ = loaded(PixelInput::PositionZ), .posW = loaded(PixelInput::PositionW), .frontFace = loaded(PixelInput::FrontFace), + .frontFaceAllBits = loaded(PixelInput::FrontFace) && ((read(context, spiBarycCntl, RegisterBank::Context) >> 24u) & 0x1u) != 0, .ancillary = loaded(PixelInput::Ancillary), .sampleShading = loaded(PixelInput::PerspectiveSample) && loaded(PixelInput::LinearSample), .noPerspective = loaded(PixelInput::LinearCenter), diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp index c5d9168d1fe..2a05a98e6f3 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp @@ -403,7 +403,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi Require(module.modes.size() == 1, "unsupported tessellation-control execution mode"); } else if (evaluation) { mode(state.rectList ? spv::ExecutionModeQuads : spv::ExecutionModeTriangles, {}); - mode(state.rectList ? spv::ExecutionModeSpacingEqual : spv::ExecutionModeSpacingFractionalOdd, {}); + mode(state.rectList || state.stages.tessellation->partitioning == 0u ? spv::ExecutionModeSpacingEqual : state.stages.tessellation->partitioning == 3u ? spv::ExecutionModeSpacingFractionalEven : spv::ExecutionModeSpacingFractionalOdd, {}); mode(spv::ExecutionModeVertexOrderCw, {}); Require(module.modes.size() == 3, "unsupported tessellation-evaluation execution mode"); } else if (geometry) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index e81cdb830b0..4ca4e6cfa4f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -671,7 +671,7 @@ ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup) { ShaderRecompiler::TessellationConfiguration tess{(config >> 8u) & 0x3fu, (config >> 14u) & 0x3fu, parameters & 3u, (parameters >> 2u) & 7u, (parameters >> 5u) & 7u}; validate(tess.inputControlPoints != 0 && tess.inputControlPoints <= 32 && tess.outputControlPoints != 0 && tess.outputControlPoints <= 32, "invalid tessellation control-point counts"); if ((parameters & ~0x000600ffu) != 0) Require(false, zeroMessage(0x2db, parameters, "VGT_TF_PARAM donut, factor detection, memory type or reserved state")); - validate(tess.domain == 1 && tess.partitioning == 2 && tess.outputTopology == 2, "only triangular, fractional-odd, clockwise tessellation is supported by the reference path"); + validate(tess.domain == 1 && (tess.partitioning == 0 || tess.partitioning == 2 || tess.partitioning == 3) && tess.outputTopology == 2, "only triangular, integer, fractional-even or fractional-odd, clockwise tessellation is supported by the reference path"); Require(readFloat(queue.context, 0x287) <= 1.0f && readFloat(queue.context, 0x286) >= 63.0f, "tessellation level clamps inside the fractional-odd range [1, 63] are unsupported"); result.tessellation = tess; } else { @@ -1022,6 +1022,11 @@ std::array ExportPackings(const State& return packings; } +std::uint32_t PixelLayerBase(const State& state) { + if (state.layered || !state.hasColorTarget) return 0u; + return state.color.depth > 1 ? state.color.depthSlice : state.color.arraySlice; +} + 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; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 4e574a03da7..257403faaf5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -54,7 +54,7 @@ AgcDriver::QueueState makeState() { queue.userConfig[0x242] = 4; queue.context = { {0x2d5, 0x2000}, - {0x1b6, 0}, {0x207, 0}, {0x200, 0}, {0x203, 0x800}, + {0x1b6, 0}, {0x1b8, 0}, {0x207, 0}, {0x200, 0}, {0x203, 0x800}, {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, @@ -700,6 +700,10 @@ void ShaderStageTests() { queue.context[0x2db] = parameters; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, reason); } + queue.context[0x2db] = 0x40041; + Require(AgcDriver::Graphics::DecodeState(queue).stages.tessellation->partitioning == 0, "integer-partitioned triangle patches decoded differently"); + queue.context[0x2db] = 0x4004d; + Require(AgcDriver::Graphics::DecodeState(queue).stages.tessellation->partitioning == 3, "fractional-even-partitioned triangle patches decoded differently"); queue.context[0x2db] = 0x40049; for (const auto [maximum, minimum] : {std::pair{0x41100000u, 0x3f800000u}, std::pair{0x42800000u, 0x40000000u}}) { queue.context[0x286] = maximum; @@ -1019,6 +1023,11 @@ void PixelInputLayoutTests() { Require(PixelInputVgpr(pixel.inputAddr, PixelInput::PerspectiveCenter) == 0u && PixelInputVgpr(pixel.inputAddr, PixelInput::PerspectiveCentroid) == 2u && PixelInputVgpr(pixel.inputAddr, PixelInput::LinearCenter) == 4u && PixelInputVgpr(pixel.inputAddr, PixelInput::PositionX) == 6u && PixelInputVgpr(pixel.inputAddr, PixelInput::PositionY) == 7u, "the centroid layout moved the inputs"); pixel = decode(0x1146u, 0x1146u); Require(pixel.linearCentroid && !pixel.noPerspective && PixelInputVgpr(pixel.inputAddr, PixelInput::LinearCentroid) == 4u && PixelInputVgpr(pixel.inputAddr, PixelInput::PositionX) == 6u && PixelInputVgpr(pixel.inputAddr, PixelInput::FrontFace) == 7u, "the linear centroid layout moved the inputs"); + Require(!pixel.frontFaceAllBits, "FRONT_FACE_ALL_BITS was decoded from a clear SPI_BARYC_CNTL"); + queue.context[0x1b8] = 1u << 24u; + Require(decode(0x1146u, 0x1146u).frontFaceAllBits, "SPI_BARYC_CNTL FRONT_FACE_ALL_BITS was not decoded"); + Require(!decode(0x326u, 0x326u).frontFaceAllBits, "FRONT_FACE_ALL_BITS was decoded for a pixel program without the front-face input"); + queue.context[0x1b8] = 0u; pixel = decode(0x506u, 0x7afu); Require(pixel.inputAddr == 0x7afu && !pixel.posY && pixel.posZ && PixelInputVgpr(pixel.inputAddr, PixelInput::PerspectiveCentroid) == 4u && PixelInputVgpr(pixel.inputAddr, PixelInput::PositionX) == 12u && PixelInputVgpr(pixel.inputAddr, PixelInput::PositionZ) == 14u, "ADDR-only inputs did not reserve their VGPRs"); for (const auto bit : {0x8u, 0x4000u}) { @@ -3710,6 +3719,25 @@ void orderedPixelShaderTests() { expectFailure([&] { static_cast(ShaderRecompiler::Recompile(orderedPixelRequest(0x30600u, plain))); }, "fragmentShaderPixelInterlock"); } +void PixelLayerBaseTests() { + using namespace AgcDriver::Graphics; + // Unlike upstream, which takes the slice of the color view as is, the base is 0 when the vertex stage + // exports the layer (it already names the slice) or when there is no color target to take a slice from. + State state{}; + state.hasColorTarget = true; + state.color.arraySlice = 3; + state.color.depthSlice = 5; + state.color.depth = 1; + Require(PixelLayerBase(state) == 3u, "a 2D array color view did not start at its array slice"); + state.color.depth = 4; + Require(PixelLayerBase(state) == 5u, "a 3D color view did not start at its depth slice"); + state.layered = true; + Require(PixelLayerBase(state) == 0u, "a layered draw added the color view slice to the exported layer"); + state.layered = false; + state.hasColorTarget = false; + Require(PixelLayerBase(state) == 0u, "a draw without a color target got a layer base"); +} + void LineStripTests() { using namespace AgcDriver::Graphics; auto queue = makeState(); @@ -4768,6 +4796,7 @@ int main(int argc, char** argv) { conservativeZExportTests(); orderedPixelShaderTests(); LineStripTests(); + PixelLayerBaseTests(); ProvokingVertexTests(); ProvokingVertexRenderPassTests(); ConservativeRasterizationTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 645d6152429..aa2ad94219a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -1489,6 +1489,25 @@ void verifyHalfWaveReduction() { }; require(reductions(vertex(scan(0xffffffffu, 0x26000000u, 0x83800000u), 32u), spv::OpGroupNonUniformUMin) == 1u, "half-wave reduction: the wave64 vertex UMin scan, whose entry EXEC is every invocation, did not read lane 31 as a subgroup UMin on 32 lanes"); require(halves(vertex(scan(0x80000000u, 0x24000000u, 0x84000000u), 64u), spv::OpGroupNonUniformSMax, 0x80000000u), "half-wave reduction: the wave64 vertex SMax scan did not read lanes 31 and 63 as reductions of host invocations 0-31 and 32-63 on 64 lanes"); + const auto domain = [](std::vector code, std::uint32_t subgroupSize) { + *std::find(code.begin(), code.end(), 0xf800180fu) = 0xf80008cfu; + *std::find(code.begin(), code.end(), 0x7e160f00u) = 0x7e160f01u; + RecompileRequest request{}; + request.shader = {ShaderStage::TessellationEvaluation, 0x10000u, code, 0, {}}; + request.context.waveSize = 64; + request.context.vertex = ShaderVertexStageInfo{}; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = subgroupSize; + request.target.supportedCapabilities = capabilities; + request.target.fragmentShaderBarycentricEnabled = false; + request.layout.pushConstantSizeBytes = 128; + request.graphics = GraphicsCompileContext{0u, {}, std::nullopt, TessellationConfiguration{3u, 3u, 1u, 2u, 2u}, {}}; + request.useCache = false; + return Recompile(request).spirv; + }; + require(reductions(domain(scan(0xffffffffu, 0x26000000u, 0x83800000u), 32u), spv::OpGroupNonUniformUMin) == 1u, "half-wave reduction: the wave64 domain UMin scan under s_orn2_saveexec_b64 vcc, exec did not read lane 31 as a subgroup UMin on 32 lanes"); + require(halves(domain(scan(0u, 0x38000000u, 0x88000000u), 64u), spv::OpGroupNonUniformBitwiseOr, 0u), "half-wave reduction: the wave64 domain OR scan did not read lanes 31 and 63 as reductions of host invocations 0-31 and 32-63 on 64 lanes"); const auto floatScan = [&scan](std::uint32_t identity, std::uint32_t vector, std::uint32_t join) { auto code = scan(identity, vector, 0u); code[23] = join; @@ -1530,6 +1549,7 @@ void verifyHalfWaveReduction() { auto overwritten = umin; overwritten.insert(overwritten.begin() + 17, {0x7ef80500u, 0x7e008514u}); expectFailure([&] { static_cast(pixel(overwritten, 32u, true)); }, lane63, "half-wave reduction: a scan of window registers read lane 63 after a v_movreld_b32 at a runtime M0 that may overwrite them"); + require(reductions(domain(divergent, 32u), spv::OpGroupNonUniformUMin) == 1u, "half-wave reduction: domain keys masked inside a divergent branch did not read lane 31 as a subgroup UMin on 32 lanes"); const char* arithmetic = "v_readlane_b32 of lane 31 of a wave64 half-wave reduction scan needs subgroup arithmetic"; expectFailure([&] { static_cast(pixel(umin, 32u, false)); }, arithmetic, "half-wave reduction: a device without subgroup arithmetic read lane 31 on 32 lanes"); expectFailure([&] { static_cast(pixel(umin, 64u, false)); }, arithmetic, "half-wave reduction: a device without subgroup arithmetic read lane 31 on 64 lanes"); @@ -1551,6 +1571,113 @@ void verifyHalfWaveReduction() { expectFailure([&] { static_cast(pixel(umin, 8u, true)); }, "v_permlanex16_b32 from lanes 16-31 is outside the 8-lane host subgroup", "half-wave reduction: the scan built on an 8-lane subgroup"); } +// Guest programs of tests/execution/Tessellation.cpp, compiled here without a device: a hull that writes the +// tessellation factors and a domain shader, both for a three-point triangle patch. +alignas(256) constexpr std::array TessellationHull{ + 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, + 0x02151101, 0x362402ff, 0x000000ff, 0x160028a0, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0000, + 0x02000015, 0x162c2890, 0xd5430016, 0x045a24ff, 0x00000030, 0xbf8cc07f, 0xe0781000, 0x02020216, + 0x7da82881, 0xbe94047e, 0xb06a0060, 0x162602f9, 0x0086066a, 0xd8d80008, 0x06000013, 0xd8dc1008, + 0x07000013, 0xbf8cc07f, 0x7e300306, 0x7e320306, 0x7e340306, 0x7e360306, 0x34142484, 0xbefe0414, + 0xe0781000, 0x0403180a, 0x4a162481, 0x16161690, 0x4c1616ff, 0x00008000, 0xe0741000, 0x0202070b, + 0xbf810000, +}; + +alignas(256) constexpr std::array TessellationDomain{ + 0xf8000941, 0x00000000, 0x34020e84, 0x16020283, 0xe0341000, 0x04030a01, 0xe0341010, 0x04030c01, + 0xe0341020, 0x04030e01, 0xbf8c3f70, 0x08040af2, 0x08040d02, 0x10201505, 0x3e201906, 0x3e201d02, + 0x10221705, 0x3e221b06, 0x3e221f02, 0xd5510012, 0x040a0d05, 0x102424ff, 0x40400000, 0x7e260280, + 0x7e2802f2, 0xf80008cf, 0x14131110, 0xf800020f, 0x14131214, 0xbf810000, +}; + +alignas(256) constexpr std::array TessellationIndices{0x100, 0x102, 0x101, 0x010, 0x012, 0x011, 0x520, 0x522, 0x521}; + +std::vector compileTessellation(ShaderRecompiler::ShaderStage stage, std::span code, std::uint32_t partitioning, std::uint32_t outputControlPoints = 3u) { + using namespace ShaderRecompiler; + static constexpr std::array capabilities{spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityGroupNonUniformArithmetic}; + static const std::array hullUserData{0x1000u}; + static const std::array domainUserData{}; + const bool hull = stage == ShaderStage::TessellationControl; + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + RecompileRequest request{}; + request.shader = {stage, reinterpret_cast(code.data()), code, 0, {}}; + request.context.waveSize = 64; + request.context.userDataBaseRegister = hull ? 0u : 8u; + request.context.userData = hull ? std::span(hullUserData) : std::span(domainUserData); + request.context.vertex = ShaderVertexStageInfo{}; + request.context.memory = memory; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 64; + request.target.supportedCapabilities = capabilities; + request.target.fragmentShaderBarycentricEnabled = false; + request.layout.pushConstantSizeBytes = 128; + request.graphics = GraphicsCompileContext{hull ? 0u : 8u, {}, std::nullopt, TessellationConfiguration{3u, outputControlPoints, 1u, partitioning, 2u}, {reinterpret_cast(TessellationIndices.data()), static_cast(TessellationIndices.size()), 2, 1}}; + request.useCache = false; + return Recompile(request).spirv; +} + +struct TessellationFacts { + std::vector modes; + std::vector minBounds; + std::vector> inputComponents; +}; + +// Reads the spacing execution modes, the constants every GLSL.std.450 FMin clamps to, and the (row, column) +// constants of every access chain into an Input variable that has three indices (patch vertex, row, column). +TessellationFacts readTessellationFacts(const std::vector& spirv) { + TessellationFacts facts; + std::map constants; + std::map inputVariables; + for (int pass = 0; pass < 2; ++pass) { + for (std::size_t at = 5; at < spirv.size();) { + const auto op = spirv[at] & 0xffffu; + const auto count = spirv[at] >> 16u; + require(count != 0u && at + count <= spirv.size(), "malformed SPIR-V instruction"); + if (pass == 0 && op == spv::OpConstant && count == 4u) constants[spirv[at + 2]] = spirv[at + 3]; + if (pass == 0 && op == spv::OpVariable && spirv[at + 3] == spv::StorageClassInput) inputVariables[spirv[at + 2]] = 1u; + if (pass == 1 && op == spv::OpExecutionMode && (spirv[at + 2] == spv::ExecutionModeSpacingEqual || spirv[at + 2] == spv::ExecutionModeSpacingFractionalEven || spirv[at + 2] == spv::ExecutionModeSpacingFractionalOdd)) facts.modes.push_back(spirv[at + 2]); + if (pass == 1 && op == spv::OpExtInst && count == 7u && spirv[at + 4] == 37u && constants.count(spirv[at + 6]) != 0u) { + float bound; + std::memcpy(&bound, &constants.at(spirv[at + 6]), sizeof(bound)); + facts.minBounds.push_back(bound); + } + if (pass == 1 && op == spv::OpAccessChain && count == 7u && inputVariables.count(spirv[at + 3]) != 0u && constants.count(spirv[at + 5]) != 0u && constants.count(spirv[at + 6]) != 0u) facts.inputComponents.emplace_back(constants.at(spirv[at + 5]), constants.at(spirv[at + 6])); + at += count; + } + } + return facts; +} + +void verifyTessellationPartitioning() { + using namespace ShaderRecompiler; + const auto contains = [](const auto& values, auto value) { return std::find(values.begin(), values.end(), value) != values.end(); }; + struct Case { std::uint32_t partitioning; std::uint32_t spacing; float bound; const char* name; }; + for (const auto& partitioning : {Case{0u, spv::ExecutionModeSpacingEqual, 64.0f, "integer"}, Case{2u, spv::ExecutionModeSpacingFractionalOdd, 63.0f, "fractional-odd"}, Case{3u, spv::ExecutionModeSpacingFractionalEven, 64.0f, "fractional-even"}}) { + const auto domain = readTessellationFacts(compileTessellation(ShaderStage::TessellationEvaluation, TessellationDomain, partitioning.partitioning)); + require(domain.modes.size() == 1u && domain.modes[0] == partitioning.spacing, (std::string(partitioning.name) + " partitioning did not select its spacing execution mode").c_str()); + const auto hull = readTessellationFacts(compileTessellation(ShaderStage::TessellationControl, TessellationHull, partitioning.partitioning)); + require(contains(hull.minBounds, partitioning.bound), (std::string(partitioning.name) + " partitioning did not bound the tessellation factors at " + std::to_string(static_cast(partitioning.bound))).c_str()); + require(!contains(hull.minBounds, partitioning.bound == 64.0f ? 63.0f : 64.0f), (std::string(partitioning.name) + " partitioning also used the other factor bound").c_str()); + } + // A stride of 24 bytes is not a multiple of 16, so a read at a constant past the first control point places + // its attribute and component by the offset inside the slot (constant % stride), not by constant % 16. A + // single output control point per patch lets the hull read at 32 bytes without a per-invocation vertex term. + std::vector narrow(TessellationHull.begin(), TessellationHull.end()); + narrow[11] = 0x16002898u; // v_mul_u32_u24 v0, 24, v20: the control point stride + narrow[14] = 0x48u; // three control points of 24 bytes per patch + narrow[15] = 0xdbfc0020u; // ds_read_b128 at byte 32: bytes 8-23 of the second control point + narrow[20] = 0x10u; // one 16-byte output control point per patch + narrow[26] = 0xb06a0048u; + narrow[31] = 0xd8dc0c06u; + const auto slots = readTessellationFacts(compileTessellation(ShaderStage::TessellationControl, narrow, 2u, 1u)).inputComponents; + require(slots == std::vector>{{0u, 2u}, {0u, 3u}, {1u, 0u}, {1u, 1u}}, "a hull read past the first control point of a 24-byte slot did not place its components by the offset inside the slot"); + for (const std::uint32_t unsupported : {1u, 4u, 7u}) { + expectFailure([&] { static_cast(compileTessellation(ShaderStage::TessellationEvaluation, TessellationDomain, unsupported)); }, "unsupported tessellation configuration", "a tessellation partitioning other than integer, fractional-odd and fractional-even was accepted"); + expectFailure([&] { static_cast(compileTessellation(ShaderStage::TessellationControl, TessellationHull, unsupported)); }, "unsupported tessellation configuration", "a hull of an unsupported tessellation partitioning was accepted"); + } +} + void verifyNarrowVertexWave() { using namespace ShaderRecompiler; static constexpr std::array capabilities{spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityGroupNonUniformArithmetic, spv::CapabilityClipDistance}; @@ -1816,10 +1943,10 @@ void verifyPixelRequestSerialization() { const auto fields = [](const ShaderPixelStageInfo& value) { return std::tie(value.interpolatorCount, value.interpolatorSettings, value.wave32, value.inputAddr, value.hasPerspectiveCenterVgpr, value.perspectiveCentroid, value.posX, value.posY, - value.posZ, value.posW, value.frontFace, value.ancillary, value.sampleShading, + value.posZ, value.posW, value.frontFace, value.frontFaceAllBits, value.ancillary, value.sampleShading, value.noPerspective, value.linearCentroid, value.pixelKillEnable, value.depthExportEnable, value.sampleMaskExportEnable, value.earlyZ, value.executeOnNoop, value.conservativeZExport, value.orderedPixelShader, - value.targetOutputMode, value.targetExportMapping, value.alphaToCoverage); + value.targetOutputMode, value.targetExportMapping, value.alphaToCoverage, value.layerBase); }; const std::array code{0xbf810000u}; const std::array userData{0x12345678u, 0xabcdef01u, 0x87654321u}; @@ -1848,6 +1975,7 @@ void verifyPixelRequestSerialization() { .posZ = true, .posW = true, .frontFace = true, + .frontFaceAllBits = true, .ancillary = true, .sampleShading = true, .noPerspective = true, @@ -1863,6 +1991,7 @@ void verifyPixelRequestSerialization() { }; for (std::uint32_t i = 0; i < pixel.interpolatorSettings.size(); ++i) pixel.interpolatorSettings[i] = 0x10101010u + i; pixel.alphaToCoverage = true; + pixel.layerBase = 0x2a5u; const std::array, 3> mappings{{ {0x00u, 0xe4u, 0xc6u, 0x1bu, 0xffu, 0x80u, 0x55u, 0xaau}, {0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u}, @@ -1903,13 +2032,13 @@ void verifyPixelRequestSerialization() { minimal.context.pixel = ShaderPixelStageInfo{}; minimal.context.pixel->rasterPrimitive = RasterPrimitive::Rectangle; const auto encoded = serializer.Serialize(minimal); - require(requestPrefix(encoded, 8u) == "NVNQQRMAAAA=", "new requests did not use serialization version 19"); + require(requestPrefix(encoded, 8u) == "NVNQQRgAAAA=", "new requests did not use serialization version 24"); 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 < 20u; ++bytes) { expectFailure([&] { static_cast(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source, alpha-to-coverage, stencil reference export flag or primitive class was accepted"); } - for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQRQAAAA="}) { + for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQRkAAAA="}) { expectFailure([&] { static_cast(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted"); } static constexpr std::string_view version18Request{ @@ -3716,6 +3845,7 @@ int main(int argc, char** argv) { verifyProgramCounterRelativeData(); verifyLanesOutsideHostSubgroup(); verifyHalfWaveReduction(); + verifyTessellationPartitioning(); verifyNarrowVertexWave(); verifySingleLaneVertexWave(); verifyMeshConfiguration(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/FrontFaceAllBits.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/FrontFaceAllBits.cpp new file mode 100644 index 00000000000..d3f9b23635e --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/FrontFaceAllBits.cpp @@ -0,0 +1,133 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#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::uint32_t WaveSize = 64; +constexpr std::uint32_t Kept = 0x12345678u; +alignas(256) std::array Pixels{}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array PixelCode{ + 0xf8001801, 0x00000000, 0xbf810000, +}; + +constexpr std::array, 6> Corners{{ + {1.0f, 1.0f}, {1.0f, 15.0f}, {15.0f, 1.0f}, + {17.0f, 1.0f}, {31.0f, 1.0f}, {17.0f, 15.0f}, +}}; + +alignas(256) std::array, Corners.size()> Vertices{}; + +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}; +} + +void Draw(AgcDriver::VulkanDevice& device, bool allBits) { + Pixels.fill(Kept); + for (std::size_t index = 0; index < Corners.size(); ++index) { + Vertices[index] = {Corners[index][0] * 2.0f / Width - 1.0f, 1.0f - Corners[index][1] * 2.0f / Height, 0.5f, 1.0f}; + } + const auto target = device.Target(); + + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(Vertices.data(), 16u, static_cast(Vertices.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 = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::FrontFace); + pixel.frontFace = true; + pixel.frontFaceAllBits = allBits; + pixel.targetOutputMode[0] = 1; + pixel.targetExportMapping.fill(0xe4u); + const std::vector pixelUserData(8, 0u); + const std::array pixelMemory{{{reinterpret_cast(PixelCode.data()), std::as_bytes(std::span(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)} + }}; + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, WaveSize, WaveSize, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(Pixels.data()), {Width, Height}, VK_FORMAT_R32_UINT, sizeof(Pixels), 0xe4u}; + state.colors = {state.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.blend.colorWriteMask = 15; + state.blends = {state.blend}; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Vertices.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +void Check(std::uint32_t front, std::uint32_t back, std::string_view what) { + for (const auto& [x, y, isFront] : std::array, 2>{{{4, 4, true}, {20, 4, false}}}) { + const auto expected = isFront ? front : back; + const auto actual = Pixels[y * Width + x]; + char text[160]; + std::snprintf(text, sizeof(text), "%.*s: the %s-facing triangle read FRONT_FACE as 0x%08x, expected 0x%08x", static_cast(what.size()), what.data(), isFront ? "front" : "back", actual, expected); + Require(actual == expected, text); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Draw(*device, false); + Check(0x3f800000u, 0xbf800000u, "without FRONT_FACE_ALL_BITS"); + Draw(*device, true); + Check(0xffffffffu, 0u, "with FRONT_FACE_ALL_BITS"); + std::puts("front face all-bits 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 d84f2714a58..9074d4177c0 100644 --- a/core/libs/tests/PixelAncillary.cpp +++ b/core/libs/tests/PixelAncillary.cpp @@ -21,15 +21,17 @@ struct Row { bool frontFacing; RasterPrimitive primitive = RasterPrimitive::Triangle; bool layerExported = false; + std::uint32_t layerBase = 0; }; -constexpr std::array Rows {{{0u, 0u, false}, {0u, 0u, true}, {15u, 0x1FFFu, false}, {15u, 0x1FFFu, true}, {0u, 0x1FFFu, true}, {15u, 0u, false}, - {0u, 0x1FFFu, false, RasterPrimitive::Triangle, true}, {15u, 0x1FFFu, true, RasterPrimitive::Triangle, true}, {15u, 3u, false, RasterPrimitive::Triangle, true}, {0u, 0u, true, RasterPrimitive::Triangle, true}}}; +constexpr std::array Rows {{{0u, 0u, false}, {0u, 0u, true}, {15u, 0x1FFFu, false}, {15u, 0x1FFFu, true}, {0u, 0x1FFFu, true}, {15u, 0u, false}, + {0u, 0x1FFFu, false, RasterPrimitive::Triangle, true}, {15u, 0x1FFFu, true, RasterPrimitive::Triangle, true}, {15u, 3u, false, RasterPrimitive::Triangle, true}, {0u, 0u, true, RasterPrimitive::Triangle, true}, + {3u, 3u, false, RasterPrimitive::Triangle, true, 5u}, {15u, 9u, true, RasterPrimitive::Triangle, false, 7u}}}; static std::uint32_t Evaluate(const IrValue* value, const Row& row); static std::uint32_t BitField(std::uint32_t word, std::uint32_t offset, std::uint32_t count, bool isSigned); static void RequireField(const IrValue& user, StageInputKind kind, std::uint32_t fieldOffset, std::uint32_t count) { if (kind == StageInputKind::Layer) { - for (const Row& row : Rows) Require(Evaluate(&user, row) == (row.layerExported ? BitField(row.layer, fieldOffset, count, user.Opcode() == IrOpcode::BitFieldSExtract) : 0u)); + for (const Row& row : Rows) Require(Evaluate(&user, row) == BitField((row.layerExported ? row.layer : 0u) + row.layerBase, fieldOffset, count, user.Opcode() == IrOpcode::BitFieldSExtract)); return; } const IrValue* field = user.Argument(0)->Resolve(); @@ -161,12 +163,14 @@ static std::uint32_t Evaluate(const IrValue* value, const Row& row) { case StageInputKind::FrontFacing: return row.frontFacing ? 1u : 0u; case StageInputKind::RasterPrimitive: return static_cast(row.primitive); case StageInputKind::LayerExported: return row.layerExported ? 1u : 0u; + case StageInputKind::LayerBase: return row.layerBase; default: break; } break; case IrOpcode::ShiftLeftLogical32: return Evaluate(value->Argument(0), row) << Evaluate(value->Argument(1), row); case IrOpcode::BitwiseOr32: return Evaluate(value->Argument(0), row) | Evaluate(value->Argument(1), row); case IrOpcode::IMul32: return Evaluate(value->Argument(0), row) * Evaluate(value->Argument(1), row); + case IrOpcode::IAdd32: return Evaluate(value->Argument(0), row) + Evaluate(value->Argument(1), row); case IrOpcode::BitFieldUExtract: case IrOpcode::BitFieldSExtract: return BitField(Evaluate(value->Argument(0), row), Evaluate(value->Argument(1), row), Evaluate(value->Argument(2), row), value->Opcode() == IrOpcode::BitFieldSExtract); @@ -177,7 +181,7 @@ static std::uint32_t Evaluate(const IrValue* value, const Row& row) { throw std::runtime_error("packed pixel ancillary evaluation reached an unsupported value"); } static std::uint32_t PackedWord(const Row& row) { - return UnmodelledBits | (row.sample << 8u) | (row.layerExported ? row.layer << 16u : 0u); + return UnmodelledBits | (row.sample << 8u) | (((row.layerExported ? row.layer : 0u) + row.layerBase) << 16u); } static void Values(IrOpcode opcode, std::uint32_t offset, std::uint32_t count, bool throughSelect) { IrProgram program; @@ -477,11 +481,12 @@ static void ExportedWholeWord() { LowerMasked(program, true); Require(!AncillaryRemains(program) && program.Info().rasterPrimitive); Require(BuiltinRemains(program, StageInputKind::LayerExported)); + Require(BuiltinRemains(program, StageInputKind::LayerBase)); 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}, - {15u, 0x7FFu, false, RasterPrimitive::Point, true}, {3u, 5u, false, RasterPrimitive::Triangle, true}}}; + 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}, + {15u, 0x7FFu, false, RasterPrimitive::Point, true}, {3u, 5u, false, RasterPrimitive::Triangle, true}, {3u, 5u, false, RasterPrimitive::Triangle, true, 4u}, {0u, 0u, false, RasterPrimitive::Line, false, 6u}}}; for (const Row& row : words) { - Require(Evaluate(word, row) == (static_cast(row.primitive) | (row.sample << 8u) | (row.layerExported ? row.layer << 16u : 0u))); + Require(Evaluate(word, row) == (static_cast(row.primitive) | (row.sample << 8u) | (((row.layerExported ? row.layer : 0u) + row.layerBase) << 16u))); } } static void WholeWordReplacesRefusal(HelperRead read) { diff --git a/core/shader/recompiler/CacheKey.hpp b/core/shader/recompiler/CacheKey.hpp index 89c6cf33171..3ffc9cab313 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -165,6 +165,7 @@ class RecompileCacheKey { append(key, value.posZ); append(key, value.posW); append(key, value.frontFace); + append(key, value.frontFaceAllBits); append(key, value.ancillary); append(key, value.sampleShading); append(key, value.noPerspective); diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index 57256425467..c818b282756 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -336,6 +336,8 @@ void writePixelInfo(Writer& writer, const ShaderPixelStageInfo& info) { writer.WriteBool(info.stencilRefExportEnable); writer.WriteU8(static_cast(info.rasterPrimitive)); writer.WriteBool(info.layerExported); + writer.WriteBool(info.frontFaceAllBits); + writer.WriteU32(info.layerBase); } ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { @@ -387,6 +389,8 @@ ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { if (version >= 17u) info.stencilRefExportEnable = reader.ReadBool(); if (version >= 19u) info.rasterPrimitive = readRasterPrimitive(reader); if (version >= 23u) info.layerExported = reader.ReadBool(); + if (version >= 24u) info.frontFaceAllBits = reader.ReadBool(); + if (version >= 24u) info.layerBase = reader.ReadU32(); 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) | @@ -750,7 +754,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const std::string buffer; Writer writer(buffer); writer.WriteU32(0x41505335u); - writer.WriteU32(23u); + writer.WriteU32(24u); writeShaderBinary(writer, request.shader); writeGuestContext(writer, request.context); writeSpirvTarget(writer, request.target); @@ -782,7 +786,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 > 23u) throw std::runtime_error("unsupported recompile request serialization version"); + if (version < 1u || version > 24u) 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 8879b166be8..34ffc8790c4 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp @@ -37,6 +37,7 @@ struct ShaderInfo { bool dispatchThreadLimit = false; bool rasterPrimitive = false; bool layerExported = false; + bool layerBase = 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 e83cf8bf6a9..0d7e98342c9 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp @@ -45,6 +45,7 @@ enum class StageInputKind { HelperInvocation, RasterPrimitive, LayerExported, + LayerBase, }; enum class StageOutputKind { diff --git a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp index 35339f2ed35..44a8916ef58 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp @@ -162,6 +162,7 @@ struct ShaderPixelInputInfo { bool psPosZ = false; bool psPosW = false; bool psFrontFace = false; + bool psFrontFaceAllBits = false; bool psAncillary = false; bool psNoPerspective = false; bool psPixelKillEnable = false; diff --git a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp index 51ea739e123..52da7ad43cb 100644 --- a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp +++ b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp @@ -418,7 +418,10 @@ void lowerPackedAncillary(IrProgram& program, IrBuilder& builder, IrValue& ancil } std::array fields {}; const auto field = [&](std::size_t index) -> IrValue& { - if (fields[index] == nullptr && index == 1u) fields[index] = &insertBefore(program, ancillary, IrOpcode::IMul32, insertBuiltin(program, builder, ancillary, StageInputKind::Layer), insertBuiltin(program, builder, ancillary, StageInputKind::LayerExported)); + if (fields[index] == nullptr && index == 1u) { + IrValue& exportedLayer = insertBefore(program, ancillary, IrOpcode::IMul32, insertBuiltin(program, builder, ancillary, StageInputKind::Layer), insertBuiltin(program, builder, ancillary, StageInputKind::LayerExported)); + fields[index] = &insertBefore(program, ancillary, IrOpcode::IAdd32, exportedLayer, insertBuiltin(program, builder, ancillary, StageInputKind::LayerBase)); + } if (fields[index] == nullptr) { fields[index] = &insertBuiltin(program, builder, ancillary, StageInputKind::SampleId); } @@ -897,7 +900,8 @@ bool ConstantFolder::ComposePackedAncillary(IrProgram& program) const { 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& exportedLayer = insertBefore(program, *word, IrOpcode::IMul32, insertBuiltin(program, builder, *word, StageInputKind::Layer), insertBuiltin(program, builder, *word, StageInputKind::LayerExported)); - IrValue& layer = insertBefore(program, *word, IrOpcode::ShiftLeftLogical32, exportedLayer, builder.Constant(16u)); + IrValue& totalLayer = insertBefore(program, *word, IrOpcode::IAdd32, exportedLayer, insertBuiltin(program, builder, *word, StageInputKind::LayerBase)); + IrValue& layer = insertBefore(program, *word, IrOpcode::ShiftLeftLogical32, totalLayer, builder.Constant(16u)); IrValue& fields = insertBefore(program, *word, IrOpcode::BitwiseOr32, sample, layer); word->ReplaceAllUsesWith(&insertBefore(program, *word, IrOpcode::BitwiseOr32, primitive, fields)); } diff --git a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp index d514b395a47..40097adec4c 100644 --- a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp +++ b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp @@ -145,6 +145,7 @@ void ValidateValueReferences(const IrProgram& program, ShaderStageInputInfo inpu case StageInputKind::SampleId: case StageInputKind::RasterPrimitive: case StageInputKind::LayerExported: + case StageInputKind::LayerBase: if (program.Resources().stage != IrShaderStage::Pixel || component != 0u) { return Fail("typed pixel scalar input is invalid"); } @@ -394,6 +395,9 @@ void CollectBuiltinInputs(const IrProgram& program, ShaderInfo& info) { case StageInputKind::LayerExported: info.layerExported = true; break; + case StageInputKind::LayerBase: + info.layerBase = true; + break; case StageInputKind::PackedAncillary: case StageInputKind::Parameter: break; diff --git a/core/shader/recompiler/Optimization/src/TessellationLowering.cpp b/core/shader/recompiler/Optimization/src/TessellationLowering.cpp index 1e663d60658..67930aa0ecd 100644 --- a/core/shader/recompiler/Optimization/src/TessellationLowering.cpp +++ b/core/shader/recompiler/Optimization/src/TessellationLowering.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -69,39 +70,169 @@ bool IsLaneId(const IrValue* value) { return value != nullptr && value->Resolve()->Opcode() == IrOpcode::LaneId; } -IrValue* BallotSource(IrValue* raw, std::uint32_t word) { - IrValue* value = raw->Resolve(); - if (value->Opcode() != IrOpcode::CompositeExtractU32x4 || !IsImmediate(value->Argument(1), word)) return nullptr; - IrValue* ballot = value->Argument(0)->Resolve(); - return ballot->Opcode() == IrOpcode::Ballot ? ballot->Argument(0)->Resolve() : nullptr; -} - bool IsTrue(const IrValue* value) { return value != nullptr && value->HasImmediate() && value->Type() == IrType::Bool && value->ImmediateBool(); } -IrValue* HostLaneWord(IrValue* raw, std::uint32_t word) { - IrValue* value = raw->Resolve(); - while (value->Opcode() == IrOpcode::BitwiseAnd32 && IsTrue(BallotSource(value->Argument(1), word))) value = value->Argument(0)->Resolve(); - return value; +using LaneWords = std::pair; + +IrValue* BallotPredicate(IrValue* word, std::uint32_t half) { + IrValue* ballot = word->Argument(0)->Resolve(); + return ballot->Opcode() == IrOpcode::Ballot && IsImmediate(word->Argument(1), half) ? ballot->Argument(0)->Resolve() : nullptr; } -IrValue* SavedLaneBit(IrValue* value) { - if (value->Opcode() != IrOpcode::INotEqual32) return nullptr; +bool SamePredicate(const IrValue* lhs, const IrValue* rhs) { + if (lhs == nullptr || rhs == nullptr) return false; + if (lhs == rhs) return true; + return lhs->HasImmediate() && rhs->HasImmediate() && lhs->Type() == IrType::Bool && rhs->Type() == IrType::Bool && lhs->ImmediateBool() == rhs->ImmediateBool(); +} + +std::optional LaneBitWords(IrValue* value, std::uint32_t waveSize) { + if (value->Opcode() != IrOpcode::INotEqual32) return std::nullopt; IrValue* masked = VariableOperand(value, 0u); - if (masked == nullptr || masked->Opcode() != IrOpcode::BitwiseAnd32) return nullptr; + if (masked == nullptr || masked->Opcode() != IrOpcode::BitwiseAnd32) return std::nullopt; IrValue* shifted = VariableOperand(masked, 1u); - if (shifted == nullptr || shifted->Opcode() != IrOpcode::ShiftRightLogical32) return nullptr; + if (shifted == nullptr || shifted->Opcode() != IrOpcode::ShiftRightLogical32) return std::nullopt; IrValue* bit = shifted->Argument(1)->Resolve(); - if (bit->Opcode() != IrOpcode::BitwiseAnd32 || !IsLaneId(VariableOperand(bit, 31u))) return nullptr; + if (bit->Opcode() != IrOpcode::BitwiseAnd32 || !IsLaneId(VariableOperand(bit, 31u))) return std::nullopt; IrValue* word = shifted->Argument(0)->Resolve(); - if (word->Opcode() != IrOpcode::SelectU32) return BallotSource(HostLaneWord(word, 0u), 0u); + if (waveSize != 64u) return LaneWords{word, nullptr}; + if (word->Opcode() != IrOpcode::SelectU32) return LaneWords{word, word}; IrValue* low = word->Argument(0)->Resolve(); - if (low->Opcode() != IrOpcode::ULessThan32 || !IsLaneId(low->Argument(0)) || !IsImmediate(low->Argument(1), 32u)) return nullptr; - IrValue* source = BallotSource(HostLaneWord(word->Argument(1), 0u), 0u); - return source != nullptr && BallotSource(HostLaneWord(word->Argument(2), 1u), 1u) == source ? source : nullptr; + if (low->Opcode() != IrOpcode::ULessThan32 || !IsLaneId(low->Argument(0)) || !IsImmediate(low->Argument(1), 32u)) return LaneWords{word, word}; + return LaneWords{word->Argument(1)->Resolve(), word->Argument(2)->Resolve()}; } +class LaneBits { +public: + explicit LaneBits(IrProgram& program) : program(program) {} + + IrValue* Of(const LaneWords& words) { + std::vector visited; + if (matches(words, visited)) return &build(words); + for (const auto& pair : visited) { + if (known.at(pair)) known.erase(pair); + } + return nullptr; + } + +private: + static LaneWords argument(const LaneWords& words, std::size_t index) { + return {words.first->Argument(index)->Resolve(), words.second == nullptr ? nullptr : words.second->Argument(index)->Resolve()}; + } + + bool matches(const LaneWords& words, std::vector& visited) { + if (const auto found = known.find(words); found != known.end()) return found->second; + known.emplace(words, true); + visited.push_back(words); + const bool result = check(words, visited); + if (!result) known[words] = false; + return result; + } + + bool check(const LaneWords& words, std::vector& visited) { + const auto [low, high] = words; + if (low->HasImmediate()) { + if (low->Type() != IrType::U32 || (low->ImmediateU32() != 0u && low->ImmediateU32() != 0xffffffffu)) return false; + return high == nullptr || (high->HasImmediate() && high->Type() == IrType::U32 && high->ImmediateU32() == low->ImmediateU32()); + } + if (high != nullptr && (high->HasImmediate() || high->Opcode() != low->Opcode())) return false; + switch (low->Opcode()) { + case IrOpcode::CompositeExtractU32x4: { + if (BallotPredicate(low, 0u) == nullptr) return false; + return high == nullptr || SamePredicate(BallotPredicate(low, 0u), BallotPredicate(high, 1u)); + } + case IrOpcode::BitwiseAnd32: + case IrOpcode::BitwiseOr32: + case IrOpcode::BitwiseXor32: + return matches(argument(words, 0), visited) && matches(argument(words, 1), visited); + case IrOpcode::BitwiseNot32: + return matches(argument(words, 0), visited); + case IrOpcode::SelectU32: + if (high != nullptr && high->Argument(0)->Resolve() != low->Argument(0)->Resolve()) return false; + return matches(argument(words, 1), visited) && matches(argument(words, 2), visited); + case IrOpcode::Phi: + if (low->ArgumentCount() == 0 || low->ArgumentCount() != low->PhiBlockCount()) return false; + if (high != nullptr && (high->Parent() != low->Parent() || high->ArgumentCount() != low->ArgumentCount() || high->PhiBlockCount() != low->PhiBlockCount())) return false; + for (std::size_t index = 0; index < low->ArgumentCount(); ++index) { + if (high != nullptr && high->PhiBlock(index) != low->PhiBlock(index)) return false; + if (!matches(argument(words, index), visited)) return false; + } + return true; + default: + return false; + } + } + + IrValue& boolean(bool value) { + IrValue& result = program.CreateValue(IrOpcode::Void, IrType::Bool); + result.SetImmediateBool(value); + return result; + } + + IrValue& emit(IrValue* position, IrOpcode opcode, std::initializer_list arguments) { + IrValue& value = program.CreateValue(opcode, IrType::Bool); + for (IrValue* argument : arguments) value.AddArgument(argument); + position->Parent()->InsertInstructionBefore(position, &value); + return value; + } + + IrValue& combine(IrValue* position, IrOpcode opcode, IrValue& lhs, IrValue& rhs) { + for (const auto& [fixed, other] : {std::pair{&lhs, &rhs}, std::pair{&rhs, &lhs}}) { + if (!fixed->HasImmediate()) continue; + if (opcode == IrOpcode::LogicalAnd) return fixed->ImmediateBool() ? *other : *fixed; + if (opcode == IrOpcode::LogicalOr) return fixed->ImmediateBool() ? *fixed : *other; + if (!fixed->ImmediateBool()) return *other; + } + return emit(position, opcode, {&lhs, &rhs}); + } + + IrValue& build(const LaneWords& words) { + IrValue* low = words.first; + if (low->HasImmediate()) return boolean(low->ImmediateU32() != 0u); + if (const auto found = built.find(words); found != built.end()) return *found->second; + IrValue* result = nullptr; + switch (low->Opcode()) { + case IrOpcode::CompositeExtractU32x4: + result = BallotPredicate(low, 0u); + break; + case IrOpcode::BitwiseAnd32: + case IrOpcode::BitwiseOr32: + case IrOpcode::BitwiseXor32: { + const auto opcode = low->Opcode() == IrOpcode::BitwiseAnd32 ? IrOpcode::LogicalAnd : low->Opcode() == IrOpcode::BitwiseOr32 ? IrOpcode::LogicalOr : IrOpcode::LogicalXor; + IrValue& lhs = build(argument(words, 0)); + IrValue& rhs = build(argument(words, 1)); + result = &combine(low, opcode, lhs, rhs); + break; + } + case IrOpcode::BitwiseNot32: { + IrValue& operand = build(argument(words, 0)); + result = operand.HasImmediate() ? &boolean(!operand.ImmediateBool()) : &emit(low, IrOpcode::LogicalNot, {&operand}); + break; + } + case IrOpcode::SelectU32: { + IrValue& chosen = build(argument(words, 1)); + IrValue& other = build(argument(words, 2)); + result = &emit(low, IrOpcode::SelectU1, {low->Argument(0)->Resolve(), &chosen, &other}); + break; + } + default: { + IrValue& phi = program.CreateValue(IrOpcode::Phi, IrType::Bool); + low->Parent()->InsertInstructionBefore(low, &phi); + built.emplace(words, &phi); + for (std::size_t index = 0; index < low->ArgumentCount(); ++index) phi.AddPhiOperand(low->PhiBlock(index), &build(argument(words, index))); + return phi; + } + } + built.emplace(words, result); + return *result; + } + + IrProgram& program; + std::map known; + std::map built; +}; + bool Implies(IrValue* rawPredicate, IrValue* rawCondition) { IrValue* predicate = rawPredicate->Resolve(); IrValue* condition = rawCondition->Resolve(); @@ -420,17 +551,14 @@ class Lowering { : program(program), tessellation(tessellation), stage(program.Resources().stage), evaluator(stage == IrShaderStage::TessellationControl ? tessellation.outputControlPoints : 0u, stage == IrShaderStage::Local ? LocalVertexIndexCount : PatchIndexCount) {} void Run() { - for (IrBlock* block : program.BlockOrder()) { - for (IrValue* inst : block->Instructions()) { - if (IrValue* source = SavedLaneBit(inst)) inst->ReplaceAllUsesWith(source); - } - } + if (stage == IrShaderStage::TessellationControl) lowerOwnLaneBits(); collect(); derive(); for (const auto& access : accesses) rewrite(access); if (stage == IrShaderStage::TessellationEvaluation) removePrimitiveExports(); constexpr DeadCodeEliminator deadCodeEliminator; deadCodeEliminator.Eliminate(program); + if (stage == IrShaderStage::TessellationControl) removeUnobservedCycles(); std::vector relative; for (IrBlock* block : program.BlockOrder()) { for (IrValue* inst : block->Instructions()) { @@ -452,6 +580,17 @@ class Lowering { } private: + void lowerOwnLaneBits() { + LaneBits laneBits(program); + for (IrBlock* block : program.BlockOrder()) { + for (IrValue* inst : block->Instructions()) { + const auto words = LaneBitWords(inst, program.WaveSize()); + if (!words) continue; + if (IrValue* bit = laneBits.Of(*words)) inst->ReplaceAllUsesWith(bit); + } + } + } + void collect() { const auto& memoryInfo = program.Resources().memoryInfo; for (IrBlock* block : program.BlockOrder()) { @@ -512,11 +651,16 @@ class Lowering { void derive() { const auto inputs = static_cast(tessellation.inputControlPoints); const auto outputs = static_cast(tessellation.outputControlPoints); + const auto soleControlPoint = [&](const Access& access) { + const auto end = access.address.constant + 4 * static_cast(access.components); + return outputs == 1 && access.address.vertex == 0 && access.address.relative > 0 && access.address.constant >= 0 && end <= access.address.relative; + }; for (const auto& access : accesses) { if (access.kind == TessellationAttribute::LocalOutput) agree(localStride, access.address.relative, access.pc); if (access.kind == TessellationAttribute::ControlInput && access.address.vertex != 0) agree(localStride, access.address.vertex, access.pc); - if (access.kind == TessellationAttribute::ControlInput && access.address.dynamic && access.address.relative > 0 && access.address.relative % inputs == 0) agree(localStride, access.address.relative / inputs, access.pc); + if (access.kind == TessellationAttribute::ControlInput && (access.address.dynamic || access.address.vertex == 0) && access.address.relative > 0 && access.address.relative % inputs == 0) agree(localStride, access.address.relative / inputs, access.pc); if (access.kind == TessellationAttribute::ControlOutput && access.address.vertex != 0) agree(controlStride, access.address.vertex, access.pc); + if (access.kind == TessellationAttribute::ControlOutput && soleControlPoint(access)) agree(controlStride, access.address.relative, access.pc); if (access.kind == TessellationAttribute::EvaluationInput) { if (access.address.relative <= 0 || access.address.relative % outputs != 0) Fail("domain shader reads of patch constants or other patches are unsupported", access.pc); agree(controlStride, access.address.relative / outputs, access.pc); @@ -538,7 +682,7 @@ class Lowering { if (address.dynamic && (address.dynamicScale == 0 || address.dynamicScale % localStride != 0)) Fail("the hull shader reads LDS at a computed offset that is not a whole number of control points", access.pc); break; case TessellationAttribute::ControlOutput: - if (address.vertex == 0) { + if (address.vertex == 0 && !soleControlPoint(access)) { if (controlStride == 0u || address.constant < outputs * controlStride) Fail("the hull shader writes off-chip memory outside its control points and patch constants", access.pc); if (patchStride && *patchStride != address.relative) Fail("inconsistent patch-constant stride", access.pc); patchStride = address.relative; @@ -602,7 +746,38 @@ class Lowering { } } + void removeUnobservedCycles() { + std::unordered_set observed; + std::vector pending; + for (IrBlock* block : program.BlockOrder()) { + for (IrValue* inst : block->Instructions()) { + if (inst->MayHaveSideEffects() && observed.insert(inst).second) pending.push_back(inst); + } + } + while (!pending.empty()) { + IrValue* value = pending.back(); + pending.pop_back(); + for (IrValue* argument : value->Arguments()) { + if (argument != nullptr && observed.insert(argument).second) pending.push_back(argument); + } + } + std::vector unobserved; + for (IrBlock* block : program.BlockOrder()) { + for (IrValue* inst : block->Instructions()) { + if (!observed.contains(inst)) unobserved.push_back(inst); + } + } + for (IrValue* inst : unobserved) { + for (std::size_t index = 0; index < inst->ArgumentCount(); ++index) inst->ReplaceArgument(index, nullptr); + } + for (IrValue* inst : unobserved) { + inst->Parent()->RemoveInstruction(inst); + inst->Invalidate(); + } + } + void requireFirstInvocation(const Access& access) { + if (tessellation.outputControlPoints == 1u) return; std::unordered_set path; if (!FirstInvocationOnly(access.predicate, evaluator, path)) Fail("per-patch tessellation output is not written by control point 0 alone", access.pc); } diff --git a/core/shader/recompiler/PipelineSpecialization.hpp b/core/shader/recompiler/PipelineSpecialization.hpp index 44e1e996fe3..ed26ea277e5 100644 --- a/core/shader/recompiler/PipelineSpecialization.hpp +++ b/core/shader/recompiler/PipelineSpecialization.hpp @@ -28,6 +28,7 @@ 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 LayerExported = 8234u; +inline constexpr std::uint32_t LayerBase = 8235u; 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 ec1cc6386eb..3fac66aab4e 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -902,6 +902,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil moduleKey.push_back(request.context.pixel->dualSourceBlend ? 1u : 0u); if (variant.info.info.rasterPrimitive) moduleKey.push_back(static_cast(request.context.pixel->rasterPrimitive)); if (variant.info.info.layerExported) moduleKey.push_back(request.context.pixel->layerExported ? 1u : 0u); + if (variant.info.info.layerBase) moduleKey.push_back(request.context.pixel->layerBase); } result.vertexAttributes.reserve(result.vertexInputs.size()); std::array vertexClasses{}; @@ -950,6 +951,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil constants.push_back({PipelineSpecialization::DualSourceBlend, moduleKey[index++]}); if (variant.info.info.rasterPrimitive) constants.push_back({PipelineSpecialization::RasterPrimitive, moduleKey[index++]}); if (variant.info.info.layerExported) constants.push_back({PipelineSpecialization::LayerExported, moduleKey[index++]}); + if (variant.info.info.layerBase) constants.push_back({PipelineSpecialization::LayerBase, moduleKey[index++]}); } if (!artifact.vertexInputPatches.empty()) { for (const auto& input : result.vertexInputs) { @@ -1180,6 +1182,7 @@ std::uint64_t snapshotHash(const RecompileRequest& request, const ResourceSnapsh mix(request.context.pixel->dualSourceBlend); mix(static_cast(request.context.pixel->rasterPrimitive)); mix(request.context.pixel->layerExported); + mix(static_cast(request.context.pixel->layerBase)); } 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 35eaeb0c556..97388db220f 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -132,6 +132,7 @@ struct ShaderPixelStageInfo { bool posZ; bool posW; bool frontFace; + bool frontFaceAllBits; bool ancillary; bool sampleShading; bool noPerspective; @@ -151,6 +152,7 @@ struct ShaderPixelStageInfo { bool stencilRefExportEnable; RasterPrimitive rasterPrimitive = RasterPrimitive::Triangle; bool layerExported = false; + std::uint32_t layerBase = 0; }; struct ShaderVertexBufferResource { diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index 4a483318df9..dd7dd0b5439 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -114,7 +114,7 @@ constexpr std::size_t fieldCount() { } } -static_assert(fieldCount() == 23, "ShaderInfo changed: update the info encoder"); +static_assert(fieldCount() == 24, "ShaderInfo changed: update the info encoder"); static_assert(fieldCount() == 13, "BufferResource changed: update the info encoder"); static_assert(fieldCount() == 40, "ImageResource changed: update the info encoder"); static_assert(fieldCount() == 9, "SamplerResource changed: update the info encoder"); @@ -737,6 +737,7 @@ void encodeShaderInfo(Writer& writer, const ShaderInfo& info) { writer.Value(info.dispatchThreadLimit); writer.Value(info.rasterPrimitive); writer.Value(info.layerExported); + writer.Value(info.layerBase); } void decodeShaderInfo(Reader& reader, ShaderInfo& info) { @@ -847,6 +848,7 @@ void decodeShaderInfo(Reader& reader, ShaderInfo& info) { reader.Value(info.dispatchThreadLimit); reader.Value(info.rasterPrimitive); reader.Value(info.layerExported); + reader.Value(info.layerBase); } void encodeInfo(Writer& writer, const CompiledShaderInfo& compiled) { diff --git a/core/shader/recompiler/ShaderDiskCache.hpp b/core/shader/recompiler/ShaderDiskCache.hpp index d219baef357..4a7d20f7a68 100644 --- a/core/shader/recompiler/ShaderDiskCache.hpp +++ b/core/shader/recompiler/ShaderDiskCache.hpp @@ -21,7 +21,8 @@ namespace ShaderRecompiler::ShaderDiskCache { // 35: fork format 34 plus the shader output changes of upstream PRs 5110-5498 and 907 (U6: shader semantics). // 36: fork format 35 plus the image-mode ImageResource fields of upstream PRs 5375, 5461 and 5509 and // BufferResource::formattedWritten of upstream PR 5172 (U8a: Metal image-mode views). -inline constexpr std::uint32_t FormatVersion = 36; +// 37: fork format 36 plus upstream PR 5500 (layerBase in the pixel info and the request key; U7). +inline constexpr std::uint32_t FormatVersion = 37; enum class LoadStatus { Loaded, diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 82178f6995c..c44861accbe 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -51,6 +51,9 @@ std::uint32_t EmitBuiltinU32(SpirvEmitterState& state, StageInputKind kind, std: if (kind == StageInputKind::LayerExported) { return state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::LayerExported, 0u); } + if (kind == StageInputKind::LayerBase) { + return state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::LayerBase, 0u); + } if (state.laneCount == 2 && (kind == StageInputKind::LocalInvocationId || kind == StageInputKind::GlobalInvocationId)) { const auto* workgroup = ShaderWorkgroupInputFor(state); if (workgroup == nullptr) { @@ -548,7 +551,7 @@ std::uint32_t TessellationPointer(SpirvValueEmitContext& ctx, const IrValue& ins if (kind == TessellationAttribute::ControlInput && scaled != nullptr && sum->Argument(1)->HasImmediate() && scaled->Opcode() == IrOpcode::IMul32 && scaled->Argument(1)->HasImmediate() && scaled->Argument(1)->ImmediateU32() == stride) { const auto constant = sum->Argument(1)->ImmediateU32(); const auto vertex = inPatch(EmitBinaryU32(state, spv::OpIAdd, ctx.Arg(*scaled, 0), ConstantU32(state, constant / stride))); - state.module.AddFunction(spv::OpAccessChain, type, pointer, variable, vertex, ConstantU32(state, constant % stride / 16u), ConstantU32(state, constant % 16u / 4u)); + state.module.AddFunction(spv::OpAccessChain, type, pointer, variable, vertex, ConstantU32(state, constant % stride / 16u), ConstantU32(state, constant % stride % 16u / 4u)); return pointer; } const auto offset = kind == TessellationAttribute::PatchOutput ? address : EmitBinaryU32(state, spv::OpUMod, address, ConstantU32(state, stride)); @@ -631,7 +634,7 @@ void DefineTessellationInterfaces(SpirvEmitterState& state) { void DefineTessellationExecutionModes(SpirvEmitterState& state) { const auto& tess = state.inputInfo.vertex->tess; - if (tess.domain != 1u || tess.partitioning != 2u || tess.outputTopology != 2u) { + if (tess.domain != 1u || (tess.partitioning != 0u && tess.partitioning != 2u && tess.partitioning != 3u) || tess.outputTopology != 2u) { throw std::runtime_error("unsupported tessellation domain, partitioning, or output topology"); } state.module.EmitCapability(spv::CapabilityTessellation); @@ -639,7 +642,7 @@ void DefineTessellationExecutionModes(SpirvEmitterState& state) { state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeOutputVertices, tess.outputControlPoints); } else { state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeTriangles); - state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeSpacingFractionalOdd); + state.module.AddExecutionMode(state.mainFunc, tess.partitioning == 0u ? spv::ExecutionModeSpacingEqual : tess.partitioning == 3u ? spv::ExecutionModeSpacingFractionalEven : spv::ExecutionModeSpacingFractionalOdd); state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeVertexOrderCw); } } @@ -1246,7 +1249,7 @@ void EmitSetTessellationAttribute(SpirvValueEmitContext& ctx, const IrValue& ins const auto inside = state.module.AllocateId(); state.module.AddFunction(spv::OpFOrdGreaterThan, TypeBool(state), inside, floating, lower); const auto clamped = state.module.AllocateId(); - state.module.AddFunction(spv::OpExtInst, TypeF32(state), clamped, GlslStd450(state), GLSLstd450FMin, floating, ConstantF32Value(state, 63.0f)); + state.module.AddFunction(spv::OpExtInst, TypeF32(state), clamped, GlslStd450(state), GLSLstd450FMin, floating, ConstantF32Value(state, state.inputInfo.vertex->tess.partitioning == 2u ? 63.0f : 64.0f)); value = state.module.AllocateId(); state.module.AddFunction(spv::OpSelect, TypeF32(state), value, inside, clamped, outer ? floating : lower); } diff --git a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp index eb760cee8ec..f663697f360 100644 --- a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp +++ b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp @@ -366,7 +366,9 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO setVector(static_cast(vgpr(PixelInput::PositionW)), entryIr.BitCastU32(w)); } if (loaded(PixelInput::FrontFace)) { - setVector(static_cast(vgpr(PixelInput::FrontFace)), builtin(StageInputKind::FrontFacing)); + // The front value under FRONT_FACE_ALL_BITS is not measured (docs/dev/TechnicalDebt.md). + IrValue& facing = builtin(StageInputKind::FrontFacing); + setVector(static_cast(vgpr(PixelInput::FrontFace)), ps->psFrontFaceAllBits ? entryIr.Select(entryIr.IEqual(facing, entryIr.Constant(0x3f800000u)), entryIr.Constant(0xffffffffu), entryIr.Constant(0u)) : facing); } if (loaded(PixelInput::Ancillary)) { setVector(static_cast(vgpr(PixelInput::Ancillary)), builtin(StageInputKind::PackedAncillary)); diff --git a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp index 88f8ef6ef9b..1e9b8d7477e 100644 --- a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp +++ b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp @@ -134,6 +134,7 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues pixelStorage.psPosZ = pixel.posZ; pixelStorage.psPosW = pixel.posW; pixelStorage.psFrontFace = pixel.frontFace; + pixelStorage.psFrontFaceAllBits = pixel.frontFaceAllBits; pixelStorage.psAncillary = pixel.ancillary; pixelStorage.psNoPerspective = pixel.noPerspective; pixelStorage.psPixelKillEnable = pixel.pixelKillEnable; @@ -183,7 +184,7 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues if (stage == ShaderStageKind::Local || stage == ShaderStageKind::TessellationControl || stage == ShaderStageKind::TessellationEvaluation) { if (tessellation == nullptr) throw std::runtime_error("ShaderInputInfoBuilder: tessellation configuration is missing"); if (tessellation->inputControlPoints == 0u || tessellation->inputControlPoints > 32u || tessellation->outputControlPoints == 0u || tessellation->outputControlPoints > 32u) throw std::runtime_error("ShaderInputInfoBuilder: invalid tessellation control-point counts"); - if (tessellation->domain != 1u || tessellation->partitioning != 2u || tessellation->outputTopology != 2u) throw std::runtime_error("ShaderInputInfoBuilder: unsupported tessellation configuration"); + if (tessellation->domain != 1u || (tessellation->partitioning != 0u && tessellation->partitioning != 2u && tessellation->partitioning != 3u) || tessellation->outputTopology != 2u) throw std::runtime_error("ShaderInputInfoBuilder: unsupported tessellation configuration"); auto& target = vertexStorage.tess; target.inputControlPoints = tessellation->inputControlPoints; target.outputControlPoints = tessellation->outputControlPoints; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0e789c937e1..c2b31477005 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -80,7 +80,8 @@ Throughout the project, every function at every stage either **does exactly what - [sceKernelGetAppInfo](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - only `app_id` and the title ID (offset 0x10, up to 9 characters plus a terminator) are written; callers declare the structure with different sizes, so the rest is left alone. `app_id` is 1, the running title's id in libSceSystemService, which is a guess, not something measured. The error codes follow shadPS4: `EPERM` for another process, and 0 without writing anything for a null pointer. - [libScePlayGo](../../core/libs/prx/libScePlayGo/Export.cpp) - the chunk set comes from `playgo-chunkdefs.xml` (every listed chunk plus chunks 0 through the default chunk), otherwise from `sce_sys/playgo-chunk.dat`, whose `plgx` header is read as holding the chunk count as a 16-bit value at offset 0x0A and ids 0 to count - 1 are taken as valid. The field was identified on four dumps, where exactly that many 0x20-byte entries follow from offset 0x100 (RIDE 4 PPSA01599: 35, Persona 5 Royal PPSA05110: 25, LEGO Horizon Adventures PPSA14632 and Kena PPSA01802: 1); the rest of the format is not decoded and ids are assumed to be contiguous. Every listed chunk is reported as installed (`LOCAL_FAST`). Without either file every chunk id is accepted while `scePlayGoGetChunkId` lists only chunk 0; the console's answer for an id outside the package's chunk table is not checked. - [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. The sample index is the SampleId builtin, an equality inferred, not measured. The render target index is 0: vertex layer exports are ignored, so no pre-rasterization stage writes Layer and the first layer is used. Reading the unwritten Layer builtin instead makes AMD's Windows driver drop the fragment's writes (#4800). 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 field, 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. In the whole word only bits 0-1 and 8-11 have a source. Bits 2-7, 12-15 and 29-31 are assumed zero, not measured: the VRS rates (2-5) and the eye id (7) because the driver supports neither variable-rate shading nor multiview, so a title that enables either would read other bits on the console, and bits 6, 12-15 and 29-31 because no source gives them a meaning. 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`). The whole word has the same 0 at bits 16-28. A change that makes draws export a layer must restore the Layer read in both places +- [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. The sample index is the SampleId builtin, an equality inferred, not measured. The render target index is the draw's color view slice (the pipeline specialization constant `LayerBase`: the depth slice of a 3D target or the array slice of an array target, 0 for a layered draw or a draw without a color target): vertex layer exports are ignored, so no pre-rasterization stage writes Layer and the Layer builtin is not read. Reading the unwritten Layer builtin instead makes AMD's Windows driver drop the fragment's writes (#4800). 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 field, 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. In the whole word only bits 0-1 and 8-11 have a source. Bits 2-7, 12-15 and 29-31 are assumed zero, not measured: the VRS rates (2-5) and the eye id (7) because the driver supports neither variable-rate shading nor multiview, so a title that enables either would read other bits on the console, and bits 6, 12-15 and 29-31 because no source gives them a meaning. 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`). The whole word has the same 0 at bits 16-28. A change that makes draws export a layer must restore the Layer read in both places +- [Front-face input with `FRONT_FACE_ALL_BITS`](../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (shader recompiler) - with `SPI_BARYC_CNTL.FRONT_FACE_ALL_BITS` set, a back face reads 0 and a front face 0xffffffff. Mesa 22.0 RADV sets the bit ([radv_pipeline.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-22.0.0/src/amd/vulkan/radv_pipeline.c#L5953)) and ACO tests the input with `v_cmp_lg_u32 0` ([aco_instruction_selection.cpp](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-22.0.0/src/amd/compiler/aco_instruction_selection.cpp#L8123-L8128)), so only back = 0 and front ≠ 0 are sourced; a shipped title's two-sided cloth shaders test it with `v_cmp_lt_u32 0`. The exact front value is not measured. Without the bit the input is ±1.0, as current Mesa reads it (`fgt 0`, `fsign`; [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)) - [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. 8-bit sRGB and UNORM (`Format8Srgb`, `Format8_8Srgb`, `Format8_8_8_8Srgb`, `Format8UNorm`, `Format8_8UNorm`, `Format8_8_8_8UNorm`) return 0 for every compare function, measured with `image_sample_c_lz` on gfx1035 over references -0.5..1.5 and texel codes 0..255. Anisotropic filters are refused, except on those 8-bit formats, where every texel comparison is 0 (not measured with an anisotropic S#). Bilinear cube comparisons of float, snorm and the other unorm formats without `DISABLE_CUBE_WRAP` are refused; on that hardware a 4x4 R32 float cube blends across faces (46 of 90 edge rows differ from a per-face clamp; face 0, ref 0.1, u=3.9 v=2.5 is 0.6016 and u=1.5 v=0.1 is 0.3984), and with `DISABLE_CUBE_WRAP` the per-face clamp matches. An 8-bit sRGB or UNORM bilinear cube without that bit returns 0, since every texel comparison is already 0 for every compare function and the selected face cannot change the result. 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 @@ -595,7 +596,9 @@ Throughout the project, every function at every stage either **does exactly what - [sceAjmDecMp3ParseFrame](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) throws when the original file length is requested (`parse_ofl` non-zero) for a layer other than III, for a CRC-protected frame, or for a VBRI header outside MPEG-1 stereo: shadPS4 looks for the tag after the CRC and after the side information, while LAME and FFmpeg place it without the CRC and VBRI always at byte 36. - [ATRAC9 gapless segments](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - a run job stops after the gapless total of output samples counted from the start of the segment, leaving the rest of its input unconsumed, and the next run starts the next segment with the decoder as it is and without a second skip. FMOD in PPSA29141 sets a total shorter than a sound's frames and reads the frames after it on later runs; with the output held at the total its stream reader waited for them forever and the last buffer repeated. The console's handling of the frames after the total is not documented; a reset (`SetGaplessDecode` with the reset flag, `ClearContext`, initialization) starts a new segment. An ATRAC9 run job without `RUN_MULTIPLE_FRAMES` decodes its whole input as one with it: FMOD in PPSA29141 puts two run jobs in one batch with the second's input right after the whole of the first's. Whether the console stops after one frame there is not verified; MP3 and Opus run jobs still stop after one frame - [Tessellation rings](../../core/shader/recompiler/Optimization/src/TessellationLowering.cpp) of local, hull and domain programs become Vulkan tessellation interfaces and are never written to guest memory; a title that reads the off-chip or factor ring back sees no data. -- [Inner tessellation factors](../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) of fractional-odd triangle patches that are NaN tessellate as 1, as in the D3D11 reference tessellator; the console is not measured. +- [Inner tessellation factors](../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) of integer, fractional-even or fractional-odd triangle patches that are NaN tessellate as 1 (fractional-even then raises them to 2, as Vulkan does), as in the D3D11 reference tessellator; the console is not measured. +- [Integer partitioning](../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (`VGT_TF_PARAM.PARTITIONING` 0) is drawn with equal spacing, which [radeonsi programs as `PART_INTEGER`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-24.0.0/src/gallium/drivers/radeonsi/si_state_shaders.cpp#L539-540), with factors bounded at 64 instead of fractional-odd's 63; how the console rounds a clamped factor up to a whole number of segments is not measured. Power-of-two (1) partitioning throws. +- [Fractional-even partitioning](../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (`VGT_TF_PARAM.PARTITIONING` 3) is drawn with fractional-even spacing, which [radeonsi programs as `PART_FRAC_EVEN`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-24.0.0/src/gallium/drivers/radeonsi/si_state_shaders.cpp#L536-537), with factors bounded at 64. Vulkan raises every factor that is not culled to at least 2 after `VGT_HOS_MIN/MAX_TESS_LEVEL`; the console's order of the HOS clamp and that minimum is not measured. - [VGT_TF_PARAM](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - DISTRIBUTION_MODE is taken as scheduling only; the host tessellator decides the order of a patch's triangles. - [Hull programs](../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) start from the hull launch registers (user data, ring bases, v0, v1); on the console they start with what the local program leaves, and that it leaves them unchanged is not checked. - [Local programs](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) that the hull header leaves at address 0 are prepared at their first draw, one artifact per tessellation configuration, instead of at registration.