fix(shader): support wave32 mesh programs
Use the guest wave width when constructing NGG mesh entry registers. Add resource-analysis boundaries and indexed/non-indexed rendering regressions.
This commit is contained in:
parent
745b775e2d
commit
b2c825b91b
|
|
@ -38,6 +38,13 @@ alignas(256) constexpr std::array<std::uint32_t, 104> 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<std::uint32_t, 7> PixelCode{
|
||||
0xc8020002, 0xc8060102, 0xc80a0202, 0xc80e0302, 0xf800180f, 0x03020100, 0xbf810000,
|
||||
};
|
||||
|
|
@ -119,6 +126,7 @@ struct MeshDraw {
|
|||
std::array<std::uint32_t, 4> 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<ShaderRecompiler::MemoryRegion, 1> geometryMemory{{{reinterpret_cast<std::uintptr_t>(GeometryCode.data()), std::as_bytes(std::span(GeometryCode))}}};
|
||||
const auto& code = setup.waveSize == 32u ? Wave32GeometryCode : GeometryCode;
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> geometryMemory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(std::span(code))}}};
|
||||
ShaderRecompiler::RecompileRequest geometry{
|
||||
{ShaderStage::Mesh, reinterpret_cast<std::uintptr_t>(GeometryCode.data()), GeometryCode, 0, {}},
|
||||
{64, 0, userData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, geometryMemory},
|
||||
{ShaderStage::Mesh, reinterpret_cast<std::uintptr_t>(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<std::uintptr_t>(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<std::uintptr_t>(Indices.data()), static_cast<std::uint32_t>(Indices.size()), 2, 1, 0, true}, VertexBufferDescriptor(Scrambled.data(), static_cast<std::uint32_t>(Scrambled.size())), ShaderRecompiler::MeshDrawPushOffsetBytes, false, 32u});
|
||||
CheckTriangles("indexed wave32 triangle list");
|
||||
ClearPixels();
|
||||
DrawMesh(device, {subgroup, {0, static_cast<std::uint32_t>(Ordered.size()), 0, 1, 0, false}, VertexBufferDescriptor(Ordered.data(), static_cast<std::uint32_t>(Ordered.size())), ShaderRecompiler::MeshDrawPushOffsetBytes, false, 32u});
|
||||
CheckTriangles("non-indexed wave32 triangle list");
|
||||
}
|
||||
|
||||
constexpr std::size_t recordBlockBytes = 65536;
|
||||
auto* record = static_cast<std::uint32_t*>(::operator new(recordBlockBytes, std::align_val_t{65536}));
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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<std::uint32_t, 8> 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<void>(PrepareResourceProgram(request)); }, "mesh shader translation configuration is not supported", "mesh translation accepted a partial wave or overflowing wave count");
|
||||
}
|
||||
}
|
||||
|
||||
ShaderRecompiler::ShaderPixelStageInfo twoParameterPixel() {
|
||||
|
|
|
|||
|
|
@ -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<ScalarReg>(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<ScalarReg>(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);
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in New Issue