aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorTakoroka <137927095+Takoroka@users.noreply.github.com>2026-11-01 13:25:40 -0500
committerTakoroka <137927095+Takoroka@users.noreply.github.com>2026-11-01 13:25:40 -0500
commit235e2f5f5548e20620c247b76586cbb559f4158d (patch)
tree4b282626b7d70498cb493653a585ac9d8a4f074c /src
parent6bde8c20d1f7c020a920e59a989c03822a08445d (diff)
parent004ca96531812ea3674273ed568e3a672e664932 (diff)
Merge remote-tracking branch 'origin/feat/nx2-mesh-shaders' into test/linux-all-prs
Diffstat (limited to 'src')
-rw-r--r--src/shader_recompiler/CMakeLists.txt1
-rw-r--r--src/shader_recompiler/backend/glasm/emit_glasm.cpp3
-rw-r--r--src/shader_recompiler/backend/glasm/emit_glasm_instructions.h4
-rw-r--r--src/shader_recompiler/backend/glasm/emit_glasm_not_implemented.cpp16
-rw-r--r--src/shader_recompiler/backend/glasm/glasm_emit_context.cpp3
-rw-r--r--src/shader_recompiler/backend/glsl/emit_glsl_instructions.h4
-rw-r--r--src/shader_recompiler/backend/glsl/emit_glsl_not_implemented.cpp16
-rw-r--r--src/shader_recompiler/backend/glsl/glsl_emit_context.cpp3
-rw-r--r--src/shader_recompiler/backend/spirv/emit_spirv.cpp60
-rw-r--r--src/shader_recompiler/backend/spirv/emit_spirv_instructions.h6
-rw-r--r--src/shader_recompiler/backend/spirv/emit_spirv_mesh.cpp227
-rw-r--r--src/shader_recompiler/backend/spirv/emit_spirv_special.cpp7
-rw-r--r--src/shader_recompiler/backend/spirv/spirv_emit_context.cpp127
-rw-r--r--src/shader_recompiler/backend/spirv/spirv_emit_context.h27
-rw-r--r--src/shader_recompiler/environment.h13
-rw-r--r--src/shader_recompiler/frontend/ampere/lower_to_maxwell.h618
-rw-r--r--src/shader_recompiler/frontend/ir/ir_emitter.cpp16
-rw-r--r--src/shader_recompiler/frontend/ir/ir_emitter.h5
-rw-r--r--src/shader_recompiler/frontend/ir/microinstruction.cpp3
-rw-r--r--src/shader_recompiler/frontend/ir/opcodes.cpp4
-rw-r--r--src/shader_recompiler/frontend/ir/opcodes.h4
-rw-r--r--src/shader_recompiler/frontend/ir/opcodes.inc4
-rw-r--r--src/shader_recompiler/frontend/ir/program.h1
-rw-r--r--src/shader_recompiler/frontend/maxwell/maxwell.inc1
-rw-r--r--src/shader_recompiler/frontend/maxwell/structured_control_flow.cpp29
-rw-r--r--src/shader_recompiler/frontend/maxwell/translate/impl/internal_stage_buffer_entry_read.cpp36
-rw-r--r--src/shader_recompiler/frontend/maxwell/translate_program.cpp62
-rw-r--r--src/shader_recompiler/ir_opt/collect_shader_info_pass.cpp7
-rw-r--r--src/shader_recompiler/runtime_info.h3
-rw-r--r--src/shader_recompiler/shader_info.h15
-rw-r--r--src/shader_recompiler/stage.h5
-rw-r--r--src/video_core/dirty_flags.cpp2
-rw-r--r--src/video_core/engines/maxwell_3d.h13
-rw-r--r--src/video_core/renderer_vulkan/maxwell_to_vk.cpp4
-rw-r--r--src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp35
-rw-r--r--src/video_core/renderer_vulkan/vk_graphics_pipeline.h5
-rw-r--r--src/video_core/renderer_vulkan/vk_pipeline_cache.cpp30
-rw-r--r--src/video_core/renderer_vulkan/vk_rasterizer.cpp31
-rw-r--r--src/video_core/renderer_vulkan/vk_rasterizer.h1
-rw-r--r--src/video_core/shader_cache.cpp27
-rw-r--r--src/video_core/shader_cache.h2
-rw-r--r--src/video_core/shader_environment.cpp25
-rw-r--r--src/video_core/vulkan_common/vulkan_device.cpp9
-rw-r--r--src/video_core/vulkan_common/vulkan_device.h7
-rw-r--r--src/video_core/vulkan_common/vulkan_wrapper.cpp1
-rw-r--r--src/video_core/vulkan_common/vulkan_wrapper.h5
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(&params);
PrepareDraw(params.is_indexed, [this, &params] {
+ 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,