diff options
| author | Takoroka <137927095+Takoroka@users.noreply.github.com> | 2026-11-01 13:25:40 -0500 |
|---|---|---|
| committer | Takoroka <137927095+Takoroka@users.noreply.github.com> | 2026-11-01 13:25:40 -0500 |
| commit | 235e2f5f5548e20620c247b76586cbb559f4158d (patch) | |
| tree | 4b282626b7d70498cb493653a585ac9d8a4f074c /src | |
| parent | 6bde8c20d1f7c020a920e59a989c03822a08445d (diff) | |
| parent | 004ca96531812ea3674273ed568e3a672e664932 (diff) | |
Merge remote-tracking branch 'origin/feat/nx2-mesh-shaders' into test/linux-all-prs
Diffstat (limited to 'src')
46 files changed, 1377 insertions, 150 deletions
diff --git a/src/shader_recompiler/CMakeLists.txt b/src/shader_recompiler/CMakeLists.txt index e60f26d47f..e4df9c045d 100644 --- a/src/shader_recompiler/CMakeLists.txt +++ b/src/shader_recompiler/CMakeLists.txt @@ -24,6 +24,7 @@ add_library(shader_recompiler STATIC backend/spirv/emit_spirv_memory.cpp backend/spirv/emit_spirv_select.cpp backend/spirv/emit_spirv_shared_memory.cpp + backend/spirv/emit_spirv_mesh.cpp backend/spirv/emit_spirv_special.cpp backend/spirv/emit_spirv_undefined.cpp backend/spirv/emit_spirv_warp.cpp diff --git a/src/shader_recompiler/backend/glasm/emit_glasm.cpp b/src/shader_recompiler/backend/glasm/emit_glasm.cpp index b795c01797..c18eec63cd 100644 --- a/src/shader_recompiler/backend/glasm/emit_glasm.cpp +++ b/src/shader_recompiler/backend/glasm/emit_glasm.cpp @@ -341,6 +341,9 @@ std::string_view StageHeader(Stage stage) { return "!!NVfp5.0\n"; case Stage::Compute: return "!!NVcp5.0\n"; + case Stage::Task: + case Stage::Mesh: + break; } throw InvalidArgument("Invalid stage {}", stage); } diff --git a/src/shader_recompiler/backend/glasm/emit_glasm_instructions.h b/src/shader_recompiler/backend/glasm/emit_glasm_instructions.h index 6f522ab5c2..d66fe2f3d6 100644 --- a/src/shader_recompiler/backend/glasm/emit_glasm_instructions.h +++ b/src/shader_recompiler/backend/glasm/emit_glasm_instructions.h @@ -58,6 +58,10 @@ void EmitGetAttributeIndexed(EmitContext& ctx, IR::Inst& inst, ScalarS32 offset, void EmitSetAttributeIndexed(EmitContext& ctx, ScalarU32 offset, ScalarF32 value, ScalarU32 vertex); void EmitGetPatch(EmitContext& ctx, IR::Inst& inst, IR::Patch patch); void EmitSetPatch(EmitContext& ctx, IR::Patch patch, ScalarF32 value); +void EmitIsbeRead(EmitContext& ctx); +void EmitIsbeWriteAttribute(EmitContext& ctx); +void EmitIsbeWriteMapU8(EmitContext& ctx); +void EmitIsbeWriteMapU32(EmitContext& ctx); void EmitSetFragColor(EmitContext& ctx, u32 index, u32 component, ScalarF32 value); void EmitSetSampleMask(EmitContext& ctx, ScalarS32 value); void EmitSetFragDepth(EmitContext& ctx, ScalarF32 value); diff --git a/src/shader_recompiler/backend/glasm/emit_glasm_not_implemented.cpp b/src/shader_recompiler/backend/glasm/emit_glasm_not_implemented.cpp index 1f4ffdd624..41c829ca5d 100644 --- a/src/shader_recompiler/backend/glasm/emit_glasm_not_implemented.cpp +++ b/src/shader_recompiler/backend/glasm/emit_glasm_not_implemented.cpp @@ -97,4 +97,20 @@ void EmitGetInBoundsFromOp(EmitContext& ctx) { NotImplemented(); } +void EmitIsbeRead(EmitContext& ctx) { + NotImplemented(); +} + +void EmitIsbeWriteAttribute(EmitContext& ctx) { + NotImplemented(); +} + +void EmitIsbeWriteMapU8(EmitContext& ctx) { + NotImplemented(); +} + +void EmitIsbeWriteMapU32(EmitContext& ctx) { + NotImplemented(); +} + } // namespace Shader::Backend::GLASM diff --git a/src/shader_recompiler/backend/glasm/glasm_emit_context.cpp b/src/shader_recompiler/backend/glasm/glasm_emit_context.cpp index 333a91cc58..02b6992b52 100644 --- a/src/shader_recompiler/backend/glasm/glasm_emit_context.cpp +++ b/src/shader_recompiler/backend/glasm/glasm_emit_context.cpp @@ -80,6 +80,9 @@ EmitContext::EmitContext(IR::Program& program, Bindings& bindings, const Profile case Stage::Compute: stage_name = "invocation"; break; + case Stage::Task: + case Stage::Mesh: + throw NotImplementedException("GLASM task and mesh programs"); } const std::string_view attr_stage{stage == Stage::Fragment ? "fragment" : "vertex"}; const VaryingState loads{info.loads.mask | info.passthrough.mask}; diff --git a/src/shader_recompiler/backend/glsl/emit_glsl_instructions.h b/src/shader_recompiler/backend/glsl/emit_glsl_instructions.h index 96aac502d5..06069d4c56 100644 --- a/src/shader_recompiler/backend/glsl/emit_glsl_instructions.h +++ b/src/shader_recompiler/backend/glsl/emit_glsl_instructions.h @@ -72,6 +72,10 @@ void EmitSetAttributeIndexed(EmitContext& ctx, std::string_view offset, std::str std::string_view vertex); void EmitGetPatch(EmitContext& ctx, IR::Inst& inst, IR::Patch patch); void EmitSetPatch(EmitContext& ctx, IR::Patch patch, std::string_view value); +void EmitIsbeRead(EmitContext& ctx); +void EmitIsbeWriteAttribute(EmitContext& ctx); +void EmitIsbeWriteMapU8(EmitContext& ctx); +void EmitIsbeWriteMapU32(EmitContext& ctx); void EmitSetFragColor(EmitContext& ctx, u32 index, u32 component, std::string_view value); void EmitSetSampleMask(EmitContext& ctx, std::string_view value); void EmitSetFragDepth(EmitContext& ctx, std::string_view value); diff --git a/src/shader_recompiler/backend/glsl/emit_glsl_not_implemented.cpp b/src/shader_recompiler/backend/glsl/emit_glsl_not_implemented.cpp index 9f1ed95a47..18779e51b8 100644 --- a/src/shader_recompiler/backend/glsl/emit_glsl_not_implemented.cpp +++ b/src/shader_recompiler/backend/glsl/emit_glsl_not_implemented.cpp @@ -97,4 +97,20 @@ void EmitGetInBoundsFromOp(EmitContext& ctx) { NotImplemented(); } +void EmitIsbeRead(EmitContext& ctx) { + NotImplemented(); +} + +void EmitIsbeWriteAttribute(EmitContext& ctx) { + NotImplemented(); +} + +void EmitIsbeWriteMapU8(EmitContext& ctx) { + NotImplemented(); +} + +void EmitIsbeWriteMapU32(EmitContext& ctx) { + NotImplemented(); +} + } // namespace Shader::Backend::GLSL diff --git a/src/shader_recompiler/backend/glsl/glsl_emit_context.cpp b/src/shader_recompiler/backend/glsl/glsl_emit_context.cpp index a2df6159fb..e4b11d6844 100644 --- a/src/shader_recompiler/backend/glsl/glsl_emit_context.cpp +++ b/src/shader_recompiler/backend/glsl/glsl_emit_context.cpp @@ -317,6 +317,9 @@ EmitContext::EmitContext(IR::Program& program, Bindings& bindings, const Profile header += "layout(early_fragment_tests)in;"; } break; + case Stage::Task: + case Stage::Mesh: + throw NotImplementedException("GLSL task and mesh programs"); case Stage::Compute: stage_name = "cs"; const u32 local_x{(std::max)(program.workgroup_size[0], 1u)}; diff --git a/src/shader_recompiler/backend/spirv/emit_spirv.cpp b/src/shader_recompiler/backend/spirv/emit_spirv.cpp index a903480c78..633ab25935 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv.cpp +++ b/src/shader_recompiler/backend/spirv/emit_spirv.cpp @@ -248,6 +248,20 @@ void DefineEntryPoint(const IR::Program& program, EmitContext& ctx, Id main) { case Stage::VertexB: execution_model = spv::ExecutionModel::Vertex; break; + case Stage::Task: + execution_model = spv::ExecutionModel::TaskEXT; + ctx.AddExecutionMode(main, spv::ExecutionMode::LocalSize, program.workgroup_size[0], 1U, + 1U); + break; + case Stage::Mesh: + execution_model = spv::ExecutionModel::MeshEXT; + ctx.AddExecutionMode(main, spv::ExecutionMode::LocalSize, program.workgroup_size[0], 1U, + 1U); + ctx.AddExecutionMode(main, spv::ExecutionMode::OutputVertices, program.mesh.max_vertices); + ctx.AddExecutionMode(main, spv::ExecutionMode::OutputPrimitivesEXT, + program.mesh.max_primitives); + ctx.AddExecutionMode(main, spv::ExecutionMode::OutputTrianglesEXT); + break; case Stage::TessellationControl: execution_model = spv::ExecutionModel::TessellationControl; ctx.AddCapability(spv::Capability::Tessellation); @@ -548,6 +562,51 @@ void RewriteOpcodes(std::vector<u32>& code, std::span<const std::pair<u32, spv:: offset += word_count; } } + +// Replaces the OpStore markers of the mesh instructions Sirit cannot emit: OpStore(vertices, +// primitives) becomes OpSetMeshOutputsEXT, and OpStore(x, y), OpStore(z, payload), OpReturn +// becomes the OpEmitMeshTasksEXT terminator. +void PatchMeshInstructions(std::vector<u32>& code, const EmitContext& ctx) { + const bool set_outputs{Sirit::ValidId(ctx.mesh_outputs_marker[0])}; + const bool emit_tasks{Sirit::ValidId(ctx.mesh_tasks_marker[0])}; + if (!set_outputs && !emit_tasks) { + return; + } + const auto store_at = [&](size_t offset, Id pointer, Id object) { + return offset + 2 < code.size() && + code[offset] == ((3U << 16) | static_cast<u32>(spv::Op::OpStore)) && + code[offset + 1] == pointer.value && code[offset + 2] == object.value; + }; + const auto& tasks{ctx.mesh_tasks_marker}; + size_t offset = 5; + bool patched{}; + while (offset < code.size()) { + const u32 word_count{code[offset] >> 16}; + if (word_count == 0) { + break; + } + if (set_outputs && + store_at(offset, ctx.mesh_outputs_marker[0], ctx.mesh_outputs_marker[1])) { + code[offset] = (3U << 16) | static_cast<u32>(spv::Op::OpSetMeshOutputsEXT); + patched = true; + } else if (emit_tasks && store_at(offset, tasks[0], tasks[1]) && + store_at(offset + 3, tasks[2], tasks[3]) && offset + 6 < code.size() && + code[offset + 6] == ((1U << 16) | static_cast<u32>(spv::Op::OpReturn))) { + const std::array<u32, 5> emit{(5U << 16) | static_cast<u32>(spv::Op::OpEmitMeshTasksEXT), + tasks[0].value, tasks[1].value, tasks[2].value, + tasks[3].value}; + code.erase(code.begin() + offset, code.begin() + offset + 7); + code.insert(code.begin() + offset, emit.begin(), emit.end()); + patched = true; + offset += emit.size(); + continue; + } + offset += word_count; + } + if (!patched) { + throw LogicError("Mesh instruction markers not found"); + } +} } // Anonymous namespace std::vector<u32> EmitSPIRV(const Profile& profile, const RuntimeInfo& runtime_info, IR::Program& program, Bindings& bindings) { @@ -564,6 +623,7 @@ std::vector<u32> EmitSPIRV(const Profile& profile, const RuntimeInfo& runtime_in PatchPhiNodes(program, ctx); std::vector<u32> code{ctx.Assemble()}; RewriteOpcodes(code, ctx.opcode_rewrites); + PatchMeshInstructions(code, ctx); return code; } diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_instructions.h b/src/shader_recompiler/backend/spirv/emit_spirv_instructions.h index 165ec3fe84..ec526ff354 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_instructions.h +++ b/src/shader_recompiler/backend/spirv/emit_spirv_instructions.h @@ -61,6 +61,12 @@ Id EmitGetAttributeIndexed(EmitContext& ctx, Id offset, Id vertex); void EmitSetAttributeIndexed(EmitContext& ctx, Id offset, Id value, Id vertex); Id EmitGetPatch(EmitContext& ctx, IR::Patch patch); void EmitSetPatch(EmitContext& ctx, IR::Patch patch, Id value); +Id EmitIsbeRead(EmitContext& ctx, Id address); +void EmitIsbeWriteAttribute(EmitContext& ctx, Id address, Id value); +void EmitIsbeWriteMapU8(EmitContext& ctx, Id address, Id value); +void EmitIsbeWriteMapU32(EmitContext& ctx, const IR::Value& address, Id value); +void EmitMeshPrologue(EmitContext& ctx); +void EmitMeshEpilogue(EmitContext& ctx); void EmitSetFragColor(EmitContext& ctx, u32 index, u32 component, Id value); void EmitSetSampleMask(EmitContext& ctx, Id value); void EmitSetFragDepth(EmitContext& ctx, Id value); diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_mesh.cpp b/src/shader_recompiler/backend/spirv/emit_spirv_mesh.cpp new file mode 100644 index 0000000000..6c599528cc --- /dev/null +++ b/src/shader_recompiler/backend/spirv/emit_spirv_mesh.cpp @@ -0,0 +1,227 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// Switch 2 (NVN) task and mesh programs. They talk to the hardware through the stage buffer +// (ISBE): a task program writes the number of mesh workgroups at byte 4 of its output region and +// its payload after it; each mesh workgroup reads that region (its own index at byte 0) and +// writes its vertices and primitives to an attribute region in groups of 32, one 32-word line per +// attribute, plus the primitive count and 8-bit vertex indices to a map region. Here the task's +// output region is the task payload, the mesh output regions live in workgroup memory, and the +// epilogue (reached by every invocation, see structured_control_flow.cpp) hands them to Vulkan. + +#include <algorithm> +#include <span> + +#include "shader_recompiler/backend/spirv/emit_spirv.h" +#include "shader_recompiler/backend/spirv/emit_spirv_instructions.h" +#include "shader_recompiler/backend/spirv/spirv_emit_context.h" + +namespace Shader::Backend::SPIRV { +namespace { +void Barrier(EmitContext& ctx) { + ctx.OpControlBarrier(ctx.Const(static_cast<u32>(spv::Scope::Workgroup)), + ctx.Const(static_cast<u32>(spv::Scope::Workgroup)), + ctx.Const(static_cast<u32>(spv::MemorySemanticsMask::AcquireRelease | + spv::MemorySemanticsMask::WorkgroupMemory))); +} + +Id LocalInvocation(EmitContext& ctx) { + return ctx.OpCompositeExtract(ctx.U32[1], ctx.OpLoad(ctx.U32[3], ctx.local_invocation_id), 0U); +} + +Id Clamped(EmitContext& ctx, Id index, u32 count) { + return ctx.OpUMin(ctx.U32[1], index, ctx.Const(count - 1)); +} + +Id PayloadWord(EmitContext& ctx, Id index) { + return ctx.OpAccessChain(ctx.mesh_payload_u32, ctx.mesh_payload, + Clamped(ctx, index, ctx.mesh_info.payload_words)); +} + +Id SharedWord(EmitContext& ctx, Id array, Id index) { + return ctx.OpAccessChain(ctx.mesh_shared_u32, array, index); +} + +// Runs body under `if (condition)`. +template <typename Func> +void If(EmitContext& ctx, Id condition, Func&& body) { + const Id then_label{ctx.OpLabel()}; + const Id merge_label{ctx.OpLabel()}; + ctx.OpSelectionMerge(merge_label, spv::SelectionControlMask::MaskNone); + ctx.OpBranchConditional(condition, then_label, merge_label); + ctx.AddLabel(then_label); + body(); + ctx.OpBranch(merge_label); + ctx.AddLabel(merge_label); +} + +// Copies the lines of element `index` (a vertex or a primitive) from the attribute region. +void CopyLines(EmitContext& ctx, Id index, u32 base, u32 num_lines, + const std::vector<EmitContext::MeshLineTarget>& targets) { + // Word of line 0: base + (index / 32) * num_lines * 32 + index % 32. + const Id group{ctx.OpShiftRightLogical(ctx.U32[1], index, ctx.Const(5U))}; + const Id slot{ctx.OpBitwiseAnd(ctx.U32[1], index, ctx.Const(31U))}; + const Id first{ctx.OpIAdd(ctx.U32[1], + ctx.OpIMul(ctx.U32[1], group, ctx.Const(num_lines * 32)), + ctx.OpIAdd(ctx.U32[1], slot, ctx.Const(base)))}; + for (u32 line = 0; line < num_lines; ++line) { + const EmitContext::MeshLineTarget& target{targets[line]}; + if (!Sirit::ValidId(target.variable)) { + continue; + } + const Id word{ctx.OpLoad( + ctx.U32[1], + SharedWord(ctx, ctx.mesh_attributes, ctx.OpIAdd(ctx.U32[1], first, ctx.Const(line * 32))))}; + const Id value{target.is_float ? ctx.OpBitcast(ctx.F32[1], word) : word}; + const Id pointer_type{target.is_float ? ctx.output_f32 : ctx.output_u32}; + const Id pointer{target.element == ~0U + ? ctx.OpAccessChain(pointer_type, target.variable, index) + : ctx.OpAccessChain(pointer_type, target.variable, index, + ctx.Const(target.element))}; + ctx.OpStore(pointer, value); + } +} + +// OpenGL-style clip space: z = (z + w) / 2, from the stage buffer's position lines. +void ConvertDepth(EmitContext& ctx, Id vertex) { + const auto& lines{ctx.mesh_info.vertex_lines}; + const auto z_line{std::ranges::find(lines, static_cast<u32>(IR::Attribute::PositionZ))}; + const auto w_line{std::ranges::find(lines, static_cast<u32>(IR::Attribute::PositionW))}; + if (z_line == lines.end() || w_line == lines.end()) { + return; + } + const u32 num_lines{static_cast<u32>(lines.size())}; + const Id group{ctx.OpShiftRightLogical(ctx.U32[1], vertex, ctx.Const(5U))}; + const Id slot{ctx.OpBitwiseAnd(ctx.U32[1], vertex, ctx.Const(31U))}; + const Id first{ctx.OpIAdd(ctx.U32[1], ctx.OpIMul(ctx.U32[1], group, ctx.Const(num_lines * 32)), + slot)}; + const auto read = [&](auto line) { + const u32 offset{static_cast<u32>(line - lines.begin()) * 32}; + return ctx.OpBitcast(ctx.F32[1], + ctx.OpLoad(ctx.U32[1], SharedWord(ctx, ctx.mesh_attributes, + ctx.OpIAdd(ctx.U32[1], first, + ctx.Const(offset))))); + }; + const Id z{ctx.OpFMul(ctx.F32[1], ctx.OpFAdd(ctx.F32[1], read(z_line), read(w_line)), + ctx.Const(0.5f))}; + ctx.OpStore(ctx.OpAccessChain(ctx.output_f32, ctx.output_position, vertex, ctx.Const(2U)), z); +} + +void TaskEpilogue(EmitContext& ctx) { + Barrier(ctx); + const Id count{ctx.OpLoad(ctx.U32[1], PayloadWord(ctx, ctx.Const(1U)))}; + const Id one{ctx.Const(1U)}; + // Marker for OpEmitMeshTasksEXT count, 1, 1, payload (PatchMeshInstructions). + ctx.OpStore(count, one); + ctx.OpStore(one, ctx.mesh_payload); + ctx.mesh_tasks_marker = {count, one, one, ctx.mesh_payload}; +} + +void MeshEpilogue(EmitContext& ctx) { + const MeshInfo& mesh{ctx.mesh_info}; + Barrier(ctx); + const Id count{ctx.OpUMin(ctx.U32[1], ctx.OpLoad(ctx.U32[1], ctx.mesh_primitive_count), + ctx.Const(mesh.max_primitives))}; + const Id max_vertices{ctx.Const(mesh.max_vertices)}; + // Marker for OpSetMeshOutputsEXT max_vertices, count (PatchMeshInstructions). + ctx.OpStore(max_vertices, count); + ctx.mesh_outputs_marker = {max_vertices, count}; + + const Id invocation{LocalInvocation(ctx)}; + const u32 threads{ctx.mesh_threads}; + const bool convert_depth{ctx.runtime_info.convert_depth_mode && !ctx.profile.support_native_ndc && + Sirit::ValidId(ctx.output_position)}; + const u32 vertex_lines{static_cast<u32>(mesh.vertex_lines.size())}; + for (u32 first = 0; first < mesh.max_vertices; first += threads) { + const Id vertex{ctx.OpIAdd(ctx.U32[1], invocation, ctx.Const(first))}; + const auto copy = [&] { + CopyLines(ctx, vertex, 0, vertex_lines, ctx.mesh_vertex_targets); + if (convert_depth) { + ConvertDepth(ctx, vertex); + } + }; + if (first + threads > mesh.max_vertices) { + If(ctx, ctx.OpULessThan(ctx.U1, vertex, max_vertices), copy); + } else { + copy(); + } + } + const u32 primitive_lines{static_cast<u32>(mesh.primitive_lines.size())}; + for (u32 first = 0; first < mesh.max_primitives; first += threads) { + const Id primitive{ctx.OpIAdd(ctx.U32[1], invocation, ctx.Const(first))}; + If(ctx, ctx.OpULessThan(ctx.U1, primitive, count), [&] { + // Vertex indices: map bytes 4 + 3 * primitive. + const Id byte{ctx.OpIAdd(ctx.U32[1], ctx.OpIMul(ctx.U32[1], primitive, ctx.Const(3U)), + ctx.Const(4U))}; + std::array<Id, 3> indices; + for (u32 element = 0; element < 3; ++element) { + const Id address{ctx.OpIAdd(ctx.U32[1], byte, ctx.Const(element))}; + indices[element] = ctx.OpLoad(ctx.U32[1], SharedWord(ctx, ctx.mesh_map, address)); + } + const Id output_indices{ctx.TypePointer(spv::StorageClass::Output, ctx.U32[3])}; + ctx.OpStore(ctx.OpAccessChain(output_indices, ctx.mesh_primitive_indices, primitive), + ctx.OpCompositeConstruct(ctx.U32[3], std::span<const Id>(indices))); + CopyLines(ctx, primitive, ctx.mesh_primitive_base, primitive_lines, + ctx.mesh_primitive_targets); + }); + } +} +} // Anonymous namespace + +Id EmitIsbeRead(EmitContext& ctx, Id address) { + const Id workgroup{ + ctx.OpCompositeExtract(ctx.U32[1], ctx.OpLoad(ctx.U32[3], ctx.workgroup_id), 0U)}; + if (ctx.stage == Stage::Task) { + return workgroup; // the input region holds the workgroup index (VERTEX_ID) + } + const Id index{ctx.OpShiftRightLogical(ctx.U32[1], address, ctx.Const(2U))}; + const Id word{ctx.OpLoad(ctx.U32[1], PayloadWord(ctx, index))}; + return ctx.OpSelect(ctx.U32[1], ctx.OpIEqual(ctx.U1, address, ctx.u32_zero_value), workgroup, + word); +} + +void EmitIsbeWriteAttribute(EmitContext& ctx, Id address, Id value) { + const Id index{ctx.OpShiftRightLogical(ctx.U32[1], address, ctx.Const(2U))}; + if (ctx.stage == Stage::Task) { + ctx.OpStore(PayloadWord(ctx, index), value); + return; + } + ctx.OpStore(SharedWord(ctx, ctx.mesh_attributes, Clamped(ctx, index, ctx.mesh_attribute_words)), + value); +} + +void EmitIsbeWriteMapU8(EmitContext& ctx, Id address, Id value) { + if (ctx.stage != Stage::Mesh) { + throw NotImplementedException("Stage buffer map write outside mesh programs"); + } + ctx.OpStore(SharedWord(ctx, ctx.mesh_map, Clamped(ctx, address, ctx.mesh_map_bytes)), + ctx.OpBitwiseAnd(ctx.U32[1], value, ctx.Const(0xffU))); +} + +void EmitIsbeWriteMapU32(EmitContext& ctx, const IR::Value& address, Id value) { + // The primitive count: the map is written from its end, so its first word is at byte 3. + if (ctx.stage != Stage::Mesh || !address.IsImmediate() || address.U32() != 3) { + throw NotImplementedException("32-bit stage buffer map write"); + } + ctx.OpStore(ctx.mesh_primitive_count, value); +} + +void EmitMeshPrologue(EmitContext& ctx) { + if (ctx.stage != Stage::Mesh) { + return; + } + // Every invocation stores the zero: no branch here, the entry block's label is a phi + // predecessor for later blocks. + ctx.OpStore(ctx.mesh_primitive_count, ctx.u32_zero_value); + Barrier(ctx); +} + +void EmitMeshEpilogue(EmitContext& ctx) { + if (ctx.stage == Stage::Task) { + TaskEpilogue(ctx); + } else if (ctx.stage == Stage::Mesh) { + MeshEpilogue(ctx); + } +} + +} // namespace Shader::Backend::SPIRV diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_special.cpp b/src/shader_recompiler/backend/spirv/emit_spirv_special.cpp index 8eca1fac29..2b36c1fd8d 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_special.cpp +++ b/src/shader_recompiler/backend/spirv/emit_spirv_special.cpp @@ -137,9 +137,16 @@ void EmitPrologue(EmitContext& ctx) { if (ctx.stage == Stage::VertexB || ctx.stage == Stage::Geometry) { SetFixedPipelinePointSize(ctx); } + if (ctx.stage == Stage::Task || ctx.stage == Stage::Mesh) { + EmitMeshPrologue(ctx); + } } void EmitEpilogue(EmitContext& ctx) { + if (ctx.stage == Stage::Task || ctx.stage == Stage::Mesh) { + EmitMeshEpilogue(ctx); + return; + } if (ctx.stage == Stage::VertexB && ctx.runtime_info.convert_depth_mode && !ctx.profile.support_native_ndc) { ConvertDepthMode(ctx); diff --git a/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp b/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp index b562e82983..f80f0440df 100644 --- a/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp +++ b/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp @@ -257,6 +257,10 @@ std::string_view StageName(Stage stage) { return "fs"; case Stage::Compute: return "cs"; + case Stage::Task: + return "ts"; + case Stage::Mesh: + return "ms"; } throw InvalidArgument("Invalid stage {}", stage); } @@ -475,6 +479,7 @@ EmitContext::EmitContext(const Profile& profile_, const RuntimeInfo& runtime_inf DefineCommonTypes(program.info); DefineCommonConstants(); DefineInterfaces(program); + DefineMesh(program); DefineLocalMemory(program); DefineSharedMemory(program); DefineSharedMemoryFunctions(program); @@ -1586,6 +1591,16 @@ void EmitContext::DefineInputs(const IR::Program& program) { } const bool is_integer = input_type == AttributeType::SignedInt || input_type == AttributeType::UnsignedInt; + if (runtime_info.per_primitive_generics.test(index)) { + // Written per primitive by a mesh program: constant over the primitive. + AddExtension("SPV_EXT_mesh_shader"); + AddCapability(spv::Capability::MeshShadingEXT); + Decorate(id, spv::Decoration::PerPrimitiveEXT); + if (is_integer) { + Decorate(id, spv::Decoration::Flat); + } + continue; + } if (is_integer) { Decorate(id, spv::Decoration::Flat); } else { @@ -1614,8 +1629,120 @@ void EmitContext::DefineInputs(const IR::Program& program) { } } +void EmitContext::DefineMesh(const IR::Program& program) { + if (stage != Stage::Task && stage != Stage::Mesh) { + return; + } + AddExtension("SPV_EXT_mesh_shader"); + AddCapability(spv::Capability::MeshShadingEXT); + mesh_info = program.mesh; + mesh_threads = program.workgroup_size[0]; + // The task's output region, which its mesh workgroups read as their input region. + const Id payload_type{TypeArray(U32[1], Const(mesh_info.payload_words))}; + mesh_payload = DefineVariable(*this, payload_type, std::nullopt, + spv::StorageClass::TaskPayloadWorkgroupEXT); + Name(mesh_payload, "task_payload"); + mesh_payload_u32 = TypePointer(spv::StorageClass::TaskPayloadWorkgroupEXT, U32[1]); + if (stage == Stage::Task) { + return; + } + // Output attribute region: groups of 32 vertices, then groups of 32 primitives, one 32-word + // line per attribute. The map region keeps one map byte per word. + const u32 vertex_lines{static_cast<u32>(mesh_info.vertex_lines.size())}; + const u32 primitive_lines{static_cast<u32>(mesh_info.primitive_lines.size())}; + mesh_primitive_base = Common::DivCeil(mesh_info.max_vertices, 32U) * vertex_lines * 32; + mesh_attribute_words = (std::max)( + mesh_primitive_base + Common::DivCeil(mesh_info.max_primitives, 32U) * primitive_lines * 32, + 1U); + mesh_map_bytes = 4 + 3 * mesh_info.max_primitives; + mesh_shared_u32 = TypePointer(spv::StorageClass::Workgroup, U32[1]); + mesh_attributes = DefineVariable(*this, TypeArray(U32[1], Const(mesh_attribute_words)), + std::nullopt, spv::StorageClass::Workgroup); + Name(mesh_attributes, "isbe_attributes"); + mesh_map = DefineVariable(*this, TypeArray(U32[1], Const(mesh_map_bytes)), std::nullopt, + spv::StorageClass::Workgroup); + Name(mesh_map, "isbe_map"); + mesh_primitive_count = + DefineVariable(*this, U32[1], std::nullopt, spv::StorageClass::Workgroup); + Name(mesh_primitive_count, "isbe_primitive_count"); + + const Id vertex_count{Const(mesh_info.max_vertices)}; + const Id primitive_count{Const(mesh_info.max_primitives)}; + mesh_primitive_indices = + DefineVariable(*this, TypeArray(U32[3], primitive_count), + spv::BuiltIn::PrimitiveTriangleIndicesEXT, spv::StorageClass::Output); + std::array<Id, IR::NUM_GENERICS> generics[2]{}; + Id point_size{}, primitive_layer{}, primitive_viewport{}, primitive_id_output{}; + const auto per_primitive_builtin = [&](Id& variable, spv::BuiltIn builtin) { + if (!Sirit::ValidId(variable)) { + variable = DefineVariable(*this, TypeArray(U32[1], primitive_count), builtin, + spv::StorageClass::Output); + Decorate(variable, spv::Decoration::PerPrimitiveEXT); + } + return MeshLineTarget{variable, ~0U, false}; + }; + const auto target = [&](u32 line, bool per_primitive) -> MeshLineTarget { + const IR::Attribute attribute{line}; + if (IR::IsGeneric(attribute)) { + const u32 index{IR::GenericAttributeIndex(attribute)}; + Id& variable{generics[per_primitive][index]}; + if (!Sirit::ValidId(variable)) { + variable = DefineVariable( + *this, TypeArray(F32[4], per_primitive ? primitive_count : vertex_count), + std::nullopt, spv::StorageClass::Output); + Decorate(variable, spv::Decoration::Location, index); + if (per_primitive) { + Decorate(variable, spv::Decoration::PerPrimitiveEXT); + } + } + return {variable, IR::GenericAttributeElement(attribute), true}; + } + if (per_primitive) { + switch (attribute) { + case IR::Attribute::PrimitiveId: + return per_primitive_builtin(primitive_id_output, spv::BuiltIn::PrimitiveId); + case IR::Attribute::Layer: + return per_primitive_builtin(primitive_layer, spv::BuiltIn::Layer); + case IR::Attribute::ViewportIndex: + return per_primitive_builtin(primitive_viewport, spv::BuiltIn::ViewportIndex); + default: + return {}; + } + } + switch (attribute) { + case IR::Attribute::PositionX: + case IR::Attribute::PositionY: + case IR::Attribute::PositionZ: + case IR::Attribute::PositionW: + if (!Sirit::ValidId(output_position)) { + output_position = DefineVariable(*this, TypeArray(F32[4], vertex_count), + spv::BuiltIn::Position, spv::StorageClass::Output); + } + return {output_position, + static_cast<u32>(attribute) - static_cast<u32>(IR::Attribute::PositionX), true}; + case IR::Attribute::PointSize: + if (!Sirit::ValidId(point_size)) { + point_size = DefineVariable(*this, TypeArray(F32[1], vertex_count), + spv::BuiltIn::PointSize, spv::StorageClass::Output); + } + return {point_size, ~0U, true}; + default: + return {}; // per-primitive values in vertex lines have no vertex output + } + }; + for (const u32 line : mesh_info.vertex_lines) { + mesh_vertex_targets.push_back(target(line, false)); + } + for (const u32 line : mesh_info.primitive_lines) { + mesh_primitive_targets.push_back(target(line, true)); + } +} + void EmitContext::DefineOutputs(const IR::Program& program) { const Info& info{program.info}; + if (stage == Stage::Task || stage == Stage::Mesh) { + return; // DefineMesh + } const std::optional<u32> invocations{program.invocations}; if (runtime_info.convert_depth_mode || info.stores.AnyComponent(IR::Attribute::PositionX) || stage == Stage::VertexB) { diff --git a/src/shader_recompiler/backend/spirv/spirv_emit_context.h b/src/shader_recompiler/backend/spirv/spirv_emit_context.h index a5821e9e93..1f9b534aaf 100644 --- a/src/shader_recompiler/backend/spirv/spirv_emit_context.h +++ b/src/shader_recompiler/backend/spirv/spirv_emit_context.h @@ -363,6 +363,32 @@ public: std::vector<Id> interfaces; std::vector<std::pair<u32, spv::Op>> opcode_rewrites; + // Task and mesh programs. Their stage buffer: the task's output region is the task payload + // (the mesh stage's input region); the mesh output regions live in workgroup memory until + // the epilogue copies them to the outputs. The two mesh instructions Sirit lacks are emitted + // as OpStore markers with these operands and replaced after assembly (PatchMeshInstructions). + struct MeshLineTarget { + Id variable; ///< arrayed output + u32 element; ///< vector element, or ~0 for a scalar output + bool is_float; ///< bitcast the stage buffer word to F32 + }; + Id mesh_payload{}; + Id mesh_payload_u32{}; ///< pointer type to one payload word + Id mesh_attributes{}; + Id mesh_map{}; + Id mesh_primitive_count{}; + Id mesh_shared_u32{}; ///< pointer type to one workgroup word + Id mesh_primitive_indices{}; + MeshInfo mesh_info{}; + u32 mesh_threads{}; + u32 mesh_attribute_words{}; + u32 mesh_map_bytes{}; + u32 mesh_primitive_base{}; ///< first word of the per-primitive groups + std::vector<MeshLineTarget> mesh_vertex_targets; + std::vector<MeshLineTarget> mesh_primitive_targets; + std::array<Id, 2> mesh_outputs_marker{}; + std::array<Id, 4> mesh_tasks_marker{}; + Id load_const_func_u8{}; Id load_const_func_u16{}; Id load_const_func_u32{}; @@ -402,6 +428,7 @@ private: void DefineInputs(const IR::Program& program); void DefineOutputs(const IR::Program& program); + void DefineMesh(const IR::Program& program); }; } // namespace Shader::Backend::SPIRV diff --git a/src/shader_recompiler/environment.h b/src/shader_recompiler/environment.h index 67486430c8..d186b0c1d3 100644 --- a/src/shader_recompiler/environment.h +++ b/src/shader_recompiler/environment.h @@ -121,9 +121,22 @@ public: return is_proprietary_driver; } + /// Task and mesh programs: SET_MESH_SHADER_A, SET_MESH_SHADER_B and SET_MESH_INIT_SHADER. + [[nodiscard]] const std::array<u32, 3>& MeshRegisters() const noexcept { + return mesh_registers; + } + + /// Mesh programs: the header of the pass-through geometry program bound with them, whose + /// output map lists the per-primitive outputs. + [[nodiscard]] const ProgramHeader& MeshPrimitiveHeader() const noexcept { + return mesh_primitive_header; + } + protected: ProgramHeader sph{}; std::array<u32, 8> gp_passthrough_mask{}; + std::array<u32, 3> mesh_registers{}; + ProgramHeader mesh_primitive_header{}; Stage stage{}; u32 start_address{}; bool is_proprietary_driver{}; diff --git a/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h b/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h index 031f7fac75..66593b68e4 100644 --- a/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h +++ b/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h @@ -34,6 +34,9 @@ struct LoweredProgram { std::vector<u32> source_instruction_offsets; u32 source_size{}; u32 required_texture_buffer = ~0U; + // UR28 and up seen without a mirror: lowering again with this many registers below R216 + // reserved for them (even, so pairs stay adjacent) maps them. + u32 high_uniform_registers{}; u32 error_offset{}; std::string_view error; @@ -43,8 +46,10 @@ struct LoweredProgram { // Lower only operations whose semantics are representable by the existing // Maxwell frontend. These are new instructions, not reinterpretations of the // native bytes. Scheduling/control words are added by the environment adapter. -inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) { +inline LoweredProgram LowerToMaxwellWith(std::span<const Instruction> code, u32 high_uniforms) { LoweredProgram result; + // UR28 and up (when used) live just below R216: UR28 at R(216 - high_uniforms). + const u32 high_uniform_base = 216 - high_uniforms; struct Relocation { u32 output_index; s64 target; }; std::vector<Relocation> relocations; std::vector<u32> barrier_tokens; @@ -55,8 +60,8 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) { bool uses_uniform_bank = false; for (const auto& instruction : code) { const u32 op = instruction.Bits(0, 12); - uses_uniform_bank |= op == 0x3c4 || op == 0x2bd || op == 0xc0c || op == 0x986 || op == 0x9a8 || op == 0xc12 || op == 0xc17 || op == 0xe24 || op == 0xc24 || op == 0xd00 || op == 0xd09 || op == 0x9c3 || op == 0xc45 || op == 0x2a4 || op == 0x891 || op == 0x890 || op == 0x290 || op == 0x28c || op == 0x88c || op == 0x287 || op == 0x887 || op == 0x958 || op == 0xab9 || op == 0xabb || op == 0x899 || op == 0xc20 || op == 0xe21 || op == 0xe23 || op == 0xc02 || - op == 0x886 || op == 0x3c2 || op == 0xc82 || op == 0x882 || op == 0x292 || op == 0x892 || op == 0x291 || op == 0x981 || op == 0xc13 || op == 0xc10 || op == 0xc23 || op == 0x526 || op == 0x2be || op == 0x8a4 || op == 0xd05 || (op == 0x984 && instruction.Bits(91, 1)) || op == 0x89c || op == 0x883 || op == 0x2a5 || op == 0xd06 || op == 0x299 || op == 0xc09 || (op == 0x81c && instruction.Bits(67, 1)); + uses_uniform_bank |= op == 0x3c4 || op == 0x2bd || op == 0x896 || op == 0xf60 || op == 0xc11 || op == 0xd08 || op == 0xc0c || op == 0x986 || op == 0x9a8 || op == 0xc12 || op == 0xc17 || op == 0xe24 || op == 0xc24 || op == 0xd00 || op == 0xd09 || op == 0x9c3 || op == 0xc45 || op == 0x2a4 || op == 0x891 || op == 0x890 || op == 0x290 || op == 0x28c || op == 0x88c || op == 0x287 || op == 0x887 || op == 0x958 || op == 0xab9 || op == 0xabb || op == 0x899 || op == 0xc20 || op == 0xe21 || op == 0xe23 || op == 0xc02 || + op == 0x886 || op == 0x3c2 || op == 0xc82 || op == 0x882 || op == 0x292 || op == 0x892 || op == 0x291 || op == 0x981 || op == 0xc13 || op == 0xc10 || op == 0xc23 || op == 0x526 || op == 0x2be || op == 0x8a4 || op == 0xd05 || (op == 0x984 && instruction.Bits(91, 1)) || op == 0x89c || op == 0x883 || op == 0x2a5 || op == 0xd06 || op == 0xf63 || op == 0xf69 || op == 0xc0b || op == 0x8a5 || op == 0x299 || op == 0xc09 || (op == 0x81c && instruction.Bits(67, 1)); } const auto address_of = [](u32 index) { return 0x50U + index * 8 + ((index + 1) / 3) * 8; }; const auto fail = [&](u32 index, std::string_view reason) { @@ -101,22 +106,24 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) { for (u32 bit : {16U, 24U, 32U, 64U}) { if (bit == 32 && insn.Bits(9, 3) != 1 && opcode != 0xb60 && opcode != 0xb66) continue; const u32 reg = insn.Bits(bit, 8); - if (reg >= (uses_uniform_bank ? 216U : 246U) && reg < 255) return fail(index, "native program uses lowering scratch registers"); + if (reg >= (uses_uniform_bank ? high_uniform_base : 246U) && reg < 255) return fail(index, "native program uses lowering scratch registers"); } // UR0-UR21 live in R216-R237 and UR22-UR27 in R240-R245 (R239 holds the uniform - // predicates), so even/odd pairs stay adjacent. Sites that still check reg < 16 rely on - // UR0-UR15 being R216-R231. + // predicates), so even/odd pairs stay adjacent; UR28 and up below R216. Sites that + // still check reg < 16 rely on UR0-UR15 being R216-R231. const auto uniform_register = [&](u32 reg) -> u32 { if (reg < 22) return 216 + reg; if (reg < 28) return 218 + reg; if (reg == 63) return 255; + if (reg < 63 && reg - 28 < high_uniforms) return high_uniform_base + (reg - 28); + if (reg < 63) result.high_uniform_registers = (std::max)(result.high_uniform_registers, (reg - 26) & ~1U); return ~0U; }; bool rewritten_uniform = false; bool uniform_guard = false; u32 uniform_output = 7; // ULOP3's UPn output: computed in P(6-n), then merged into R239 if (opcode == 0x2a4 || opcode == 0x8a4 || opcode == 0x891 || opcode == 0x890 || opcode == 0x290 || - opcode == 0x292 || opcode == 0x892 || opcode == 0x291) { + opcode == 0x292 || opcode == 0x892 || opcode == 0x291 || opcode == 0x896 || opcode == 0x8a5) { if (insn.Bits(12, 4) != 7 && opcode != 0x292 && opcode != 0x892) return fail(index, "native uniform arithmetic predication"); uniform_guard = insn.Bits(12, 4) != 7; @@ -138,7 +145,8 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) { insn.words[2] = (insn.words[2] & ~(7U << 17)) | ((6 - uniform_output) << 17); } opcode = opcode == 0x2a4 ? 0x224 : opcode == 0x8a4 ? 0x824 : opcode == 0x891 ? 0x811 : opcode == 0x890 ? 0x810 - : opcode == 0x292 ? 0x212 : opcode == 0x892 ? 0x812 : opcode == 0x291 ? 0x211 : 0x210; + : opcode == 0x292 ? 0x212 : opcode == 0x892 ? 0x812 : opcode == 0x291 ? 0x211 + : opcode == 0x896 ? 0x816 : opcode == 0x8a5 ? 0x825 : 0x210; } const bool uniform_predication = opcode == 0xabb || opcode == 0xab9 || opcode == 0xc82 || opcode == 0x882; const u32 native_predicate = insn.Bits(12, 4); @@ -496,6 +504,28 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) { emit(0x5c10000000070000ULL | d_low | (u64{250} << 8) | (u64{255} << 20)); break; } + case 0x825: { // IMAD.WIDE.U32 Rd:Rd+1 = Ra * immediate + Rc:Rc+1 (pointer arithmetic) + const u32 a = insn.Bits(24, 8), b = insn.words[1], c = insn.Bits(64, 8); + const u32 d = static_cast<u32>(dest), c_high = c == 255 ? 255U : c + 1; + if ((insn.words[2] & ~0xffU) != 0x078e0000 || (d & 1) || d >= 246 || a >= 246 || + (c != 255 && ((c & 1) || c >= 246))) + return fail(index, "native wide multiply mode"); + // As UIMAD.WIDE.U32: the high word in R253, the low word in R250, then the 64-bit add. + xmad(250, a, b, 255, false, false, 0, true); + xmad(251, a, b, 250, true, false, 2, true); + xmad(250, a, b, 251, false, true, 1, true); + xmad(252, a, b, 251, true, true, 2, true); + emit(0x3828000000000000ULL | predicate | 250 | (u64{250} << 8) | (u64{16} << 20)); + emit(0x5c10000000000000ULL | predicate | 253 | (u64{250} << 8) | (u64{252} << 20)); + xmad(250, a, b, 255, false, false, 0, true); + xmad(251, a, b, 255, true, false, 0, true); + xmad(251, a, b, 251, false, true, 0, true); + emit(0x5c18000000000000ULL | predicate | 250 | (u64{251} << 8) | (u64{250} << 20) | (u64{16} << 39)); + emit(0x5c10000000000000ULL | predicate | 250 | (u64{250} << 8) | (u64{c} << 20) | (u64{1} << 47)); + emit(0x5c10000000000000ULL | predicate | (d + 1) | (u64{253} << 8) | (u64{c_high} << 20) | (u64{1} << 43)); + emit(0x5c10000000000000ULL | predicate | d | (u64{250} << 8) | (u64{255} << 20)); + break; + } case 0x827: { // IMAD.HI.U32, captured zero addend if (insn.words[2] != 0x078e00ff) return fail(index, "native high multiply mode"); const u32 a = insn.Bits(24, 8), b = insn.words[1]; @@ -708,36 +738,42 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) { } case 0x899: { // USHF.L.U32; accept a zero word outside the selected half. const u32 dst = insn.Bits(16, 8), low = insn.Bits(24, 8), high = insn.Bits(64, 8); - if (insn.words[2] == (0x08011400U | high) && low == 63 && high < 16 && - dst < 16 && native_predicate == 7 && insn.words[1] <= 32) { - emit(0x3828000000000000ULL | predicate | (dst + 216) | - (u64{high + 216} << 8) | (u64{insn.words[1]} << 20) | (u64{1} << 48)); + const u32 dst_reg = uniform_register(dst), low_reg = uniform_register(low); + const u32 high_reg = uniform_register(high); + const bool dst_ok = dst != 63 && dst_reg != ~0U; + const bool low_ok = low != 63 && low_reg != ~0U, high_ok = high != 63 && high_reg != ~0U; + if (insn.words[2] == (0x08011400U | high) && low == 63 && high_ok && + dst_ok && native_predicate == 7 && insn.words[1] <= 32) { + emit(0x3828000000000000ULL | predicate | dst_reg | + (u64{high_reg} << 8) | (u64{insn.words[1]} << 20) | (u64{1} << 48)); break; } // Logical right shifts of one register (bit 76 right, bit 80 high word, bits 73-74 // U32): USHF.R.U32 URd, URa, n, URZ and USHF.R.U32.HI URd, URZ, n, URb. - const bool right_low = insn.words[2] == 0x0800163f && low < 16; - const bool right_high = insn.words[2] == (0x08011600U | high) && low == 63 && high < 16; - if ((right_low || right_high) && dst < 16 && native_predicate == 7 && insn.words[1] <= 32) { - emit(0x3828000000000000ULL | predicate | (dst + 216) | - (u64{(right_high ? high : low) + 216} << 8) | (u64{insn.words[1]} << 20)); + const bool right_low = insn.words[2] == 0x0800163f && low_ok; + const bool right_high = insn.words[2] == (0x08011600U | high) && low == 63 && high_ok; + if ((right_low || right_high) && dst_ok && native_predicate == 7 && insn.words[1] <= 32) { + emit(0x3828000000000000ULL | predicate | dst_reg | + (u64{right_high ? high_reg : low_reg} << 8) | (u64{insn.words[1]} << 20)); break; } - const bool high_result = insn.words[2] == (0x08010600U | high) && low == 63 && high < 16; - const bool low_result = insn.words[2] == 0x0800063f && low < 16; - if (dst >= 16 || native_predicate != 7 || insn.words[1] > 32 || (!high_result && !low_result)) + const bool high_result = insn.words[2] == (0x08010600U | high) && low == 63 && high_ok; + const bool low_result = insn.words[2] == 0x0800063f && low_ok; + if (!dst_ok || native_predicate != 7 || insn.words[1] > 32 || (!high_result && !low_result)) return fail(index, "native uniform shift mode"); - emit(0x3848000000000000ULL | predicate | (dst + 216) | - (u64{(high_result ? high : low) + 216} << 8) | (u64{insn.words[1]} << 20)); + emit(0x3848000000000000ULL | predicate | dst_reg | + (u64{high_result ? high_reg : low_reg} << 8) | (u64{insn.words[1]} << 20)); break; } - case 0xb82: { // LDC.64 indexed byte offset - const u32 width = insn.Bits(73, 3); - if ((width != 4 && width != 5) || insn.Bits(78, 2) || insn.Bits(91, 1) || + case 0xb82: { // LDC.64 indexed byte offset; bits 78-79 the mode as Maxwell's (2 = IS) + // .IS: Ra + offset addresses all constant buffers as one space, the buffer in its high + // half (Kirby Air Riders' vertex shaders), which Eden's Maxwell LDC implements. + const u32 width = insn.Bits(73, 3), mode = insn.Bits(78, 2); + if ((width != 4 && width != 5) || (mode != 0 && mode != 2) || insn.Bits(91, 1) || bank() >= 18 || (width == 5 && ((dest & 1) || dest >= 254))) return fail(index, "native indexed constant load mode"); emit(base(0xef90000000000000ULL) | (u64{insn.Bits(24, 8)} << 8) | - (u64{insn.Bits(38, 16)} << 20) | (u64{bank()} << 36) | (u64{width} << 48)); + (u64{insn.Bits(38, 16)} << 20) | (u64{bank()} << 36) | (u64{mode} << 44) | (u64{width} << 48)); break; } case 0xf89: { // SHFL with immediate lane and segmentation/clamp mask, any mode. @@ -836,44 +872,74 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) { emit(0x3828000000000000ULL | predicate | (dest + 216) | (u64{insn.words[1] + 216} << 8) | (u64{1} << 39) | (u64{1} << 40)); break; - case 0x3a1: { // MATCH.ALL.U64 with only the all-equal predicate retained. + case 0x2bf: // UPOPC, as POPC of the uniform's register. + if (dest >= 16 || insn.words[1] >= 16 || insn.words[2] != 0x08000000 || native_predicate != 7) + return fail(index, "native uniform population count mode"); + emit(0x5c08000000000000ULL | predicate | (dest + 216) | (u64{insn.words[1] + 216} << 20)); + break; + case 0x3a1: { // MATCH.ALL (.U32, or .U64 on bit 73) with only the all-equal predicate retained. const u32 source = insn.Bits(24, 8), target = insn.Bits(81, 3); - if (dest != 255 || source >= 254 || (source & 1) || target >= 6 || - native_predicate != 7 || insn.words[1] || (insn.words[2] & ~0xe0000U) != 0x200) + const u32 words = insn.Bits(73, 1) ? 2 : 1; + const u32 scratch = target == 6 ? 5U : 6U; // the comparison, saved around the match + if (dest != 255 || source >= 254 || (words == 2 && (source & 1)) || target == 7 || + native_predicate != 7 || insn.words[1] || (insn.words[2] & ~0xe0200U) != 0) return fail(index, "native warp match mode"); - save_predicate(); + save_predicate(1U << scratch); + // Compare every lane with the lowest active one, then require all to agree. emit(0x50d8000000070000ULL | 250 | (u64{7} << 39) | (u64{7} << 45) | (u64{1} << 48)); emit(0x5c10000000070000ULL | 251 | (u64{250} << 8) | (u64{255} << 20) | (u64{1} << 49)); emit(0x5c40000000070000ULL | 250 | (u64{250} << 8) | (u64{251} << 20) | (u64{7} << 48)); emit(0x5c30000000070000ULL | 250 | (u64{250} << 20)); - for (u32 word = 0; word < 2; ++word) + for (u32 word = 0; word < words; ++word) emit(0xef10000000070000ULL | (251 + word) | (u64{source + word} << 8) | (u64{250} << 20) | (u64{1} << 29) | (u64{31} << 34) | (u64{7} << 48)); - emit(0x5b60000000070000ULL | 7 | (u64{6} << 3) | (u64{source} << 8) | + emit(0x5b60000000070000ULL | 7 | (u64{scratch} << 3) | (u64{source} << 8) | (u64{251} << 20) | (u64{7} << 39) | (u64{2} << 49)); - emit(0x5b60000000070000ULL | 7 | (u64{6} << 3) | (u64{source + 1} << 8) | - (u64{252} << 20) | (u64{6} << 39) | (u64{2} << 49)); - emit(0x50d8000000070000ULL | 255 | (u64{6} << 39) | (u64{target} << 45)); - restore_predicate(); - break; - } - case 0x3c4: { // REDUX.SUM (modulo 2^32) or .MAX (unsigned) over the actual active lanes. + if (words == 2) + emit(0x5b60000000070000ULL | 7 | (u64{scratch} << 3) | (u64{source + 1} << 8) | + (u64{252} << 20) | (u64{scratch} << 39) | (u64{2} << 49)); + emit(0x50d8000000070000ULL | 255 | (u64{scratch} << 39) | (u64{target} << 45)); + restore_predicate(1U << scratch); + break; + } + case 0x80d: { // CSMTEST: a test of shading-mode state Eden does not model, taken as false. + // Kirby Air Riders' vertex-side shaders run "CSMTEST.A.FCSM_TR P0" then "@P0 EXIT": + // never leave early. Outputs of PT discard the result. + const u32 first = insn.Bits(81, 3), second = insn.Bits(84, 3); + if (insn.Bits(87, 4) != 7 || insn.Bits(16, 16)) + return fail(index, "native CSM test mode"); + for (const u32 target : {first, second}) + if (target != 7) + emit(0x5090000000000000ULL | predicate | (u64{target} << 3) | 7 | (u64{7} << 12) | + (u64{1} << 15) | (u64{7} << 29) | (u64{7} << 39)); + break; + } + case 0x3c4: { // REDUX.SUM (modulo 2^32), .MIN/.MAX (unsigned), .OR or .XOR over the actual active lanes. const u32 source = insn.Bits(24, 8), target = uniform_register(static_cast<u32>(dest)); - const bool maximum = insn.words[2] == 0x14000; + const bool maximum = insn.words[2] == 0x14000, minimum = insn.words[2] == 0x10000; + // OR and XOR: inactive lanes contribute zero, as for the sum. Maxwell LOP: 1 = OR, 2 = XOR. + const u32 logical = insn.words[2] == 0x4000 ? 1U : insn.words[2] == 0x8000 ? 2U : 0U; if (target == ~0U || target == 255 || insn.words[1] || - (insn.words[2] != 0xc000 && !maximum) || native_predicate != 7) + (insn.words[2] != 0xc000 && !maximum && !minimum && !logical) || native_predicate != 7) return fail(index, "native warp sum mode"); emit(0x50d8000000070000ULL | 250 | (u64{7} << 39) | (u64{7} << 45) | (u64{1} << 48)); - mov_imm(target, 0); + mov_imm(target, minimum ? ~0U : 0U); for (u32 lane = 0; lane < 32; ++lane) { emit(0xef10000000070000ULL | 251 | (u64{source} << 8) | (u64{lane} << 20) | (u64{1} << 28) | (u64{1} << 29) | (u64{31} << 34) | (u64{7} << 48)); emit(0x3828000000070000ULL | 252 | (u64{250} << 8) | (u64{lane} << 20)); emit(0x0400000000070000ULL | 252 | (u64{252} << 8) | (u64{1} << 20)); emit(0x5c10000000070000ULL | 252 | (u64{255} << 8) | (u64{252} << 20) | (u64{1} << 48)); - emit(0x5c40000000070000ULL | 251 | (u64{251} << 8) | (u64{252} << 20) | (u64{7} << 48)); - if (maximum) { + // Inactive lanes: 0, or ~0 for the minimum (OR with the inverted lane mask). + emit(0x5c40000000070000ULL | 251 | (u64{251} << 8) | (u64{252} << 20) | (u64{7} << 48) | + (u64{minimum} << 40) | (u64{minimum} << 41)); + if (minimum) { + emit(0x5c20000000070000ULL | target | (u64{target} << 8) | (u64{251} << 20) | (u64{7} << 39)); + } else if (maximum) { emit(0x5c20000000070000ULL | target | (u64{target} << 8) | (u64{251} << 20) | (u64{15} << 39)); + } else if (logical) { + emit(0x5c40000000070000ULL | target | (u64{target} << 8) | (u64{251} << 20) | + (u64{logical} << 41) | (u64{7} << 48)); } else { emit(0x5c10000000070000ULL | target | (u64{target} << 8) | (u64{251} << 20)); } @@ -881,8 +947,9 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) { break; } case 0xf8c: { // ATOMS.POPC.INC.32 with no returned value: one increment per active lane. - const u32 stride = insn.Bits(78, 2); - if (dest != 255 || insn.words[1] || (insn.words[2] & ~0xc000U) != 0x0d00003f) + const u32 stride = insn.Bits(78, 2), offset = insn.Bits(40, 24); + if (dest != 255 || (insn.words[1] & 0xff) || (offset & 3) || + (insn.words[2] & ~0xc000U) != 0x0d00003f) return fail(index, "native shared increment mode"); u32 address = insn.Bits(24, 8); if (stride && address != 255) { @@ -891,15 +958,18 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) { address = 251; } mov_imm(250, 1); - emit(0xec00000000000000ULL | predicate | 255 | (u64{address} << 8) | (u64{250} << 20)); + emit(0xec00000000000000ULL | predicate | 255 | (u64{address} << 8) | (u64{250} << 20) | + (u64{offset >> 2} << 30)); break; } - case 0x38c: { // ATOMS.ADD.U32, naturally aligned shared byte offset. - const u32 offset = insn.Bits(40, 24); - if (insn.words[2] || (offset & 3)) return fail(index, "native shared atomic mode"); + case 0x38c: { // ATOMS (ADD/MIN/MAX/INC/DEC/AND/OR/XOR/EXCH, U32 or S32), aligned byte offset. + // The operation (bits 87-90) is numbered as in Maxwell ATOMS (52-55); bit 73 is S32. + const u32 offset = insn.Bits(40, 24), operation = insn.Bits(87, 4); + if ((insn.words[2] & ~0x07800200U) || operation > 8 || (offset & 3)) + return fail(index, "native shared atomic mode"); emit(0xec00000000000000ULL | predicate | dest | (u64{insn.Bits(24, 8)} << 8) | (u64{insn.Bits(32, 8)} << 20) | - (u64{offset >> 2} << 30)); + (u64{insn.Bits(73, 1)} << 28) | (u64{offset >> 2} << 30) | (u64{operation} << 52)); break; } case 0x3a8: { // ATOMG.E.ADD.STRONG.GPU U32 at a 64-bit register address plus an offset. @@ -924,14 +994,14 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) { (u64{1} << 48)); break; } - case 0xb97: { // SULD.P.2D/2D_ARRAY.IGN, direct descriptor, channel mask at 72-75. + case 0xb97: { // SULD.P.2D/2D_ARRAY/3D.IGN, direct descriptor, channel mask at 72-75. const u32 binding = insn.Bits(54, 5), offset = insn.Bits(40, 14); const u32 coordinate = insn.Bits(24, 8); const u32 channels = insn.Bits(72, 4); - const u32 dimension = insn.Bits(61, 3); // Maxwell's codes: 3 = 2D, 4 = 2D_ARRAY - if ((insn.words[2] & ~0xf00U) != 0x001e0000 || channels == 0 || (dimension != 3 && dimension != 4) || + const u32 dimension = insn.Bits(61, 3); // Maxwell's codes: 3 = 2D, 4 = 2D_ARRAY, 5 = 3D + if ((insn.words[2] & ~0xf00U) != 0x001e0000 || channels == 0 || dimension < 3 || dimension > 5 || binding >= 18 || offset > 0x1fff || insn.Bits(59, 2) || dest == 255 || - coordinate > (dimension == 4 ? 252U : 253U)) + coordinate > (dimension >= 4 ? 252U : 253U)) return fail(index, "native formatted surface load mode"); if (result.required_texture_buffer != ~0U && result.required_texture_buffer != binding) return fail(index, "native surface descriptor buffer conflict"); @@ -954,6 +1024,21 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) { (u64{15} << 20) | (u64{dimension} << 33) | (u64{offset} << 36) | (u64{1} << 51)); break; } + case 0x59f: { // SURED.D.2D.ADD.STRONG.GPU.IGN U32, direct descriptor (title 1's race compute). + const u32 binding = insn.Bits(54, 5), offset = insn.Bits(40, 14); + const u32 data = insn.Bits(32, 8), coordinate = insn.Bits(24, 8); + if (insn.words[2] != 0x0010e000 || insn.Bits(61, 3) != 3 || binding >= 18 || + offset > 0x1fff || insn.Bits(59, 2) || coordinate > 253) + return fail(index, "native surface reduction mode"); + if (result.required_texture_buffer != ~0U && result.required_texture_buffer != binding) + return fail(index, "native surface descriptor buffer conflict"); + result.required_texture_buffer = binding; + // Maxwell SURED (as the frontend decodes it): operand 0-7, coordinates 8-15, ADD and + // U32 are zero at 24 and 20, type 33 (3 = 2D), bound offset 36, bound 51. + emit(0xeb40000000000000ULL | predicate | data | (u64{coordinate} << 8) | + (u64{3} << 33) | (u64{offset} << 36) | (u64{1} << 51)); + break; + } case 0x301: // BREV register. if (insn.words[2] || insn.words[1] > 255) return fail(index, "native bit reverse mode"); emit(base(0x3828000000000000ULL) | (u64{insn.words[1]} << 8) | @@ -1188,10 +1273,21 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) { } break; } - case 0x919: case 0x9c3: { // S2R / S2UR thread and workgroup identities. + case 0x919: case 0x9c3: { // S2R / S2UR: special registers the Maxwell S2R handles. + // Same numbering as Maxwell: lane id (0), invocation id/info, Y direction, thread kill + // (17-19, 29), W scale factors (30-31, Kirby Air Riders), thread and workgroup ids, + // lane masks (56-60). const u32 special = insn.Bits(72, 8); + if (special == 132 && !insn.words[1] && insn.words[2] == (special << 8)) { + // SR_VARIABLE_RATE: without variable-rate shading every fragment is 1x1, its width + // and height in bytes 1 and 2 (Nintendo Switch Sports Resort multiplies them). + const u32 target = opcode == 0x9c3 ? uniform_register(static_cast<u32>(dest)) : static_cast<u32>(dest); + if (target == ~0U) return fail(index, "native uniform special destination"); + mov_imm(target, 0x00010100); + break; + } if (insn.words[1] || insn.words[2] != (special << 8) || - (special != 0 && special != 33 && special != 34 && special != 35 && + (special != 0 && (special < 17 || special > 19) && (special < 29 || special > 35) && special != 37 && special != 38 && special != 39 && special != 56 && special != 57 && special != 58 && special != 59 && special != 60)) return fail(index, "native special register"); @@ -1223,10 +1319,25 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) { if (condition > 6 || target == ~0U || a == ~0U || b == ~0U || insn.Bits(12, 4) != 7 || (insn.words[2] & ~0x07800000U) != 0x08000000U) return fail(index, "native uniform select mode"); - if (opcode == 0x887 && index > 0 && condition == 0 && insn.words[1] <= 0x7ffff && - code[index - 1].Bits(0, 12) == 0x88c && - code[index - 1].Bits(24, 8) == insn.Bits(24, 8) && - code[index - 1].words[1] == insn.words[1] && code[index - 1].words[2] == 0x0bf01070 && + // The clamp's UISETP may sit a few instructions back, with only instructions between + // that write no uniform state and do not branch (Nintendo Switch Sports' jump tables), + // and no branch seen so far landing between them. + u32 compare = index; + for (u32 back = 1; back <= 8 && back <= index; ++back) { + const u32 earlier = code[index - back].Bits(0, 12), datapath = earlier & 0x1ff; + if (earlier == 0x88c) { + compare = index - back; + break; + } + if ((datapath >= 0x080 && datapath <= 0x0bf) || (datapath >= 0x1c2 && datapath <= 0x1c4) || + (earlier >= 0x940 && earlier <= 0x95f)) + break; + } + const bool target_between = std::any_of(relocations.begin(), relocations.end(), + [&](const Relocation& relocation) { return relocation.target > compare && relocation.target <= index; }); + if (opcode == 0x887 && compare < index && !target_between && condition == 0 && insn.words[1] <= 0x7ffff && + code[compare].Bits(12, 4) == 7 && code[compare].Bits(24, 8) == insn.Bits(24, 8) && + code[compare].words[1] == insn.words[1] && code[compare].words[2] == 0x0bf01070 && insn.words[2] == 0x08000000) { // Preserve the unsigned clamp idiom for indirect-table analysis. emit(0x3820000000070000ULL | target | (u64{a} << 8) | @@ -1363,19 +1474,42 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) { } break; } - case 0xb6f: { // TXQ texture dimensions, two packed scalar outputs. - if (insn.Bits(59, 5) || insn.Bits(64, 8) != 255 || insn.Bits(72, 4) == 0 || insn.Bits(72, 4) > 3 || - insn.Bits(76, 14) || insn.Bits(91, 1) || insn.Bits(54, 5) != 2 || - insn.Bits(40, 14) > 0x1fff || (insn.Bits(72, 4) == 3 && (dest & 1)) || dest >= 254) + case 0xb6f: case 0x370: { // TXQ texture dimensions (0x370: TXQ.B). + // TXQ.B takes the handle in Ra and the level in Ra+1, as Maxwell's. Up to two outputs + // go to Rd, Rd+1 directly; more, or a second destination (bits 64-71) for the third and + // fourth, come out packed in scratch and are copied as for TEX. + const bool bindless = opcode == 0x370; + const u32 binding = insn.Bits(54, 5); // descriptor buffer, as for TLD + const u32 mask = insn.Bits(72, 4), components = static_cast<u32>(std::popcount(mask)); + const u32 second_dest = insn.Bits(64, 8); + const bool direct = second_dest == 255; + if (insn.Bits(59, 5) != (bindless ? 1U : 0U) || mask == 0 || (direct && components > 2) || + insn.Bits(76, 14) || insn.Bits(91, 1) || (!bindless && binding >= 18) || + (bindless && (insn.Bits(32, 27) || insn.Bits(24, 8) >= 245)) || + (!direct && (second_dest >= 245 || dest >= 245)) || + insn.Bits(40, 14) > 0x1fff || (direct && components == 2 && (dest & 1)) || dest >= 254) return fail(index, "native texture dimension query mode"); - result.required_texture_buffer = 2; - emit(0xdf48000000000000ULL | predicate | dest | (u64{insn.Bits(24, 8)} << 8) | - (u64{1} << 22) | (u64{insn.Bits(72, 4)} << 31) | (u64{insn.Bits(40, 14)} << 36) | - (u64{insn.Bits(90, 1)} << 49)); + if (!bindless) { + if (result.required_texture_buffer != ~0U && result.required_texture_buffer != binding) + return fail(index, "native texture descriptor buffer conflict"); + result.required_texture_buffer = binding; + } + emit((bindless ? 0xdf50000000000000ULL : 0xdf48000000000000ULL) | predicate | (direct ? dest : 250) | + (u64{insn.Bits(24, 8)} << 8) | (u64{1} << 22) | (u64{mask} << 31) | + (u64{bindless ? 0U : insn.Bits(40, 14)} << 36) | (u64{insn.Bits(90, 1)} << 49)); + for (u32 i = 0; !direct && i < components; ++i) { + const u32 target = i < 2 ? (dest == 255 ? 255U : static_cast<u32>(dest) + i) : second_dest + (i - 2); + if (target != 255) + emit(0x5c98000000000000ULL | predicate | target | (u64{15} << 39) | (u64{250 + i} << 20)); + } break; } - case 0xb66: { // TLD.SCR.LZ, direct texel fetch + case 0xb66: { // TLD.SCR, direct texel fetch at level zero (.LZ) or a given level (.LL) const u32 mask = insn.Bits(72, 4); + const bool level = insn.Bits(87, 3) == 3; // .LL: the level follows the coordinates + // Bit 76: a texel offset after the coordinates and level. 2D only, as Maxwell TLDS + // 2D.LZ.AOFFI (x, y in Ra, Ra+1, offsets in Rb) or 2D.LL.AOFFI (level, offsets in Rb, Rb+1). + const bool offsets = insn.Bits(76, 1) != 0; const u32 dimension = insn.Bits(61, 3); const bool array = dimension == 5; const bool volume = dimension == 2; @@ -1391,8 +1525,12 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) { for (u32 i = 0; i < rg_masks.size(); ++i) if (rg_masks[i] == mask) swizzle = i; } if (!insn.Bits(60, 1) || (dimension != 0 && dimension != 1 && !array && !volume) || - swizzle == ~0U || insn.Bits(76, 5) || - insn.Bits(81, 3) != 7 || insn.Bits(87, 3) != 1 || + swizzle == ~0U || (insn.Bits(76, 5) & ~1U) || + (offsets && (dimension != 1 || (source_a & 1) || source_a >= 254 || source_b == 255 || + (level && ((source_b & 1) || source_b >= 254)))) || + insn.Bits(81, 3) != 7 || (insn.Bits(87, 3) != 1 && !level) || (level && volume) || + (level && dimension == 1 && ((source_a & 1) || source_a >= 254)) || + (level && array && ((source_b & 1) || source_b >= 254)) || insn.Bits(59, 1) || insn.Bits(40, 14) > 0x1fff || insn.Bits(54, 5) >= 18 || dest == 255 || (mask & (mask - 1) && ((dest & 1) || dest >= 254)) || @@ -1405,6 +1543,25 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) { if (result.required_texture_buffer != ~0U && result.required_texture_buffer != binding) return fail(index, "native texture descriptor buffer conflict"); result.required_texture_buffer = binding; + if (array && level) { + // TLDS has no array form with a level: TLD takes (layer, x, y) from Ra and the level + // from Rb (layer, x in Ra, Ra+1; y, level in Rb, Rb+1 here). Results come out packed, + // then go where the Ampere destinations (Rd, Rd+1, then the second pair) take them. + for (u32 i = 0; i < 2; ++i) { + emit(0x5c98000000000000ULL | predicate | (250 + i) | (u64{15} << 39) | (u64{source_a + i} << 20)); + emit(0x5c98000000000000ULL | predicate | (252 + i) | (u64{15} << 39) | (u64{source_b + i} << 20)); + } + emit(0xdc00000000000000ULL | predicate | 246 | (u64{250} << 8) | (u64{253} << 20) | + (u64{3} << 28) | (u64{mask} << 31) | (u64{insn.Bits(40, 14)} << 36) | + (u64{insn.Bits(90, 1)} << 49) | (u64{7} << 51) | (u64{1} << 55)); + const u32 components = static_cast<u32>(std::popcount(mask)); + for (u32 i = 0; i < components; ++i) { + const u32 target = !split || i < 2 ? static_cast<u32>(dest) + i : second_dest + (i - 2); + if (target != 255 && (split || i < 2)) + emit(0x5c98000000000000ULL | predicate | target | (u64{15} << 39) | (u64{246 + i} << 20)); + } + break; + } if (array) { emit(0x5c98000000000000ULL | predicate | 250 | (u64{15} << 39) | (u64{source_a + 1} << 20)); emit(0x5c98000000000000ULL | predicate | 251 | (u64{15} << 39) | (u64{source_b} << 20)); @@ -1413,7 +1570,42 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) { emit(0xda00000000000000ULL | predicate | dest | (u64{source_a} << 8) | (u64{source_b} << 20) | (u64{insn.Bits(64, 8)} << 28) | (u64{insn.Bits(40, 14)} << 36) | (u64{insn.Bits(90, 1)} << 49) | - (u64{swizzle} << 50) | (u64{array ? 8U : volume ? 7U : dimension == 0 ? 0U : 2U} << 53)); + (u64{swizzle} << 50) | + (u64{offsets ? (level ? 12U : 4U) + : array ? 8U : volume ? 7U : dimension == 0 ? (level ? 1U : 0U) : (level ? 5U : 2U)} << 53)); + break; + } + case 0x367: { // TLD.B at level zero (.LZ), the handle in a register. + // The values are the coordinates, then the handle, split over Ra and Rb as for TEX.B + // (ARRAY_2D: layer, x | y, handle); Maxwell TLD.B takes the handle first in Rb. + constexpr std::array<u32, 8> coordinate_count{1, 2, 3, 0, 0, 3, 0, 0}; + constexpr std::array<u32, 8> maxwell_type{0, 2, 4, 0, 0, 3, 0, 0}; + const u32 mask = insn.Bits(72, 4), second_dest = insn.Bits(64, 8); + const u32 source_a = insn.Bits(24, 8), source_b = insn.Bits(32, 8); + const u32 dimension = insn.Bits(61, 3), coordinates = coordinate_count[dimension]; + const u32 count = coordinates + 1, in_a = (count + 1) / 2; + if (coordinates == 0 || !insn.Bits(59, 1) || !insn.Bits(60, 1) || insn.Bits(40, 19) || + insn.Bits(76, 5) || insn.Bits(81, 3) != 7 || insn.Bits(87, 3) != 1 || insn.Bits(91, 1) || + mask == 0 || source_a == 255 || source_a + in_a > 246 || source_b == 255 || + source_b + (count - in_a) > 246) + return fail(index, "native bindless texel fetch mode"); + const auto value = [&](u32 i) { return i < in_a ? source_a + i : source_b + (i - in_a); }; + const auto copy = [&](u32 target, u32 source) { + emit(0x5c98000000000000ULL | predicate | target | (u64{15} << 39) | (u64{source} << 20)); + }; + for (u32 i = 0; i < coordinates; ++i) copy(250 + i, value(i)); + copy(246, value(coordinates)); + emit(0xdd00000000000000ULL | predicate | 250 | (u64{250} << 8) | (u64{246} << 20) | + (u64{maxwell_type[dimension]} << 28) | (u64{mask} << 31) | (u64{insn.Bits(90, 1)} << 49) | + (u64{7} << 51)); + const u32 components = static_cast<u32>(std::popcount(mask)); + const bool split = second_dest != 255; + for (u32 i = 0; i < components; ++i) { + u32 target = 255; + if (!split || i < 2) target = dest == 255 ? 255U : static_cast<u32>(dest) + i; + else target = second_dest + (i - 2); + if (target != 255) copy(target, 250 + i); + } break; } case 0x91a: // DEPBAR.LE SB5, 1: scoreboard wait, represented by IR data dependencies. @@ -1421,22 +1613,35 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) { return fail(index, "native dependency barrier mode"); } break; - case 0xb69: { // TMML (LOD query), 2D. Results are 8.8 fixed point, as on Maxwell. + case 0xb69: case 0xf69: case 0x36a: { // TMML (LOD query), 1D/2D/3D/CUBE. Results are 8.8 fixed point, as on Maxwell. + // 0xf69: the handle in a uniform register (bit 91); 0x36a, TMML.B: the handle in Rb and + // every coordinate in Ra. Both become Maxwell TMML.B with the handle in Rb. + const bool bindless = opcode != 0xb69; + const u32 handle = opcode == 0xf69 ? uniform_register(insn.Bits(40, 6)) + : opcode == 0x36a ? insn.Bits(32, 8) : 255; const u32 mask = insn.Bits(72, 4), second_dest = insn.Bits(64, 8); const u32 source_a = insn.Bits(24, 8), source_b = insn.Bits(32, 8); - const u32 binding = insn.Bits(54, 5); - if (insn.Bits(61, 3) != 1 || insn.Bits(76, 4) || insn.Bits(80, 10) || insn.Bits(59, 1) || - insn.Bits(40, 14) > 0x1fff || binding >= 18 || mask == 0 || source_a >= 246 || - (result.required_texture_buffer != ~0U && result.required_texture_buffer != binding)) + const u32 binding = insn.Bits(54, 5), dimension = insn.Bits(61, 3); + // Coordinates: all from Ra when Rb is RZ, else split as for TEX (2D: Ra, Rb). + constexpr std::array<u32, 4> coordinate_count{1, 2, 3, 3}; + const u32 coordinates = dimension < 4 ? coordinate_count[dimension] : 0; + const u32 in_a = source_b == 255 || opcode == 0x36a ? coordinates : (coordinates + 1) / 2; + if (coordinates == 0 || insn.Bits(76, 4) || insn.Bits(80, 10) || mask == 0 || + insn.Bits(59, 1) != (opcode == 0x36a ? 1U : 0U) || source_a + in_a > 246 || + (in_a < coordinates && source_b + (coordinates - in_a) > 246) || + (opcode == 0xf69 && (handle >= 246 || insn.Bits(46, 13) || !insn.Bits(91, 1))) || + (opcode == 0x36a && (handle >= 246 || insn.Bits(40, 19) || insn.Bits(91, 1))) || + (!bindless && (insn.Bits(40, 14) > 0x1fff || binding >= 18 || + (result.required_texture_buffer != ~0U && result.required_texture_buffer != binding)))) return fail(index, "native texture LOD query mode"); - result.required_texture_buffer = binding; - // Coordinates: (Ra, Ra+1) when Rb is RZ, else (Ra, Rb). - emit(0x5c98000000000000ULL | predicate | 250 | (u64{15} << 39) | (u64{source_a} << 20)); - emit(0x5c98000000000000ULL | predicate | 251 | (u64{15} << 39) | - (u64{source_b == 255 ? source_a + 1 : source_b} << 20)); - emit(0xdf58000000000000ULL | predicate | 250 | (u64{250} << 8) | (u64{255} << 20) | - (u64{2} << 28) | (u64{mask} << 31) | (u64{insn.Bits(40, 14)} << 36) | - (u64{insn.Bits(90, 1)} << 49)); + if (!bindless) result.required_texture_buffer = binding; + for (u32 i = 0; i < coordinates; ++i) + emit(0x5c98000000000000ULL | predicate | (250 + i) | (u64{15} << 39) | + (u64{i < in_a ? source_a + i : source_b + (i - in_a)} << 20)); + if (bindless) emit(0x5c98000000000000ULL | predicate | 246 | (u64{15} << 39) | (u64{handle} << 20)); + emit((bindless ? 0xdf60000000000000ULL : 0xdf58000000000000ULL) | predicate | 250 | (u64{250} << 8) | + (u64{bindless ? 246U : 255U} << 20) | (u64{dimension * 2} << 28) | (u64{mask} << 31) | + (u64{bindless ? 0U : insn.Bits(40, 14)} << 36) | (u64{insn.Bits(90, 1)} << 49)); const u32 components = static_cast<u32>(std::popcount(mask)); const bool split = second_dest != 255; for (u32 i = 0; i < components; ++i) { @@ -1448,22 +1653,43 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) { } break; } - case 0xb63: { // TLD4 (gather), 2D without offsets or depth compare; component at bits 87-88. + case 0xb63: case 0xf63: { // TLD4 (gather), 2D without offsets or depth compare; component at bits 87-88. + // 0xf63: the handle in a uniform register (bit 91), Maxwell TLD4.B with the handle in Rb. + const bool bindless = opcode == 0xf63; + const u32 handle = bindless ? uniform_register(insn.Bits(40, 6)) : 255; const u32 mask = insn.Bits(72, 4), second_dest = insn.Bits(64, 8); const u32 source_a = insn.Bits(24, 8), source_b = insn.Bits(32, 8); const u32 binding = insn.Bits(54, 5); - if (insn.Bits(61, 3) != 1 || insn.Bits(76, 4) || insn.Bits(81, 3) != 7 || insn.Bits(59, 1) || - insn.Bits(40, 14) > 0x1fff || binding >= 18 || insn.Bits(89, 1) || mask == 0 || - (result.required_texture_buffer != ~0U && result.required_texture_buffer != binding)) + // Bit 76: a texel offset after the coordinates (x, y in Ra, Ra+1, offsets in Rb, as + // for TEX); Maxwell TLD4 takes it as AOFFI (bits 54-55) from Rb. + const bool offsets = insn.Bits(76, 1) != 0; + if (insn.Bits(61, 3) != 1 || insn.Bits(77, 3) || insn.Bits(81, 3) != 7 || insn.Bits(59, 1) || + (offsets && (bindless || source_b == 255 || (source_a != 255 && source_a >= 245))) || + insn.Bits(89, 1) || mask == 0 || + (bindless ? (handle >= 246 || insn.Bits(46, 13) || !insn.Bits(91, 1)) + : (insn.Bits(40, 14) > 0x1fff || binding >= 18 || + (result.required_texture_buffer != ~0U && result.required_texture_buffer != binding)))) return fail(index, "native texture gather mode"); - result.required_texture_buffer = binding; - // Two coordinates: x in Ra, y in Rb (the coordinate list splits evenly across them). + if (!bindless) result.required_texture_buffer = binding; + // Two coordinates: x in Ra, y in Rb (the coordinate list splits evenly across them); + // with an offset, x and y in Ra, Ra+1 and the offset in Rb. + const u32 y = !offsets ? source_b : source_a == 255 ? 255U : source_a + 1; emit(0x5c98000000000000ULL | predicate | 250 | (u64{15} << 39) | (u64{source_a} << 20)); - emit(0x5c98000000000000ULL | predicate | 251 | (u64{15} << 39) | - (u64{source_b == 255 ? 255U : source_b} << 20)); - emit(0xc800000000000000ULL | predicate | 250 | (u64{250} << 8) | (u64{255} << 20) | - (u64{2} << 28) | (u64{mask} << 31) | (u64{insn.Bits(40, 14)} << 36) | - (u64{insn.Bits(90, 1)} << 49) | (u64{7} << 51) | (u64{insn.Bits(87, 2)} << 56)); + emit(0x5c98000000000000ULL | predicate | 251 | (u64{15} << 39) | (u64{y} << 20)); + if (offsets) { + emit(0x5c98000000000000ULL | predicate | 246 | (u64{15} << 39) | (u64{source_b} << 20)); + } + if (bindless) { + emit(0x5c98000000000000ULL | predicate | 246 | (u64{15} << 39) | (u64{handle} << 20)); + emit(0xdec0000000000000ULL | predicate | 250 | (u64{250} << 8) | (u64{246} << 20) | + (u64{2} << 28) | (u64{mask} << 31) | (u64{insn.Bits(87, 2)} << 38) | + (u64{insn.Bits(90, 1)} << 49) | (u64{7} << 51)); + } else { + emit(0xc800000000000000ULL | predicate | 250 | (u64{250} << 8) | + (u64{offsets ? 246U : 255U} << 20) | (u64{2} << 28) | (u64{mask} << 31) | + (u64{insn.Bits(40, 14)} << 36) | (u64{insn.Bits(90, 1)} << 49) | (u64{7} << 51) | + (u64{offsets ? 1U : 0U} << 54) | (u64{insn.Bits(87, 2)} << 56)); + } const u32 components = static_cast<u32>(std::popcount(mask)); const bool split = second_dest != 255; for (u32 i = 0; i < components; ++i) { @@ -1475,7 +1701,16 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) { } break; } - case 0xb60: { // TEX, scalar 2D implicit-LOD sample + case 0xb60: case 0xf60: case 0x361: { // TEX; 0xf60: handle in a uniform register; 0x361: TEX.B + // Bindless forms become Maxwell TEX.B, the handle first among the meta registers (the + // texture pass tracks it back to its constant buffer words). 0x361 lists the handle + // right after the coordinates, then the LOD (see general_tex); forms with offsets, + // whose place next to the handle is unverified, are not taken. + const bool bindless = opcode != 0xb60; + const u32 handle = opcode == 0xf60 ? uniform_register(insn.Bits(40, 6)) : 255; + if (bindless && ((opcode == 0xf60 && (handle == ~0U || handle >= 246 || insn.Bits(46, 13))) || + (opcode == 0x361 && (insn.Bits(40, 19) || insn.Bits(87, 3) > 3 || insn.Bits(76, 1))))) + return fail(index, "native bindless texture mode"); const u32 mask = insn.Bits(72, 4); const u32 second_dest = insn.Bits(64, 8); constexpr std::array<u32, 8> rg_masks{1, 2, 4, 8, 3, 9, 10, 12}; @@ -1485,7 +1720,7 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) { const bool depth_compare = insn.Bits(78, 1); const u32 dimension = insn.Bits(61, 3), lod = insn.Bits(87, 3); const u32 source_a = insn.Bits(24, 8), source_b = insn.Bits(32, 8); - if (dimension == 5 && depth_compare && lod == 0 && !split && mask == 1) { + if (!bindless && dimension == 5 && depth_compare && lod == 0 && !split && mask == 1) { if (!insn.Bits(60, 1) || insn.Bits(76, 2) || insn.Bits(81, 3) != 7 || insn.Bits(59, 1) || insn.Bits(40, 14) > 0x1fff || insn.Bits(54, 5) != 2 || (source_a & 1) || (source_b & 1) || source_a >= 248 || source_b >= 248 || dest >= 248) @@ -1510,14 +1745,21 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) { constexpr std::array<u32, 8> coordinate_count{1, 2, 3, 3, 2, 3, 0, 4}; constexpr std::array<u32, 8> maxwell_type{0, 2, 4, 6, 1, 3, 0, 7}; const bool offsets = insn.Bits(76, 1) != 0; - if (dimension == 6 || insn.Bits(77, 1) || insn.Bits(81, 3) != 7 || insn.Bits(59, 1) || - insn.Bits(40, 14) > 0x1fff || insn.Bits(54, 5) >= 18 || (lod > 3 && lod != 6 && lod != 7) || - (depth_compare && mask != 1) || mask == 0) + // Bit 59 is .B, set in 0x361's encoding. + if (dimension == 6 || insn.Bits(77, 1) || insn.Bits(81, 3) != 7 || + insn.Bits(59, 1) != (opcode == 0x361 ? 1U : 0U) || + (!bindless && (insn.Bits(40, 14) > 0x1fff || insn.Bits(54, 5) >= 18)) || + (lod > 3 && lod != 6 && lod != 7) || (depth_compare && mask != 1) || mask == 0) return false; const u32 coordinates = coordinate_count[dimension]; const bool has_lod = lod == 2 || lod == 3 || lod == 6 || lod == 7; - const u32 count = coordinates + (has_lod ? 1 : 0) + (offsets ? 1 : 0) + (depth_compare ? 1 : 0); - const u32 in_a = count <= 4 ? (count + 1) / 2 : 4; + // 0x361's handle is one more value after the coordinates: 2D.DC is (x, y | handle, + // reference), ARRAY_2D is (layer, x | y, handle); with five or more values the + // coordinates keep Ra (CUBE.LL is (x, y, z | handle, LOD)). + const u32 handle_slot = opcode == 0x361 ? 1 : 0; + const u32 count = coordinates + handle_slot + (has_lod ? 1 : 0) + (offsets ? 1 : 0) + + (depth_compare ? 1 : 0); + const u32 in_a = count <= 4 ? (count + 1) / 2 : opcode == 0x361 ? coordinates : 4; const auto value = [&](u32 i) -> u32 { if (i < in_a) return source_a == 255 ? 255U : source_a + i; return source_b == 255 ? 255U : source_b + (i - in_a); @@ -1525,25 +1767,38 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) { if ((source_a != 255 && source_a + in_a > 246) || (count > in_a && source_b != 255 && source_b + (count - in_a) > 246)) return false; - const u32 binding = insn.Bits(54, 5); - if (result.required_texture_buffer != ~0U && result.required_texture_buffer != binding) - return false; - result.required_texture_buffer = binding; + const u32 texture_handle = opcode == 0x361 ? value(coordinates) : handle; + if (bindless && texture_handle >= 246) return false; + if (!bindless) { + const u32 binding = insn.Bits(54, 5); + if (result.required_texture_buffer != ~0U && result.required_texture_buffer != binding) + return false; + result.required_texture_buffer = binding; + } const auto copy = [&](u32 target, u32 source) { emit(0x5c98000000000000ULL | predicate | target | (u64{15} << 39) | (u64{source} << 20)); }; // Coordinates to R250.., then LOD/bias, offsets and reference to R246... for (u32 i = 0; i < coordinates; ++i) copy(250 + i, value(i)); u32 meta = 246; - u32 next = coordinates; + u32 next = coordinates + handle_slot; + if (bindless) copy(meta++, texture_handle); if (has_lod) copy(meta++, value(next++)); if (offsets) copy(meta++, value(next++)); if (depth_compare) copy(meta++, value(next++)); + if (bindless) { + // Maxwell TEX.B: AOFFI at 36 and the LOD mode at 37 instead of 54 and 55. + emit(0xde80000000000000ULL | predicate | 250 | (u64{250} << 8) | (u64{246} << 20) | + (u64{maxwell_type[dimension]} << 28) | (u64{mask} << 31) | (u64{offsets} << 36) | + (u64{lod} << 37) | (u64{insn.Bits(90, 1)} << 49) | (u64{depth_compare} << 50) | + (u64{7} << 51)); + } else { emit(0xc000000000000000ULL | predicate | 250 | (u64{250} << 8) | (u64{meta == 246 ? 255U : 246U} << 20) | (u64{maxwell_type[dimension]} << 28) | (u64{mask} << 31) | (u64{insn.Bits(40, 14)} << 36) | (u64{insn.Bits(90, 1)} << 49) | (u64{depth_compare} << 50) | (u64{7} << 51) | (u64{offsets} << 54) | (u64{lod} << 55)); + } const u32 components = static_cast<u32>(std::popcount(mask)); if (insn.Bits(79, 1)) { // FP16 results: (c0, c1) packed into the first destination, (c2, c3) into @@ -1578,7 +1833,7 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) { // SM70 scalar coordinates and split RG/BA destinations match TEXS. // Descriptor offsets are words in both encodings, but only native // explicitly encodes the constant-buffer binding. - if (!insn.Bits(60, 1) || (dimension != 1 && dimension != 5) || + if (bindless || !insn.Bits(60, 1) || (dimension != 1 && dimension != 5) || swizzle == ~0U || insn.Bits(76, 2) || insn.Bits(81, 3) != 7 || (lod > 1 && (lod != 3 || dimension != 1 || depth_compare)) || (lod == 3 && ((source_a & 1) || source_a >= 249)) || @@ -1631,11 +1886,12 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) { } break; } - case 0x307: { // FRND.F32, round to an integral floating-point value + case 0x307: case 0xb07: { // FRND.F32, round to an integral floating-point value (0xb07: cbuf) if (insn.Bits(75, 2) != 2 || insn.Bits(84, 2) != 2 || insn.Bits(81, 3) || insn.Bits(86, 6)) return fail(index, "native floating round format"); - emit(base(0x5ca8000000000000ULL) | (u64{2} << 8) | (u64{2} << 10) | - (u64{insn.Bits(32, 8)} << 20) | (u64{8 + insn.Bits(78, 2)} << 39) | + if (opcode == 0xb07 && !valid_cbuf()) return fail(index, "native constant buffer address"); + emit(base(opcode == 0xb07 ? 0x4ca8000000000000ULL : 0x5ca8000000000000ULL) | (u64{2} << 8) | (u64{2} << 10) | + (opcode == 0xb07 ? cbuf() : u64{insn.Bits(32, 8)} << 20) | (u64{8 + insn.Bits(78, 2)} << 39) | (u64{insn.Bits(80, 1)} << 44) | (u64{insn.Bits(63, 1)} << 45) | (u64{insn.Bits(62, 1)} << 49)); break; @@ -1666,15 +1922,22 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) { result.instructions.push_back(translated); break; } - case 0x387: case 0x983: { // Captured scalar local spill at an immediate byte offset. + case 0x387: case 0x983: { // Local spill: 32, 64 or 128 bits at [Ra + immediate byte offset]. + // The size is bits 73-75 (4, 5, 6 = 32, 64, 128 bits); wider accesses become 32-bit ones. + // A load's bit 85 (.LU) is a cache hint. const bool store = opcode == 0x387; - const u32 data = insn.Bits(32, 8), offset = insn.Bits(40, 24); - if (insn.Bits(24, 8) != 255 || (store && (insn.Bits(16, 8) || data > 247)) || - (!store && (data || dest >= 248)) || (offset & 3) || offset > 0x7fffff || - insn.words[2] != (store ? 0x00100800U : 0x00300800U)) + const u32 size = insn.Bits(73, 3); + const u32 data = insn.Bits(32, 8), offset = insn.Bits(40, 24), address = insn.Bits(24, 8); + const u32 words = size == 6 ? 4 : size == 5 ? 2 : 1, value = store ? data : static_cast<u32>(dest); + if ((address != 255 && address >= 246) || (store && insn.Bits(16, 8)) || (!store && data) || + (value != 255 && (value + words > 248 || (value & (words - 1)))) || (!store && value == 255) || + (offset & 3) || offset + 4 * words > 0x800000 || size < 4 || size > 6 || + (insn.words[2] & ~(store ? 0xe00U : 0x200e00U)) != 0x00100000U) return fail(index, "native local scalar spill mode"); - emit((store ? 0xef50000000000000ULL : 0xef40000000000000ULL) | predicate | - (store ? data : dest) | (u64{255} << 8) | (u64{offset} << 20) | (u64{4} << 48)); + for (u32 word = 0; word < words; ++word) + emit((store ? 0xef50000000000000ULL : 0xef40000000000000ULL) | predicate | + (value == 255 ? 255U : value + word) | (u64{address} << 8) | + (u64{offset + 4 * word} << 20) | (u64{4} << 48)); break; } case 0x803: { // P2R PR, register merge source, immediate mask, byte select (76-77) @@ -1747,9 +2010,12 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) { (u64{insn.Bits(80, 1)} << 44)); break; } - case 0x308: case 0xb08: { // MUFU + case 0x308: case 0xb08: case 0xd08: { // MUFU (register, constant or uniform register source) const u32 operation = insn.Bits(74, 6); - if (operation > 8 || operation == 6 || operation == 7 || insn.Bits(80, 12)) return fail(index, "native MUFU mode"); + // Bit 91 is part of the uniform-register form's encoding. + if (operation > 8 || operation == 6 || operation == 7 || + insn.Bits(80, 12) != (opcode == 0xd08 ? 0x800U : 0U)) + return fail(index, "native MUFU mode"); // SM70+ MUFU.COS/SIN take the angle in turns (compilers multiply by 1/2pi first); // Eden's Maxwell MUFU takes radians (its RRO is a plain move), so scale by 2pi. const bool turns = operation == 0 || operation == 1; @@ -1768,7 +2034,8 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) { emit(0x5080000000000000ULL | predicate | 250 | (u64{250} << 8) | (u64{operation} << 20)); emit(base(0x5ca8000000000000ULL) | (u64{1} << 8) | (u64{2} << 10) | (u64{250} << 20)); } else { - u32 src = operand(1); + u32 src = opcode == 0xd08 ? uniform_register(insn.Bits(32, 8)) : operand(1); + if (src == ~0U) return fail(index, "native MUFU uniform source"); if (turns) src = to_radians(src); emit(base(0x5080000000000000ULL) | (u64{src} << 8) | (u64{operation} << 20)); } @@ -1838,7 +2105,7 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) { mov_imm(static_cast<u32>(dest) + 1, 0); break; } - case 0x306: case 0xd06: { // I2F.F32.U8, low byte, round-to-nearest (0xd06: uniform source) + case 0x306: case 0xd06: { // I2F.F32.U8, round-to-nearest (0xd06: uniform source) const u32 source = opcode == 0xd06 ? uniform_register(insn.Bits(32, 8)) : insn.Bits(32, 8); if (opcode == 0x306 && insn.words[2] == 0x00101000 && !(insn.words[1] & 0xcfffff00U)) { // I2F.U16 Rd, Rs(.H1): Maxwell's 16-bit source format, byte selector 0 or 2. @@ -1846,10 +2113,12 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) { (u64{insn.Bits(60, 2) * 2} << 41)); break; } + // I2F.U8 Rd, Rs.B0-B3: the byte selector (bits 60-61) as Maxwell's (41-42). if (insn.words[2] != (opcode == 0xd06 ? 0x08001000U : 0x1000U) || - (insn.words[1] & 0xffffff00U) || source == ~0U) + (insn.words[1] & (opcode == 0xd06 ? 0xffffff00U : 0xcfffff00U)) || source == ~0U) return fail(index, "native byte-to-float conversion mode"); - emit(base(0x5cb8000000000000ULL) | (u64{2} << 8) | (u64{source} << 20)); + emit(base(0x5cb8000000000000ULL) | (u64{2} << 8) | (u64{source} << 20) | + (u64{insn.Bits(60, 2)} << 41)); break; } case 0x245: case 0xa45: case 0xc45: { // I2FP.F32.U32 @@ -1887,10 +2156,41 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) { (u64{insn.Bits(74, 2)} << 47) | (u64{insn.Bits(71, 1)} << 49)); break; } - case 0x20b: case 0x80b: case 0xa0b: // FSETP + case 0x20b: case 0x80b: case 0xa0b: case 0xc0b: // FSETP case 0x20c: case 0x80c: case 0xa0c: case 0xc0c: { // ISETP const bool floating = (opcode & 0x1ff) == 0xb; - if (!floating && insn.Bits(72, 1)) return fail(index, "native extended integer comparison"); + if (!floating && insn.Bits(72, 1)) { + // ISETP.EX (bit 72): the high-word half of a 64-bit compare. The low words' + // result comes in as the predicate at bits 68-70 (negated by 71), e.g. + // ISETP.NE P0 = lo != 0, then ISETP.NE.EX P0 = (hi != 0) OR P0. EQ: high equal + // AND low; NE: high different OR low; the orderings: high strictly ordered OR + // (high equal AND low). One or two Maxwell ISETPs into the destination; only + // the plain form (no second destination, AND with PT) is handled. + const u32 compare = insn.Bits(76, 3); + const u32 destination = insn.Bits(81, 3); + const u32 low = insn.Bits(68, 4); + if (insn.Bits(84, 3) != 7 || insn.Bits(87, 4) != 7 || insn.Bits(74, 2) != 0 || + compare == 0 || compare == 7 || + (destination != 7 && insn.Bits(12, 3) == destination)) + return fail(index, "native extended integer comparison mode"); + const u32 a = operand(0, false), b = operand(1, false); + const auto isetp = [&](u32 op, u32 input, u32 combine) { + return 0x5b60000000000000ULL | predicate | 7 | (u64{destination} << 3) | + (u64{a} << 8) | (u64{b} << 20) | (u64{input & 7} << 39) | + (u64{input >> 3} << 42) | (u64{combine} << 45) | + (u64{insn.Bits(73, 1)} << 48) | (u64{op} << 49); + }; + constexpr u32 And = 0, Or = 1, Lt = 1, Eq = 2, Gt = 4, Ne = 5; + if (compare == Eq) { + emit(isetp(Eq, low, And)); + } else if (compare == Ne) { + emit(isetp(Ne, low, Or)); + } else { + emit(isetp(Eq, low, And)); + emit(isetp(compare <= 3 ? Lt : Gt, destination, Or)); + } + break; + } const u32 a = operand(0, floating), b = operand(1, floating); const u32 combine = insn.Bits(74, 2); if (combine > 2) return fail(index, "native predicate combination"); @@ -1986,7 +2286,7 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) { (dest << 8) | (u64{b} << 20) | (u64{7} << 39) | (u64{1} << 49)); break; } - case 0x211: case 0x811: { // LEA low word, no carry result + case 0x211: case 0x811: case 0xc11: { // LEA low word (0xc11: uniform Rb), no carry result if (insn.Bits(80, 1)) { // LEA.HI Rd, Ra, Rb (or an immediate, 0x811), RZ, s: Rb + high word of (Ra << s); // bit 73 sign-extends Ra. @@ -2007,11 +2307,32 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) { } break; } - if (insn.Bits(72, 1) || insn.Bits(74, 1) || insn.Bits(80, 1) || insn.Bits(81, 3) != 7 || - (opcode == 0x211 && insn.Bits(62, 1))) return fail(index, "native LEA carry/high mode"); + // A carry out (bits 81-83) is the low half of a 64-bit address; IADD3.X adds it next. + const u32 carry_out = insn.Bits(81, 3); + if (insn.Bits(72, 1) || insn.Bits(74, 1) || insn.Bits(80, 1) || + (carry_out != 7 && (opcode != 0x211 || insn.Bits(63, 1) || (rewritten_uniform && carry_out > 1))) || + (opcode == 0x211 && insn.Bits(62, 1)) || + (opcode == 0xc11 && (insn.Bits(62, 2) || uniform_register(insn.Bits(32, 8)) == ~0U))) + return fail(index, "native LEA carry/high mode"); // 0x211 bit 63 negates Rb (LEA Rd, Ra, -Rb, s), as Maxwell ISCADD's bit 48. - u32 b = insn.Bits(32, 8); + u32 b = opcode == 0xc11 ? uniform_register(insn.Bits(32, 8)) : insn.Bits(32, 8); if (opcode == 0x811) { mov_imm(251, insn.words[1]); b = 251; } + if (carry_out != 7) { + // The sum in R250, carry = sum < Rb (unsigned), as IADD3's carry out; a uniform + // predicate goes through P(6-n) into R239. + const u32 target = rewritten_uniform ? 6 - carry_out : carry_out, mask = 1U << target; + emit(0x5c18000000000000ULL | predicate | 250 | (u64{insn.Bits(24, 8)} << 8) | + (u64{b} << 20) | (u64{insn.Bits(75, 5)} << 39)); + if (rewritten_uniform) emit(0x38e8000000070000ULL | 253 | (u64{255} << 8) | (u64{mask} << 20)); + emit(0x5b60000000000000ULL | predicate | 7 | (u64{target} << 3) | (u64{250} << 8) | + (u64{b} << 20) | (u64{7} << 39) | (u64{1} << 49)); + if (rewritten_uniform) { + emit(0x38e8000000070000ULL | 239 | (u64{239} << 8) | (u64{mask} << 20)); + emit(0x38f0000000070000ULL | (u64{253} << 8) | (u64{mask} << 20)); + } + emit(base(0x5c98000000000000ULL) | (u64{15} << 39) | (u64{250} << 20)); + break; + } emit(base(0x5c18000000000000ULL) | (u64{insn.Bits(24, 8)} << 8) | (u64{b} << 20) | (u64{insn.Bits(75, 5)} << 39) | (u64{opcode == 0x211 ? insn.Bits(63, 1) : 0U} << 48)); @@ -2076,12 +2397,13 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) { break; } if (opcode == 0xa10 && insn.Bits(74, 1)) { - if (insn.Bits(24, 8) != 255 || (insn.words[2] & ~0x07800000U) != 0x007fe4ffU) + if ((insn.words[2] & ~0x07800000U) != 0x007fe4ffU) return fail(index, "native extended constant add mode"); - const u32 b = operand(1, false); + const u32 a = insn.Bits(24, 8), b = operand(1, false); mov_imm(250, 1); emit(0x5ca0000000000000ULL | predicate | 250 | (u64{250} << 8) | (u64{255} << 20) | (u64{insn.Bits(87, 3)} << 39) | (u64{insn.Bits(90, 1)} << 42)); + if (a != 255) emit(0x5c10000000000000ULL | predicate | 250 | (u64{a} << 8) | (u64{250} << 20)); emit(base(0x5c10000000000000ULL) | (u64{b} << 8) | (u64{250} << 20)); break; } @@ -2214,6 +2536,24 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) { return finish(index); // the closing self-branch break; } + case 0x923: case 0x927: { // ISBERD / ISBEWR: task and mesh programs' stage buffer. + // Ampere: Ra 24-31, offset 40-63, ISBERD dest 16-23, ISBEWR data 32-39; 75 = .32 (else + // U8), 76-77 = 3 .ATTR (0 the map region), 78 = .SKEW, 79 = .O (the output buffer). + // Kept as an nx2 pseudo instruction (Maxwell 0xefc0, free there) that the frontend + // reads for the task or mesh stage: data/dest 0-7, Ra 8-15, offset 20-43, .32 44, + // .ATTR 45, .SKEW 46, .O 47, write 48. Addresses are linear bytes in the region. + const bool write = opcode == 0x927; + const u32 region = insn.Bits(76, 2); + const bool output = insn.Bits(79, 1) != 0; + if ((insn.words[2] & ~0xf800U) != 0 || (region != 0 && region != 3) || write != output || + (!write && region != 3) || (!write && dest == 255)) + return fail(index, "native stage buffer access mode"); + emit(0xefc0000000000000ULL | predicate | (write ? u64{insn.Bits(32, 8)} : dest) | + (u64{insn.Bits(24, 8)} << 8) | (u64{insn.Bits(40, 24)} << 20) | + (u64{insn.Bits(75, 1)} << 44) | (u64{region == 3} << 45) | + (u64{insn.Bits(78, 1)} << 46) | (u64{output} << 47) | (u64{write} << 48)); + break; + } case 0x94d: { // EXIT if (insn.Bits(84, 2)) { return fail(index, "native conditional/special EXIT"); @@ -2264,4 +2604,14 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) { return fail(static_cast<u32>(code.size()), main_exited ? "native program lacks captured library return" : "native program has no unconditional EXIT"); } +inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) { + // Programs using uniform registers above UR27 are lowered again with mirrors for them. + u32 high_uniforms = 0; + for (;;) { + LoweredProgram lowered = LowerToMaxwellWith(code, high_uniforms); + if (lowered || lowered.high_uniform_registers <= high_uniforms) return lowered; + high_uniforms = lowered.high_uniform_registers; + } +} + } // namespace Shader::Ampere diff --git a/src/shader_recompiler/frontend/ir/ir_emitter.cpp b/src/shader_recompiler/frontend/ir/ir_emitter.cpp index 95815d7d51..23d1db5bf4 100644 --- a/src/shader_recompiler/frontend/ir/ir_emitter.cpp +++ b/src/shader_recompiler/frontend/ir/ir_emitter.cpp @@ -329,6 +329,22 @@ void IREmitter::SetPatch(Patch patch, const F32& value) { Inst(Opcode::SetPatch, patch, value); } +U32 IREmitter::IsbeRead(const U32& address) { + return Inst<U32>(Opcode::IsbeRead, address); +} + +void IREmitter::IsbeWriteAttribute(const U32& address, const U32& value) { + Inst(Opcode::IsbeWriteAttribute, address, value); +} + +void IREmitter::IsbeWriteMapU8(const U32& address, const U32& value) { + Inst(Opcode::IsbeWriteMapU8, address, value); +} + +void IREmitter::IsbeWriteMapU32(const U32& address, const U32& value) { + Inst(Opcode::IsbeWriteMapU32, address, value); +} + void IREmitter::SetFragColor(u32 index, u32 component, const F32& value) { Inst(Opcode::SetFragColor, Imm32(index), Imm32(component), value); } diff --git a/src/shader_recompiler/frontend/ir/ir_emitter.h b/src/shader_recompiler/frontend/ir/ir_emitter.h index f22f0f75e2..d0cd88ca3c 100644 --- a/src/shader_recompiler/frontend/ir/ir_emitter.h +++ b/src/shader_recompiler/frontend/ir/ir_emitter.h @@ -88,6 +88,11 @@ public: [[nodiscard]] F32 GetPatch(Patch patch); void SetPatch(Patch patch, const F32& value); + [[nodiscard]] U32 IsbeRead(const U32& address); + void IsbeWriteAttribute(const U32& address, const U32& value); + void IsbeWriteMapU8(const U32& address, const U32& value); + void IsbeWriteMapU32(const U32& address, const U32& value); + void SetFragColor(u32 index, u32 component, const F32& value); void SetSampleMask(const U32& value); void SetFragDepth(const F32& value); diff --git a/src/shader_recompiler/frontend/ir/microinstruction.cpp b/src/shader_recompiler/frontend/ir/microinstruction.cpp index 84417980bc..a579ed1057 100644 --- a/src/shader_recompiler/frontend/ir/microinstruction.cpp +++ b/src/shader_recompiler/frontend/ir/microinstruction.cpp @@ -82,6 +82,9 @@ bool Inst::MayHaveSideEffects() const noexcept { case Opcode::SetAttribute: case Opcode::SetAttributeIndexed: case Opcode::SetPatch: + case Opcode::IsbeWriteAttribute: + case Opcode::IsbeWriteMapU8: + case Opcode::IsbeWriteMapU32: case Opcode::SetFragColor: case Opcode::SetSampleMask: case Opcode::SetFragDepth: diff --git a/src/shader_recompiler/frontend/ir/opcodes.cpp b/src/shader_recompiler/frontend/ir/opcodes.cpp index 94ba0f6ae0..1999f877fe 100644 --- a/src/shader_recompiler/frontend/ir/opcodes.cpp +++ b/src/shader_recompiler/frontend/ir/opcodes.cpp @@ -10,7 +10,7 @@ namespace Shader::IR { namespace Detail { -OpcodeMeta META_TABLE[534] = { +OpcodeMeta META_TABLE[538] = { #define OPCODE(name_token, type_token, ...) \ { \ .name{#name_token}, \ @@ -21,7 +21,7 @@ OpcodeMeta META_TABLE[534] = { #undef OPCODE }; -u8 NUM_ARGS[534] = { +u8 NUM_ARGS[538] = { #define OPCODE(name_token, type_token, ...) u8(CalculateNumArgsOf(Opcode::name_token)), #include "opcodes.inc" #undef OPCODE diff --git a/src/shader_recompiler/frontend/ir/opcodes.h b/src/shader_recompiler/frontend/ir/opcodes.h index 50f9d7b155..c59f50adfe 100644 --- a/src/shader_recompiler/frontend/ir/opcodes.h +++ b/src/shader_recompiler/frontend/ir/opcodes.h @@ -57,12 +57,12 @@ static constexpr Type F64x2{Type::F64x2}; static constexpr Type F64x3{Type::F64x3}; static constexpr Type F64x4{Type::F64x4}; -extern OpcodeMeta META_TABLE[534]; +extern OpcodeMeta META_TABLE[538]; constexpr size_t CalculateNumArgsOf(Opcode op) noexcept { const auto& arg_types = META_TABLE[size_t(op)].arg_types; return size_t(std::distance(arg_types.begin(), std::ranges::find(arg_types, Type::Void))); } -extern u8 NUM_ARGS[534]; +extern u8 NUM_ARGS[538]; } // namespace Detail /// Get return type of an opcode diff --git a/src/shader_recompiler/frontend/ir/opcodes.inc b/src/shader_recompiler/frontend/ir/opcodes.inc index fee0afaf33..552ec4fbb6 100644 --- a/src/shader_recompiler/frontend/ir/opcodes.inc +++ b/src/shader_recompiler/frontend/ir/opcodes.inc @@ -48,6 +48,10 @@ OPCODE(GetAttributeIndexed, F32, U32, OPCODE(SetAttributeIndexed, Void, U32, F32, U32, ) OPCODE(GetPatch, F32, Patch, ) OPCODE(SetPatch, Void, Patch, F32, ) +OPCODE(IsbeRead, U32, U32, ) +OPCODE(IsbeWriteAttribute, Void, U32, U32, ) +OPCODE(IsbeWriteMapU8, Void, U32, U32, ) +OPCODE(IsbeWriteMapU32, Void, U32, U32, ) OPCODE(SetFragColor, Void, U32, U32, F32, ) OPCODE(SetSampleMask, Void, U32, ) OPCODE(SetFragDepth, Void, F32, ) diff --git a/src/shader_recompiler/frontend/ir/program.h b/src/shader_recompiler/frontend/ir/program.h index 6b4a05c598..fee5cee721 100644 --- a/src/shader_recompiler/frontend/ir/program.h +++ b/src/shader_recompiler/frontend/ir/program.h @@ -27,6 +27,7 @@ struct Program { u32 local_memory_size{}; u32 shared_memory_size{}; bool is_geometry_passthrough{}; + MeshInfo mesh{}; }; [[nodiscard]] std::string DumpProgram(const Program& program); diff --git a/src/shader_recompiler/frontend/maxwell/maxwell.inc b/src/shader_recompiler/frontend/maxwell/maxwell.inc index e5dc9c8005..5facfb4803 100644 --- a/src/shader_recompiler/frontend/maxwell/maxwell.inc +++ b/src/shader_recompiler/frontend/maxwell/maxwell.inc @@ -55,6 +55,7 @@ INST(IMNMX_reg, "IMNMX (reg)", "0101 1100 0010 0---") INST(IMUL_cbuf, "IMUL (cbuf)", "0100 1100 0011 1---") INST(IMUL_reg, "IMUL (reg)", "0101 1100 0011 1---") INST(ISBERD, "ISBERD", "1110 1111 1101 0---") +INST(ISBE_nx2, "ISBE (nx2)", "1110 1111 1100 0---") INST(ISCADD_cbuf, "ISCADD (cbuf)", "0100 1100 0001 1---") INST(ISCADD_reg, "ISCADD (reg)", "0101 1100 0001 1---") INST(LDC, "LDC", "1110 1111 1001 0---") diff --git a/src/shader_recompiler/frontend/maxwell/structured_control_flow.cpp b/src/shader_recompiler/frontend/maxwell/structured_control_flow.cpp index 329d44443e..50cff52f88 100644 --- a/src/shader_recompiler/frontend/maxwell/structured_control_flow.cpp +++ b/src/shader_recompiler/frontend/maxwell/structured_control_flow.cpp @@ -314,7 +314,8 @@ bool NeedsLift(Node goto_stmt, Node label_stmt) noexcept { class GotoPass { public: - explicit GotoPass(Flow::CFG& cfg, ObjectPool<Statement>& stmt_pool) : pool{stmt_pool} { + explicit GotoPass(Flow::CFG& cfg, ObjectPool<Statement>& stmt_pool, bool converge_exits_) + : pool{stmt_pool}, converge_exits{converge_exits_} { std::vector gotos{BuildTree(cfg)}; const auto end{gotos.rend()}; for (auto goto_stmt = gotos.rbegin(); goto_stmt != end; ++goto_stmt) { @@ -381,7 +382,17 @@ private: u32 label_id{0}; std::vector<Node> gotos; Flow::Function& first_function{cfg.Functions().front()}; - BuildTree(cfg, first_function, label_id, gotos, root_stmt.children.end(), std::nullopt); + Node insert_point{root_stmt.children.end()}; + if (converge_exits) { + // Every EXIT jumps to one return at the end, so all invocations reach the same + // epilogue (task and mesh programs emit their outputs there, after a barrier). + Tree& root{root_stmt.children}; + exit_label = root.insert(root.end(), *pool.Create(Label{}, label_id, &root_stmt)); + ++label_id; + root.insert(root.end(), *pool.Create(Return{}, &root_stmt)); + insert_point = *exit_label; + } + BuildTree(cfg, first_function, label_id, gotos, insert_point, std::nullopt); return gotos; } @@ -454,7 +465,14 @@ private: break; } case Flow::EndClass::Exit: - root.insert(ip, *pool.Create(Return{}, &root_stmt)); + if (exit_label) { + Statement* const always_cond{ + pool.Create(Identity{}, IR::Condition{true}, &root_stmt)}; + gotos.push_back(root.insert( + ip, *pool.Create(Goto{}, always_cond, *exit_label, &root_stmt))); + } else { + root.insert(ip, *pool.Create(Return{}, &root_stmt)); + } break; case Flow::EndClass::Return: { Statement* const always_cond{pool.Create(Identity{}, block.cond, &root_stmt)}; @@ -618,6 +636,8 @@ private: ObjectPool<Statement>& pool; Statement root_stmt{FunctionTag{}}; + bool converge_exits{}; + std::optional<Node> exit_label; }; [[nodiscard]] Statement* TryFindForwardBlock(Statement& stmt) { @@ -987,7 +1007,8 @@ IR::AbstractSyntaxList BuildASL(ObjectPool<IR::Inst>& inst_pool, ObjectPool<IR:: Environment& env, Flow::CFG& cfg, const HostTranslateInfo& host_info) { ObjectPool<Statement> stmt_pool{64}; - GotoPass goto_pass{cfg, stmt_pool}; + const Stage stage{env.ShaderStage()}; + GotoPass goto_pass{cfg, stmt_pool, stage == Stage::Task || stage == Stage::Mesh}; Statement& root{goto_pass.RootStatement()}; IR::AbstractSyntaxList syntax_list; TranslatePass{inst_pool, block_pool, stmt_pool, env, root, syntax_list, host_info}; diff --git a/src/shader_recompiler/frontend/maxwell/translate/impl/internal_stage_buffer_entry_read.cpp b/src/shader_recompiler/frontend/maxwell/translate/impl/internal_stage_buffer_entry_read.cpp index 46660563a1..27de2a0171 100644 --- a/src/shader_recompiler/frontend/maxwell/translate/impl/internal_stage_buffer_entry_read.cpp +++ b/src/shader_recompiler/frontend/maxwell/translate/impl/internal_stage_buffer_entry_read.cpp @@ -125,4 +125,40 @@ void TranslatorVisitor::ISBERD(u64 insn) { X(isberd.dest_reg.Value(), X(isberd.src_reg.Value())); } +// nx2 pseudo instruction for the ISBERD/ISBEWR of Switch 2 task and mesh programs (see +// frontend/ampere/lower_to_maxwell.h). Addresses are linear bytes in the stage buffer: reads come +// from the input attribute region, writes go to the output attribute or map region. +void TranslatorVisitor::ISBE_nx2(u64 insn) { + union { + u64 raw; + BitField<0, 8, IR::Reg> reg; + BitField<8, 8, IR::Reg> address_reg; + BitField<20, 24, u64> offset; + BitField<44, 1, u64> is_32; + BitField<45, 1, u64> attribute; + BitField<48, 1, u64> write; + } const isbe{insn}; + + const Stage stage{env.ShaderStage()}; + if (stage != Stage::Task && stage != Stage::Mesh) { + throw NotImplementedException("Stage buffer access outside task and mesh programs"); + } + const IR::U32 address{ir.IAdd(X(isbe.address_reg), ir.Imm32(static_cast<u32>(isbe.offset)))}; + if (isbe.write == 0) { + X(isbe.reg, ir.IsbeRead(address)); + return; + } + const IR::U32 value{X(isbe.reg)}; + if (isbe.attribute != 0) { + if (isbe.is_32 == 0) { + throw NotImplementedException("U8 stage buffer attribute write"); + } + ir.IsbeWriteAttribute(address, value); + } else if (isbe.is_32 != 0) { + ir.IsbeWriteMapU32(address, value); + } else { + ir.IsbeWriteMapU8(address, value); + } +} + } // namespace Shader::Maxwell diff --git a/src/shader_recompiler/frontend/maxwell/translate_program.cpp b/src/shader_recompiler/frontend/maxwell/translate_program.cpp index cd4678cef6..fe6d8d8913 100644 --- a/src/shader_recompiler/frontend/maxwell/translate_program.cpp +++ b/src/shader_recompiler/frontend/maxwell/translate_program.cpp @@ -101,10 +101,12 @@ void AddNVNStorageBuffers(IR::Program& program) { switch (program.stage) { case Stage::VertexA: case Stage::VertexB: + case Stage::Task: // bound in the vertex slot return 0x110u; case Stage::TessellationControl: return 0x210u; case Stage::TessellationEval: + case Stage::Mesh: // bound in the tessellation slot return 0x310u; case Stage::Geometry: return 0x410u; @@ -304,6 +306,30 @@ void LowerGeometryPassthrough(const IR::Program& program, const HostTranslateInf } } +// The lines of a 32-element group in a mesh program's stage buffer: one per attribute an output +// map enables, in attribute order (system values, then generics). +void AddMeshLines(const ProgramHeader& sph, boost::container::static_vector<u32, 136>& lines) { + const auto& c{sph.vtg.omap_systemc}; + if (c.clip_distances != 0 || c.point_sprite_s != 0 || c.point_sprite_t != 0 || + c.fog_coordinate != 0 || c.tessellation_eval_point_u != 0 || + c.tessellation_eval_point_v != 0 || c.instance_id != 0 || c.vertex_id != 0) { + throw NotImplementedException("Mesh outputs past the generic attributes"); + } + for (u32 bit = 0; bit < 8; ++bit) { + if (((sph.vtg.omap_systemb.raw >> bit) & 1) != 0) { + lines.push_back(static_cast<u32>(IR::Attribute::PrimitiveId) + bit); + } + } + for (u32 generic = 0; generic < 32; ++generic) { + const std::array<bool, 4> mask{sph.vtg.OutputGeneric(generic)}; + for (u32 element = 0; element < 4; ++element) { + if (mask[element]) { + lines.push_back(static_cast<u32>(IR::Attribute::Generic0X) + generic * 4 + element); + } + } + } +} + void TightenOutputVertices(IR::Program& program) { if (program.stage != Stage::Geometry || program.is_geometry_passthrough) { return; @@ -368,6 +394,30 @@ IR::Program TranslateProgram(ObjectPool<IR::Inst>& inst_pool, ObjectPool<IR::Blo program.workgroup_size = env.WorkgroupSize(); program.shared_memory_size = env.SharedMemorySize(); break; + case Stage::Task: + case Stage::Mesh: { + // SET_MESH_SHADER_A: topology 3:0, max vertex 12:4, max primitive 22:13; + // SET_MESH_SHADER_B: mesh threads 25:20; SET_MESH_INIT_SHADER: task threads 5:0, local + // buffer lines 20:12, lines passed to the mesh stage 30:22 (lines are 128 bytes). + const auto& registers{env.MeshRegisters()}; + const u32 shader_a{registers[0]}, shader_b{registers[1]}, init{registers[2]}; + const u32 lines{(std::max)((init >> 12) & 0x1ff, (init >> 22) & 0x1ff)}; + program.mesh.payload_words = (std::max)(lines, 1U) * 32; + program.mesh.topology = shader_a & 0xf; + program.mesh.max_vertices = (shader_a >> 4) & 0x1ff; + program.mesh.max_primitives = (shader_a >> 13) & 0x3ff; + const u32 threads{program.stage == Stage::Task ? init & 0x3f : (shader_b >> 20) & 0x3f}; + program.workgroup_size = {(std::max)(threads, 1U), 1, 1}; + if (program.stage == Stage::Mesh) { + if (program.mesh.topology != 4) { + throw NotImplementedException("Mesh output topology {} (SET_MESH_SHADER_A {:#010x})", + program.mesh.topology, shader_a); + } + AddMeshLines(env.SPH(), program.mesh.vertex_lines); + AddMeshLines(env.MeshPrimitiveHeader(), program.mesh.primitive_lines); + } + break; + } default: break; } @@ -406,6 +456,18 @@ IR::Program TranslateProgram(ObjectPool<IR::Inst>& inst_pool, ObjectPool<IR::Blo Optimization::VerificationPass(program); } Optimization::CollectShaderInfoPass(env, program); + program.info.is_task = program.stage == Stage::Task; + program.info.is_mesh = program.stage == Stage::Mesh; + if (program.stage == Stage::Mesh) { + // Its outputs come from the stage buffer lines, not attribute stores; the next stage + // reads what they name. + for (const u32 line : program.mesh.vertex_lines) { + program.info.stores.Set(static_cast<IR::Attribute>(line), true); + } + for (const u32 line : program.mesh.primitive_lines) { + program.info.stores.Set(static_cast<IR::Attribute>(line), true); + } + } Optimization::LayerPass(program, normalized_host_info); Optimization::VendorWorkaroundPass(program); diff --git a/src/shader_recompiler/ir_opt/collect_shader_info_pass.cpp b/src/shader_recompiler/ir_opt/collect_shader_info_pass.cpp index 9010c2bf68..c212a25d37 100644 --- a/src/shader_recompiler/ir_opt/collect_shader_info_pass.cpp +++ b/src/shader_recompiler/ir_opt/collect_shader_info_pass.cpp @@ -963,10 +963,12 @@ void CollectShaderInfoPass(Environment& env, IR::Program& program) { switch (program.stage) { case Stage::VertexA: case Stage::VertexB: + case Stage::Task: // bound in the vertex slot return 0x110u; case Stage::TessellationControl: return 0x210u; case Stage::TessellationEval: + case Stage::Mesh: // bound in the tessellation slot return 0x310u; case Stage::Geometry: return 0x410u; @@ -978,6 +980,11 @@ void CollectShaderInfoPass(Environment& env, IR::Program& program) { throw InvalidArgument("Invalid stage {}", program.stage); }()}; info.nvn_buffer_base = base; + if (program.stage == Stage::Task || program.stage == Stage::Mesh) { + // The stage buffer reads the workgroup index; the epilogue splits work by invocation. + info.uses_workgroup_id = true; + info.uses_local_invocation_id = true; + } for (IR::Block* const block : program.post_order_blocks) { for (IR::Inst& inst : block->Instructions()) { diff --git a/src/shader_recompiler/runtime_info.h b/src/shader_recompiler/runtime_info.h index e6e1284762..560afb3648 100644 --- a/src/shader_recompiler/runtime_info.h +++ b/src/shader_recompiler/runtime_info.h @@ -7,6 +7,7 @@ #pragma once #include <array> +#include <bitset> #include <map> #include <optional> #include <vector> @@ -85,6 +86,8 @@ struct TransformFeedbackVarying { struct RuntimeInfo { std::array<AttributeType, 32> generic_input_types{}; VaryingState previous_stage_stores; + /// Fragment stage after a mesh program: generic inputs it writes per primitive. + std::bitset<32> per_primitive_generics{}; std::map<IR::Attribute, IR::Attribute> previous_stage_legacy_stores_mapping; bool convert_depth_mode{}; diff --git a/src/shader_recompiler/shader_info.h b/src/shader_recompiler/shader_info.h index 71309e040e..ea70b2913e 100644 --- a/src/shader_recompiler/shader_info.h +++ b/src/shader_recompiler/shader_info.h @@ -239,11 +239,26 @@ struct ImageDescriptor { }; using ImageDescriptors = boost::container::small_vector<ImageDescriptor, 4>; +/// Switch 2 (NVN) task and mesh programs: the sizes their mesh registers set and, for mesh +/// programs, the attribute (IR::Attribute index) held by each 128-byte line of a 32-element group +/// in the emulated stage buffer: per-vertex lines from the program's output map, per-primitive +/// lines from the pass-through geometry header bound with it, in attribute order. +struct MeshInfo { + u32 payload_words{}; ///< Task output region / mesh input region, in 32-bit words + u32 max_vertices{}; + u32 max_primitives{}; + u32 topology{}; ///< SET_MESH_SHADER_A output topology (4 = triangles) + boost::container::static_vector<u32, 136> vertex_lines; + boost::container::static_vector<u32, 136> primitive_lines; +}; + struct Info { static constexpr size_t MAX_INDIRECT_CBUFS{14}; static constexpr size_t MAX_CBUFS{18}; static constexpr size_t MAX_SSBOS{32}; + bool is_task{}; ///< Switch 2 task program (Stage::Task) + bool is_mesh{}; ///< Switch 2 mesh program (Stage::Mesh) bool uses_workgroup_id{}; bool uses_local_invocation_id{}; bool uses_invocation_id{}; diff --git a/src/shader_recompiler/stage.h b/src/shader_recompiler/stage.h index 9af0a1b720..34172cfee2 100644 --- a/src/shader_recompiler/stage.h +++ b/src/shader_recompiler/stage.h @@ -17,6 +17,11 @@ enum class Stage : u32 { Compute, VertexA, + + // Switch 2 (NVN) task and mesh programs, bound in the vertex and tessellation slots while + // SET_MESH_CONTROL is enabled. + Task, + Mesh, }; constexpr u32 MaxStageTypes = 6; diff --git a/src/video_core/dirty_flags.cpp b/src/video_core/dirty_flags.cpp index e6856aaca3..2f359749aa 100644 --- a/src/video_core/dirty_flags.cpp +++ b/src/video_core/dirty_flags.cpp @@ -64,6 +64,8 @@ void SetupDirtyRenderTargets(Maxwell3D::DirtyState::Tables& tables) { void SetupDirtyShaders(Maxwell3D::DirtyState::Tables& tables) { FillBlock(tables[0], OFF(pipelines), NUM(pipelines[0]) * Maxwell3D::Regs::MaxShaderProgram, Shaders); + // Task and mesh programs are translated with the mesh registers. + FillBlock(tables[1], Maxwell3D::Regs::MeshControl, 4, Shaders); } } // Anonymous namespace diff --git a/src/video_core/engines/maxwell_3d.h b/src/video_core/engines/maxwell_3d.h index 0121f1a01c..3e946db1fe 100644 --- a/src/video_core/engines/maxwell_3d.h +++ b/src/video_core/engines/maxwell_3d.h @@ -2541,6 +2541,19 @@ public: return IsShaderConfigEnabled(static_cast<std::size_t>(type)); } + // Switch 2 (NVN) task and mesh pipelines (clc597.h): SET_MESH_CONTROL, SET_MESH_SHADER_A + // and _B, SET_MESH_INIT_SHADER. While mesh control is on, the vertex slot holds the task + // program (or the mesh program when there is no task), the tessellation slot the mesh + // program and the geometry slot a pass-through header naming the per-primitive outputs. + static constexpr std::size_t MeshControl = 0x114c / 4; + static constexpr std::size_t MeshShaderA = 0x1150 / 4; + static constexpr std::size_t MeshShaderB = 0x1154 / 4; + static constexpr std::size_t MeshInitShader = 0x1158 / 4; + + bool IsMeshEnabled() const { + return (reg_array[MeshControl] & 1) != 0; + } + struct ConstantBuffer { u32 size; u32 address_high; diff --git a/src/video_core/renderer_vulkan/maxwell_to_vk.cpp b/src/video_core/renderer_vulkan/maxwell_to_vk.cpp index c8e43d0650..e03e5786fe 100644 --- a/src/video_core/renderer_vulkan/maxwell_to_vk.cpp +++ b/src/video_core/renderer_vulkan/maxwell_to_vk.cpp @@ -333,6 +333,10 @@ VkShaderStageFlagBits ShaderStage(Shader::Stage stage) { return VK_SHADER_STAGE_FRAGMENT_BIT; case Shader::Stage::Compute: return VK_SHADER_STAGE_COMPUTE_BIT; + case Shader::Stage::Task: + return VK_SHADER_STAGE_TASK_BIT_EXT; + case Shader::Stage::Mesh: + return VK_SHADER_STAGE_MESH_BIT_EXT; } UNIMPLEMENTED_MSG("Unimplemented shader stage={}", stage); return {}; diff --git a/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp b/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp index 94bace432c..86b6e5a983 100644 --- a/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp +++ b/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp @@ -54,6 +54,17 @@ using VideoCore::Surface::PixelFormatFromRenderTargetFormat; constexpr size_t NUM_STAGES = Maxwell::MaxShaderStage; +// The stage of a program: task and mesh programs sit in the vertex and tessellation slots. +VkShaderStageFlagBits StageFlag(const Shader::Info& info, VkShaderStageFlagBits slot_stage) { + if (info.is_task) { + return VK_SHADER_STAGE_TASK_BIT_EXT; + } + if (info.is_mesh) { + return VK_SHADER_STAGE_MESH_BIT_EXT; + } + return slot_stage; +} + DescriptorLayoutBuilder MakeBuilder(const Device& device, std::span<const Shader::Info> infos) { DescriptorLayoutBuilder builder{device}; for (size_t index = 0; index < infos.size(); ++index) { @@ -64,7 +75,7 @@ DescriptorLayoutBuilder MakeBuilder(const Device& device, std::span<const Shader VK_SHADER_STAGE_GEOMETRY_BIT, VK_SHADER_STAGE_FRAGMENT_BIT, }; - builder.Add(infos[index], stages.at(index)); + builder.Add(infos[index], StageFlag(infos[index], stages.at(index))); } return builder; } @@ -1073,6 +1084,19 @@ void GraphicsPipeline::MakePipeline(VkRenderPass render_pass) { } } + const bool is_mesh{IsMeshPipeline()}; + if (is_mesh) { + // Mesh pipelines have no vertex input or input assembly. + dynamic_states.erase(std::remove_if(dynamic_states.begin(), dynamic_states.end(), + [](VkDynamicState state) { + return state == VK_DYNAMIC_STATE_PRIMITIVE_TOPOLOGY_EXT || + state == VK_DYNAMIC_STATE_PRIMITIVE_RESTART_ENABLE_EXT || + state == VK_DYNAMIC_STATE_VERTEX_INPUT_EXT || + state == VK_DYNAMIC_STATE_VERTEX_INPUT_BINDING_STRIDE_EXT || + state == VK_DYNAMIC_STATE_PATCH_CONTROL_POINTS_EXT; + }), + dynamic_states.end()); + } const VkPipelineDynamicStateCreateInfo dynamic_state_ci{ .sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO, .pNext = nullptr, @@ -1095,7 +1119,8 @@ void GraphicsPipeline::MakePipeline(VkRenderPass render_pass) { .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, .pNext = nullptr, .flags = 0, - .stage = MaxwellToVK::ShaderStage(Shader::StageFromIndex(stage)), + .stage = StageFlag(stage_infos[stage], + MaxwellToVK::ShaderStage(Shader::StageFromIndex(stage))), .module = *spv_modules[stage], .pName = "main", .pSpecializationInfo = nullptr, @@ -1115,9 +1140,9 @@ void GraphicsPipeline::MakePipeline(VkRenderPass render_pass) { .flags = flags, .stageCount = static_cast<u32>(shader_stages.size()), .pStages = shader_stages.data(), - .pVertexInputState = &vertex_input_ci, - .pInputAssemblyState = &input_assembly_ci, - .pTessellationState = &tessellation_ci, + .pVertexInputState = is_mesh ? nullptr : &vertex_input_ci, + .pInputAssemblyState = is_mesh ? nullptr : &input_assembly_ci, + .pTessellationState = is_mesh ? nullptr : &tessellation_ci, .pViewportState = &viewport_ci, .pRasterizationState = &rasterization_ci, .pMultisampleState = &multisample_ci, diff --git a/src/video_core/renderer_vulkan/vk_graphics_pipeline.h b/src/video_core/renderer_vulkan/vk_graphics_pipeline.h index b1ac5a3fcc..daee7910c1 100644 --- a/src/video_core/renderer_vulkan/vk_graphics_pipeline.h +++ b/src/video_core/renderer_vulkan/vk_graphics_pipeline.h @@ -98,6 +98,11 @@ public: bool UsesExtendedDynamicState() const noexcept { return key.state.extended_dynamic_state != 0; } + + /// Switch 2 task/mesh pipeline: drawn with vkCmdDrawMeshTasksEXT. + [[nodiscard]] bool IsMeshPipeline() const noexcept { + return stage_infos[0].is_task || stage_infos[0].is_mesh; + } GraphicsPipeline& operator=(GraphicsPipeline&&) noexcept = delete; GraphicsPipeline(GraphicsPipeline&&) noexcept = delete; diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp index 05511c10e1..f1a03ff38a 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp @@ -170,6 +170,14 @@ Shader::RuntimeInfo MakeRuntimeInfo(std::span<const Shader::IR::Program> program } else { info.previous_stage_stores.mask.set(); } + if (previous_program && previous_program->stage == Shader::Stage::Mesh) { + for (const u32 line : previous_program->mesh.primitive_lines) { + const auto attribute{static_cast<Shader::IR::Attribute>(line)}; + if (Shader::IR::IsGeneric(attribute)) { + info.per_primitive_generics.set(Shader::IR::GenericAttributeIndex(attribute)); + } + } + } const Shader::Stage stage{program.stage}; const bool has_geometry{key.unique_hashes[4] != 0 && !programs[4].is_geometry_passthrough}; const bool gl_ndc{key.state.ndc_minus_one_to_one != 0}; @@ -230,6 +238,9 @@ Shader::RuntimeInfo MakeRuntimeInfo(std::span<const Shader::IR::Program> program return Shader::TessSpacing::Equal; }(); break; + case Shader::Stage::Mesh: + info.convert_depth_mode = gl_ndc; + break; case Shader::Stage::Geometry: if (program.output_topology == Shader::OutputTopology::PointList) { info.fixed_state_point_size = point_size; @@ -378,7 +389,9 @@ PipelineCache::PipelineCache(Tegra::MaxwellDeviceMemoryManager& device_memory_, Shader::Stage::TessellationEval) | subgroup_stage_bit(VK_SHADER_STAGE_GEOMETRY_BIT, Shader::Stage::Geometry) | subgroup_stage_bit(VK_SHADER_STAGE_FRAGMENT_BIT, Shader::Stage::Fragment) | - subgroup_stage_bit(VK_SHADER_STAGE_COMPUTE_BIT, Shader::Stage::Compute)}; + subgroup_stage_bit(VK_SHADER_STAGE_COMPUTE_BIT, Shader::Stage::Compute) | + subgroup_stage_bit(VK_SHADER_STAGE_TASK_BIT_EXT, Shader::Stage::Task) | + subgroup_stage_bit(VK_SHADER_STAGE_MESH_BIT_EXT, Shader::Stage::Mesh)}; if (driver_id == VK_DRIVER_ID_MESA_TURNIP) { supported_subgroup_stages &= ~(1u << static_cast<u32>(Shader::Stage::Geometry)); } @@ -669,6 +682,15 @@ void PipelineCache::LoadDiskResources(u64 title_id, std::stop_token stop_loading GraphicsPipelineCacheKey key; file.read(reinterpret_cast<char*>(&key), sizeof(key)); + // Task/mesh pipelines are not stored (their mesh registers are not in the format); skip + // any that were. + if (std::ranges::any_of(envs, [](const FileEnvironment& env) { + return env.ShaderStage() == Shader::Stage::Task || + env.ShaderStage() == Shader::Stage::Mesh; + })) { + return; + } + if ((key.state.extended_dynamic_state != 0) != dynamic_features.has_extended_dynamic_state || (key.state.extended_dynamic_state_2 != 0) != @@ -1033,6 +1055,12 @@ std::unique_ptr<GraphicsPipeline> PipelineCache::CreateGraphicsPipeline() { if (!pipeline || pipeline_cache_filename.empty()) { return pipeline; } + if (pipeline->IsMeshPipeline()) { + // Not stored: task and mesh programs are translated with the mesh registers, which the + // cache format does not carry. The few of them rebuild quickly from the driver cache. + QueueVulkanPipelineCacheFlush(); + return pipeline; + } serialization_thread.QueueWork([this, key = graphics_key, envs = std::move(environments.envs)] { boost::container::static_vector<const GenericEnvironment*, Maxwell::MaxShaderProgram> env_ptrs; diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index cb31a0e7e4..91baee40e4 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -9,6 +9,7 @@ #include <memory> #include <cstdio> #include <cstdlib> +#include <filesystem> #include <tuple> #include <set> #include <fstream> @@ -279,6 +280,22 @@ void RasterizerVulkan::PrepareDraw(bool is_indexed, Func&& draw_func) { report_failure("no pipeline"); return; } + // Task/mesh pipelines: the draw's vertex count is the number of task workgroups. + mesh_draw = pipeline->IsMeshPipeline(); + // EDEN_NX2_NO_MESH=1: skip task/mesh draws as before they were supported (A/B comparisons). + // EDEN_NX2_NO_MESH_FILE=<path>: skip them while that file exists (checked every 1024 draws), + // to compare the same scene with and without them in one run. + static const bool no_mesh_always{std::getenv("EDEN_NX2_NO_MESH") != nullptr}; + static const char* const no_mesh_file{std::getenv("EDEN_NX2_NO_MESH_FILE")}; + static bool no_mesh{no_mesh_always}; + static u32 no_mesh_check{}; + if (no_mesh_file && (no_mesh_check++ % 1024) == 0) { + no_mesh = std::filesystem::exists(no_mesh_file); + } + if (mesh_draw && (no_mesh || !device.IsExtMeshShaderSupported())) { + report_failure(no_mesh ? "EDEN_NX2_NO_MESH" : "no VK_EXT_mesh_shader"); + return; + } std::scoped_lock lock{buffer_cache.mutex, texture_cache.mutex}; // update engine as channel may be different. pipeline->SetEngine(maxwell3d, gpu_memory); @@ -541,6 +558,12 @@ void RasterizerVulkan::Draw(bool is_indexed, u32 instance_count) { const u32 num_instances{instance_count}; const DrawParams draw_params{MakeDrawParams(draw_state, num_instances, is_indexed)}; + if (mesh_draw) { + scheduler.Record([groups = draw_params.num_vertices](vk::CommandBuffer cmdbuf) { + cmdbuf.DrawMeshTasksEXT(groups, 1, 1); + }); + return; + } scheduler.Record([draw_params](vk::CommandBuffer cmdbuf) { if (draw_params.is_indexed) { cmdbuf.DrawIndexed(draw_params.num_vertices, draw_params.num_instances, @@ -573,6 +596,14 @@ void RasterizerVulkan::DrawIndirect() { const auto& params = maxwell3d->draw_manager.indirect_state; buffer_cache.SetDrawIndirect(¶ms); PrepareDraw(params.is_indexed, [this, ¶ms] { + if (mesh_draw) { + // Indirect task/mesh draws would need vkCmdDrawMeshTasksIndirectEXT's layout. + static bool logged{}; + if (!std::exchange(logged, true)) { + LOG_WARNING(Render_Vulkan, "Indirect task/mesh draw skipped"); + } + return; + } const auto indirect_buffer = buffer_cache.GetDrawIndirectBuffer(); const auto& buffer = indirect_buffer.first; const auto& offset = indirect_buffer.second; diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.h b/src/video_core/renderer_vulkan/vk_rasterizer.h index ae9929ca0b..e8989dcdfc 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.h +++ b/src/video_core/renderer_vulkan/vk_rasterizer.h @@ -230,6 +230,7 @@ private: boost::container::static_vector<VkSampler, MAX_TEXTURES> sampler_handles; u32 draw_counter = 0; + bool mesh_draw{}; ///< the draw being prepared uses a task/mesh pipeline }; } // namespace Vulkan diff --git a/src/video_core/shader_cache.cpp b/src/video_core/shader_cache.cpp index dfe80abac3..f03df6bac5 100644 --- a/src/video_core/shader_cache.cpp +++ b/src/video_core/shader_cache.cpp @@ -14,6 +14,7 @@ #include <boost/container/small_vector.hpp> #include "common/assert.h" +#include "common/cityhash.h" #include "shader_recompiler/frontend/maxwell/control_flow.h" #include "shader_recompiler/exception.h" #include "shader_recompiler/object_pool.h" @@ -48,6 +49,22 @@ void ShaderCache::SyncGuestHost() { ShaderCache::ShaderCache(Tegra::MaxwellDeviceMemoryManager& device_memory_) : device_memory{device_memory_} {} +// Task and mesh programs are translated with the mesh registers and the header of the pass-through +// geometry program (per-primitive outputs), so pipelines are keyed by them too. +u64 ShaderCache::MeshStateHash() const { + using Regs = Tegra::Engines::Maxwell3D::Regs; + const auto& regs{maxwell3d->regs}; + std::array<u32, 3 + 20> state{regs.reg_array[Regs::MeshShaderA], regs.reg_array[Regs::MeshShaderB], + regs.reg_array[Regs::MeshInitShader]}; + if (regs.IsShaderConfigEnabled(Regs::ShaderType::Geometry)) { + const auto& geometry{regs.pipelines[static_cast<size_t>(Regs::ShaderType::Geometry)]}; + const GPUVAddr address{(static_cast<u64>(geometry.reserved[0] & 0xff) << 32) | + geometry.reserved[1]}; + gpu_memory->ReadBlockUnsafe(address, state.data() + 3, 20 * sizeof(u32)); + } + return Common::CityHash64(reinterpret_cast<const char*>(state.data()), sizeof(state)) | 1; +} + bool ShaderCache::RefreshStages(std::array<u64, 6>& unique_hashes) try { auto& dirty{maxwell3d->dirty.flags}; if (!dirty[VideoCommon::Dirty::Shaders]) { @@ -57,6 +74,7 @@ bool ShaderCache::RefreshStages(std::array<u64, 6>& unique_hashes) try { const GPUVAddr base_addr{maxwell3d->regs.program_region.Address()}; const bool native = maxwell3d->GetNativeMacroEngine().native_turing; + const bool mesh{native && maxwell3d->regs.IsMeshEnabled()}; for (size_t index = 0; index < Tegra::Engines::Maxwell3D::Regs::MaxShaderProgram; ++index) { if (!maxwell3d->regs.IsShaderConfigEnabled(index)) { unique_hashes[index] = 0; @@ -69,6 +87,11 @@ bool ShaderCache::RefreshStages(std::array<u64, 6>& unique_hashes) try { unique_hashes[index] = 0; continue; } + if (mesh && program == Tegra::Engines::Maxwell3D::Regs::ShaderType::Geometry) { + // A header without code: the mesh program's environment reads its output map. + unique_hashes[index] = 0; + continue; + } const GPUVAddr shader_addr{native ? (static_cast<u64>(shader_config.reserved[0] & 0xff) << 32) | shader_config.reserved[1] : base_addr + shader_config.offset}; @@ -111,6 +134,10 @@ bool ShaderCache::RefreshStages(std::array<u64, 6>& unique_hashes) try { } shader_infos[index] = shader_info; unique_hashes[index] = shader_info->unique_hash; + if (mesh && (program == Tegra::Engines::Maxwell3D::Regs::ShaderType::VertexB || + program == Tegra::Engines::Maxwell3D::Regs::ShaderType::Tessellation)) { + unique_hashes[index] ^= MeshStateHash(); + } } last_shaders_valid = true; return true; diff --git a/src/video_core/shader_cache.h b/src/video_core/shader_cache.h index f4179dd435..80e375a38b 100644 --- a/src/video_core/shader_cache.h +++ b/src/video_core/shader_cache.h @@ -151,6 +151,8 @@ private: bool IsRejected(VAddr addr); /// Remembers the exception being handled as this address's rejection. void RememberRejection(VAddr addr); + /// Hash of the state task and mesh programs are translated with. + u64 MeshStateHash() const; Tegra::MaxwellDeviceMemoryManager& device_memory; diff --git a/src/video_core/shader_environment.cpp b/src/video_core/shader_environment.cpp index 9d189ad4f0..78438769f5 100644 --- a/src/video_core/shader_environment.cpp +++ b/src/video_core/shader_environment.cpp @@ -470,8 +470,16 @@ GraphicsEnvironment::GraphicsEnvironment(Tegra::Engines::Maxwell3D& maxwell3d_, gpu_memory->ReadBlock(program_base + start_address, &sph, sizeof(sph)); initial_offset = sizeof(sph); texture_bound = maxwell3d->regs.bindless_texture_const_buffer_slot; - if (maxwell3d->GetNativeMacroEngine().native_turing) { - if (program != Maxwell::ShaderType::VertexB && program != Maxwell::ShaderType::Pixel) { + const auto& regs{maxwell3d->regs}; + const bool native{maxwell3d->GetNativeMacroEngine().native_turing}; + // Task and mesh programs: the task in the vertex slot, its mesh program in the tessellation + // slot (without a task, the mesh program is in the vertex slot). + const bool mesh_slot{native && regs.IsMeshEnabled() && + (program == Maxwell::ShaderType::VertexB || + program == Maxwell::ShaderType::Tessellation)}; + if (native) { + if (program != Maxwell::ShaderType::VertexB && program != Maxwell::ShaderType::Pixel && + !mesh_slot) { throw Shader::RuntimeError("Unsupported native graphics shader stage {}", static_cast<u32>(program)); } if (std::getenv("EDEN_NATIVE_DRAW_DIAGNOSTICS")) { @@ -516,6 +524,19 @@ GraphicsEnvironment::GraphicsEnvironment(Tegra::Engines::Maxwell3D& maxwell3d_, ASSERT_MSG(false, "Invalid program={}", program); break; } + if (mesh_slot) { + const bool has_task{regs.IsShaderConfigEnabled(Maxwell::ShaderType::Tessellation)}; + stage = program == Maxwell::ShaderType::VertexB && has_task ? Shader::Stage::Task + : Shader::Stage::Mesh; + mesh_registers = {regs.reg_array[Maxwell::MeshShaderA], regs.reg_array[Maxwell::MeshShaderB], + regs.reg_array[Maxwell::MeshInitShader]}; + if (stage == Shader::Stage::Mesh && regs.IsShaderConfigEnabled(Maxwell::ShaderType::Geometry)) { + const auto& geometry{regs.pipelines[static_cast<size_t>(Maxwell::ShaderType::Geometry)]}; + const GPUVAddr address{(static_cast<u64>(geometry.reserved[0] & 0xff) << 32) | + geometry.reserved[1]}; + gpu_memory->ReadBlock(address, &mesh_primitive_header, sizeof(mesh_primitive_header)); + } + } const u64 local_size{sph.LocalMemorySize()}; ASSERT(local_size <= (std::numeric_limits<u32>::max)()); local_memory_size = static_cast<u32>(local_size) + sph.common3.shader_local_memory_crs_size; diff --git a/src/video_core/vulkan_common/vulkan_device.cpp b/src/video_core/vulkan_common/vulkan_device.cpp index b65c721bb8..54f773ddeb 100644 --- a/src/video_core/vulkan_common/vulkan_device.cpp +++ b/src/video_core/vulkan_common/vulkan_device.cpp @@ -1515,6 +1515,15 @@ void Device::RemoveUnsuitableExtensions() { VK_KHR_PIPELINE_EXECUTABLE_PROPERTIES_EXTENSION_NAME); } + // VK_EXT_mesh_shader: Switch 2 titles draw some geometry with task and mesh programs. Only + // the two stages are used; the multiview, shading rate and query features need more. + features.mesh_shader.multiviewMeshShader = VK_FALSE; + features.mesh_shader.primitiveFragmentShadingRateMeshShader = VK_FALSE; + features.mesh_shader.meshShaderQueries = VK_FALSE; + extensions.mesh_shader = features.mesh_shader.taskShader && features.mesh_shader.meshShader; + RemoveExtensionFeatureIfUnsuitable(extensions.mesh_shader, features.mesh_shader, + VK_EXT_MESH_SHADER_EXTENSION_NAME); + // VK_KHR_shader_quad_control extensions.shader_quad_control = features.shader_quad_control.shaderQuadControl; RemoveExtensionFeatureIfUnsuitable(extensions.shader_quad_control, features.shader_quad_control, diff --git a/src/video_core/vulkan_common/vulkan_device.h b/src/video_core/vulkan_common/vulkan_device.h index 8237868c6d..299792b5bb 100644 --- a/src/video_core/vulkan_common/vulkan_device.h +++ b/src/video_core/vulkan_common/vulkan_device.h @@ -66,6 +66,7 @@ VK_DEFINE_HANDLE(VmaAllocator) FEATURE(EXT, 4444Formats, 4444_FORMATS, format_a4b4g4r4) \ FEATURE(EXT, IndexTypeUint8, INDEX_TYPE_UINT8, index_type_uint8) \ FEATURE(EXT, LineRasterization, LINE_RASTERIZATION, line_rasterization) \ + FEATURE(EXT, MeshShader, MESH_SHADER, mesh_shader) \ FEATURE(EXT, PrimitiveTopologyListRestart, PRIMITIVE_TOPOLOGY_LIST_RESTART, \ primitive_topology_list_restart) \ FEATURE(EXT, ProvokingVertex, PROVOKING_VERTEX, provoking_vertex) \ @@ -633,6 +634,12 @@ FN_MAX_LIMIT_LIST return extensions.shader_quad_control && features.shader_quad_control.shaderQuadControl; } + /// Returns true if the device supports task and mesh shaders (VK_EXT_mesh_shader). + bool IsExtMeshShaderSupported() const { + return extensions.mesh_shader && features.mesh_shader.taskShader && + features.mesh_shader.meshShader; + } + /// Returns true if the device supports VK_KHR_image_format_list. bool IsKhrImageFormatListSupported() const { return extensions.image_format_list || instance_version >= VK_API_VERSION_1_2; diff --git a/src/video_core/vulkan_common/vulkan_wrapper.cpp b/src/video_core/vulkan_common/vulkan_wrapper.cpp index dbaaf450a0..ac67b95707 100644 --- a/src/video_core/vulkan_common/vulkan_wrapper.cpp +++ b/src/video_core/vulkan_common/vulkan_wrapper.cpp @@ -116,6 +116,7 @@ void Load(VkDevice device, DeviceDispatch& dld) noexcept { X(vkCmdDrawIndirectCount); X(vkCmdDrawIndexedIndirectCount); X(vkCmdDrawIndirectByteCountEXT); + X(vkCmdDrawMeshTasksEXT); X(vkCmdEndConditionalRenderingEXT); X(vkCmdEndQuery); X(vkCmdEndRenderPass); diff --git a/src/video_core/vulkan_common/vulkan_wrapper.h b/src/video_core/vulkan_common/vulkan_wrapper.h index 3010368973..f9b3cc15df 100644 --- a/src/video_core/vulkan_common/vulkan_wrapper.h +++ b/src/video_core/vulkan_common/vulkan_wrapper.h @@ -232,6 +232,7 @@ struct DeviceDispatch : InstanceDispatch { PFN_vkCmdDrawIndirectCount vkCmdDrawIndirectCount{}; PFN_vkCmdDrawIndexedIndirectCount vkCmdDrawIndexedIndirectCount{}; PFN_vkCmdDrawIndirectByteCountEXT vkCmdDrawIndirectByteCountEXT{}; + PFN_vkCmdDrawMeshTasksEXT vkCmdDrawMeshTasksEXT{}; PFN_vkCmdEndConditionalRenderingEXT vkCmdEndConditionalRenderingEXT{}; PFN_vkCmdEndDebugUtilsLabelEXT vkCmdEndDebugUtilsLabelEXT{}; PFN_vkCmdEndQuery vkCmdEndQuery{}; @@ -1319,6 +1320,10 @@ public: dld->vkCmdDrawIndexedIndirect(handle, src_buffer, src_offset, draw_count, stride); } + void DrawMeshTasksEXT(u32 group_count_x, u32 group_count_y, u32 group_count_z) const noexcept { + dld->vkCmdDrawMeshTasksEXT(handle, group_count_x, group_count_y, group_count_z); + } + void DrawIndirectCount(VkBuffer src_buffer, VkDeviceSize src_offset, VkBuffer count_buffer, VkDeviceSize count_offset, u32 draw_count, u32 stride) const noexcept { dld->vkCmdDrawIndirectCount(handle, src_buffer, src_offset, count_buffer, count_offset, |
