diff --git a/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp b/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp index d4ce2560d..daa3e1909 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp @@ -38,6 +38,13 @@ alignas(256) constexpr std::array GeometryCode{ 0xdbfc0410, 0x3000002b, 0xbf8cc07f, 0xf80008cf, 0x2f2e2d2c, 0xf800020f, 0x33323130, 0xbf810000, }; +alignas(256) constexpr auto Wave32GeometryCode = [] { + auto code = GeometryCode; + code[10] = 0x04190a6bu; + code[28] = 0x04250a02u; + return code; +}(); + alignas(256) constexpr std::array PixelCode{ 0xc8020002, 0xc8060102, 0xc80a0202, 0xc80e0302, 0xf800180f, 0x03020100, 0xbf810000, }; @@ -119,6 +126,7 @@ struct MeshDraw { std::array vertexBuffer; std::uint32_t geometryPushBytes = ShaderRecompiler::MeshDrawPushOffsetBytes; bool restart = false; + std::uint32_t waveSize = 64u; }; void DrawMesh(AgcDriver::VulkanDevice& device, const MeshDraw& setup) { @@ -127,10 +135,11 @@ void DrawMesh(AgcDriver::VulkanDevice& device, const MeshDraw& setup) { const auto index = AgcDriver::Graphics::MeshIndexBufferDescriptor(setup.draw); std::copy(index.begin(), index.end(), userData.begin() + ShaderRecompiler::MeshIndexBufferUserWord); std::copy(setup.vertexBuffer.begin(), setup.vertexBuffer.end(), userData.begin() + 8); - const std::array geometryMemory{{{reinterpret_cast(GeometryCode.data()), std::as_bytes(std::span(GeometryCode))}}}; + const auto& code = setup.waveSize == 32u ? Wave32GeometryCode : GeometryCode; + const std::array geometryMemory{{{reinterpret_cast(code.data()), std::as_bytes(std::span(code))}}}; ShaderRecompiler::RecompileRequest geometry{ - {ShaderStage::Mesh, reinterpret_cast(GeometryCode.data()), GeometryCode, 0, {}}, - {64, 0, userData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, geometryMemory}, + {ShaderStage::Mesh, reinterpret_cast(code.data()), code, 0, {}}, + {setup.waveSize, 0, userData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, geometryMemory}, target, {0, 0, 0, setup.geometryPushBytes}, ShaderRecompiler::GraphicsCompileContext{0, {}, setup.mesh, std::nullopt, {setup.draw.indexAddress, setup.draw.indexCount, setup.draw.indexSize, setup.draw.instanceCount}} @@ -159,7 +168,7 @@ void DrawMesh(AgcDriver::VulkanDevice& device, const MeshDraw& setup) { }}; AgcDriver::Graphics::State state{}; - state.stages = {AgcDriver::Graphics::ShaderPath::Geometry, 0x20u, 64, 64, setup.mesh, std::nullopt}; + state.stages = {AgcDriver::Graphics::ShaderPath::Geometry, 0x20u, setup.waveSize, 64, setup.mesh, std::nullopt}; state.color = {reinterpret_cast(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u}; state.colors = {state.color}; state.hasColorTarget = true; @@ -264,6 +273,17 @@ int main() { CheckTriangles((std::string("non-indexed triangle list, ") + name).c_str()); } + auto wave32Subgroup = SmallSubgroup; + wave32Subgroup.threadsPerGroup = 32u; + for (const auto& subgroup : {wave32Subgroup, SmallSubgroup, WideSubgroup}) { + ClearPixels(); + DrawMesh(device, {subgroup, {reinterpret_cast(Indices.data()), static_cast(Indices.size()), 2, 1, 0, true}, VertexBufferDescriptor(Scrambled.data(), static_cast(Scrambled.size())), ShaderRecompiler::MeshDrawPushOffsetBytes, false, 32u}); + CheckTriangles("indexed wave32 triangle list"); + ClearPixels(); + DrawMesh(device, {subgroup, {0, static_cast(Ordered.size()), 0, 1, 0, false}, VertexBufferDescriptor(Ordered.data(), static_cast(Ordered.size())), ShaderRecompiler::MeshDrawPushOffsetBytes, false, 32u}); + CheckTriangles("non-indexed wave32 triangle list"); + } + constexpr std::size_t recordBlockBytes = 65536; auto* record = static_cast(::operator new(recordBlockBytes, std::align_val_t{65536})); { diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 0d8e318a3..6259fb0d0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -801,6 +801,20 @@ void verifyMeshConfiguration() { other.graphics = GraphicsCompileContext{0u, {}, otherMesh, std::nullopt, {}}; RecompileCacheKey::Build(other, key); require(key != first && RecompileCacheKey::ContextHash(request) != RecompileCacheKey::ContextHash(other), "the cache keys ignore the mesh configuration"); + const std::array users{}; + request.context.waveSize = 32u; + request.context.userData = users; + request.context.vertex = ShaderVertexStageInfo{}; + request.target.subgroupSize = 32u; + request.graphics->mesh = MeshConfiguration{1u, 1u, 1u, 1u, 1u, 32u, 256u, 0u, 4u}; + for (const std::uint32_t threads : {32u, 64u, 128u, 480u}) { + request.graphics->mesh->threadsPerGroup = threads; + require(PrepareResourceProgram(request).WaveSize() == 32u, "a wave32 mesh program did not preserve its guest wave size"); + } + for (const std::uint32_t threads : {31u, 512u}) { + request.graphics->mesh->threadsPerGroup = threads; + expectFailure([&] { static_cast(PrepareResourceProgram(request)); }, "mesh shader translation configuration is not supported", "mesh translation accepted a partial wave or overflowing wave count"); + } } ShaderRecompiler::ShaderPixelStageInfo twoParameterPixel() { diff --git a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp index 2f5a7d6c5..059260db2 100644 --- a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp +++ b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp @@ -219,7 +219,7 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO const auto& mesh = options.inputInfo.vertex->mesh; const std::uint32_t size = mesh.InputPrimitiveSize(); const std::uint32_t stepCount = mesh.InputPrimitiveStep(); - if (options.waveSize != 64u || mesh.primitivesPerGroup == 0u || mesh.verticesPerGroup != mesh.InputVertexCount(mesh.primitivesPerGroup) || mesh.verticesPerGroup > totalThreads || mesh.primitivesPerGroup > totalThreads || totalThreads % 64u != 0u || totalThreads > 15u * 64u || mesh.esgsItemSize == 0u || mesh.esgsItemSize * mesh.verticesPerGroup > 0xffffu) { + if (mesh.primitivesPerGroup == 0u || mesh.verticesPerGroup != mesh.InputVertexCount(mesh.primitivesPerGroup) || mesh.verticesPerGroup > totalThreads || mesh.primitivesPerGroup > totalThreads || totalThreads % options.waveSize != 0u || totalThreads / options.waveSize > 15u || mesh.esgsItemSize == 0u || mesh.esgsItemSize * mesh.verticesPerGroup > 0xffffu) { throw std::runtime_error("mesh shader translation configuration is not supported (wave " + std::to_string(options.waveSize) + ", primitives per group " + std::to_string(mesh.primitivesPerGroup) + ", vertices per group " + std::to_string(mesh.verticesPerGroup) + ", threads " + std::to_string(totalThreads) + ", ESGS item size " + std::to_string(mesh.esgsItemSize) + ")"); } constexpr std::uint32_t kTriFanPrimitiveType = 5u; @@ -250,11 +250,11 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO IrValue& vertices = minimum(subtractSaturate(indexCount, firstVertex), u32(mesh.verticesPerGroup)); IrValue& primitives = entryIr.Select(entryIr.ULessThan(vertices, u32(size)), u32(0u), entryIr.IAdd(entryIr.Emit(IrOpcode::UDiv32, IrOpcodeType(IrOpcode::UDiv32), {&subtractSaturate(vertices, u32(size)), &step}), u32(1u))); entryIr.SetScalarReg(static_cast(2), entryIr.BitwiseOr(entryIr.ShiftLeftLogical(vertices, u32(12u)), entryIr.ShiftLeftLogical(primitives, u32(22u)))); - IrValue& wave = entryIr.ShiftRightLogical(local, u32(6u)); - IrValue& waveBase = entryIr.BitwiseAnd(local, u32(~63u)); - IrValue& vertexCount = minimum(subtractSaturate(vertices, waveBase), u32(64u)); - IrValue& primitiveCount = minimum(subtractSaturate(primitives, waveBase), u32(64u)); - IrValue& waveInfo = entryIr.BitwiseOr(entryIr.ShiftLeftLogical(wave, u32(24u)), u32((totalThreads / 64u) << 28u)); + IrValue& wave = entryIr.ShiftRightLogical(local, u32(options.waveSize == 32u ? 5u : 6u)); + IrValue& waveBase = entryIr.BitwiseAnd(local, u32(~(options.waveSize - 1u))); + IrValue& vertexCount = minimum(subtractSaturate(vertices, waveBase), u32(options.waveSize)); + IrValue& primitiveCount = minimum(subtractSaturate(primitives, waveBase), u32(options.waveSize)); + IrValue& waveInfo = entryIr.BitwiseOr(entryIr.ShiftLeftLogical(wave, u32(24u)), u32((totalThreads / options.waveSize) << 28u)); entryIr.SetScalarReg(static_cast(3), entryIr.BitwiseOr(waveInfo, entryIr.BitwiseOr(entryIr.ShiftLeftLogical(primitiveCount, u32(8u)), vertexCount))); IrValue& parity = mesh.inputPrimitive == kTriStripPrimitiveType ? entryIr.BitwiseAnd(entryIr.IAdd(firstPrimitive, local), u32(1u)) : u32(0u); IrValue& vertex = entryIr.IMul(local, step); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 8f2178189..a9aab642a 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -282,6 +282,7 @@ Throughout the project, every function at every stage either **does exactly what - [`v_readlane_b32`](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) and fetch-inactive `v_permlane16_b32`/`v_permlanex16_b32` read undefined data from a lane that the host subgroup does not hold or whose host invocation is inactive; the console reads the lane's VGPR. - Without fetch-inactive, [`v_permlane16_b32` and `v_permlanex16_b32`](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) take a source lane that the host subgroup does not hold as inactive and keep the old value (0 with bound_ctrl); the console reads that lane's VGPR. - On a host subgroup narrower than the wave, [`v_readlane_b32`](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) of a constant lane outside it builds only as lane 63 of a half-wave scan (DPP row_shr 1/2/4/8, `v_permlanex16_b32`, EXEC -1); other such reads throw. +- [NGG mesh entry](../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) uses the guest wave size (32 or 64) for wave indices, per-wave vertex and primitive counts, and the workgroup wave count in `s3`. The four-bit wave fields follow Mesa's [get_wave_id_in_tg, get_tgsize and get_thread_id_in_tg](https://chromium.googlesource.com/chromiumos/third_party/mesa/+/refs/heads/stabilize-13982.70.B-chromeos-amd/src/gallium/drivers/radeonsi/gfx10_shader_ngg.c); partial waves and groups exceeding 15 waves throw. Indexed and non-indexed wave32 mesh rendering with 32, 64 and 128 threads was checked by pixel readback on Windows with an NVIDIA RTX 4070 Laptop GPU. The guest register values have not been compared with console hardware. - [Shader recompilation](../../core/shader/recompiler/Recompiler.cpp) currently occurs right before it was transferred to Vulkan with caching, but should be moved to the [relinker](../../core/relinker/main.cpp) stage. For this purpose, [shader/recompiler](../../core/shader/recompiler) was written completely independently from [libs/prx](../../core/libs/prx). - [v_interp_p1ll_f16, v_interp_p1lv_f16 and v_interp_p2_f16](../../core/shader/recompiler/Translation/src/AttributeInstructions.cpp) read the 16-bit halves of each vertex's parameter through `VK_KHR_fragment_shader_barycentric` and throw without it, as fixed-function interpolation cannot split a 32-bit attribute. P10 and P20 are the vertex differences rounded to f16 to nearest even, which is not confirmed on hardware. v_interp_p1ll_f16 and v_interp_p1lv_f16 round the exact `P10 * I + P0` once to f32 and v_interp_p2_f16 rounds the exact `P20 * J + S2` once to f16, as measured on RDNA2 (#697); the exact sums are computed in f64, so these instructions need the Float64 capability. They follow the shader's float mode, measured on RDNA2 (gfx1035) in all eight combinations of `IEEE_MODE` and kept/flushed f32 and f16 denormals over 768 special and 1,024 random rows. With IEEE=1 a NaN operand is returned quieted; with IEEE=0 it is returned unquieted, except that v_interp_p2_f16's f16 result is always quiet. Flushed f32 denormals flush I/J, v_interp_p2_f16's f32 S2 and the f32 result; flushed f16 denormals flush P0, P10/P20, v_interp_p1lv_f16's f16 S2 and the f16 result. Denormal modes 1 and 2 (flush only inputs or only outputs) and rounding other than to nearest even are not measured and throw. An unknown mode follows IEEE=0 with denormals kept. Lavapipe has no barycentrics, so the draw test is skipped in CI; `agc_driver_interpolation_modes` checks the arithmetic in every mode against the hardware rows on any device with Float64. - [v_fma_f64](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp) is fused exactly only for normal inputs: with a subnormal input it is a separate multiply and add, which rounds twice.