diff --git a/CMakeLists.txt b/CMakeLists.txt index 4b24b1c0a..84c8ec836 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -655,6 +655,8 @@ if(BUILD_TESTING) add_test(NAME save_data_dialog COMMAND $) add_test(NAME http_uri_parse COMMAND $) add_test(NAME shader_recompiler_compute COMMAND $) + add_test(NAME shader_recompiler_tessellation + COMMAND $ --tessellation-only) add_test(NAME shader_recompiler_alignbyte COMMAND $ --alignbyte-only) add_test(NAME graphics_pipeline_rasterization diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index c53890f02..3b4cbe7cd 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -84,6 +84,7 @@ #include #include #include +#include #include #include #include @@ -43692,6 +43693,58 @@ void CheckPs5GameExampleImageClearRuntimeShape() { std::printf("[host] %-32s ok\n", "Ps5GameExampleImageClear"); } +struct TessellationStage { + // Immediate byte addresses of the lowered accesses, per attribute kind. + std::map> addresses; + std::string assembly; +}; + +TessellationStage CompileTessellationStage(const char *name, const std::vector &code, + ShaderType stage, + const ShaderTessellationInputInfo &tess) { + using namespace ShaderRecompiler; + ShaderVertexInputInfo vertex; + vertex.logical_stage = stage; + vertex.tess = tess; + CompileOptions options; + options.stage = stage; + options.input_info.vertex = &vertex; + auto translated = TranslateProgram(code, options); + TessellationStage result; + for (const auto *block : translated.program.blocks) { + for (const auto &inst : *block) { + if ((inst.GetOpcode() == IR::ValueOpcode::GetTessellationAttribute || + inst.GetOpcode() == IR::ValueOpcode::SetTessellationAttribute) && + inst.Arg(1).IsImmediate()) { + result.addresses[static_cast(inst.Arg(0).U32())].insert( + inst.Arg(1).U32()); + } + } + } + auto compiled = CompileProgram(std::move(translated), options, {}); + ValidateSpirv(name, compiled.spirv); + spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_2); + Require(name, "disassembly", tools.Disassemble(compiled.spirv, &result.assembly), + "failed to disassemble tessellation shader"); + return result; +} + +// Whether a Patch uvec4[vectors] variable of a storage class sits at a Location. +bool HasPatchInterface(const std::string &assembly, const std::string &storage, u32 vectors, + u32 location) { + const auto variable = " = OpVariable %_ptr_" + storage + "__arr_v4uint_uint_" + + std::to_string(vectors) + " " + storage + "\n"; + const auto end = assembly.find(variable); + if (end == std::string::npos) { + return false; + } + const auto begin = assembly.rfind('%', end); + const auto id = assembly.substr(begin, end - begin); + return assembly.find("OpDecorate " + id + " Patch\n") != std::string::npos && + assembly.find("OpDecorate " + id + " Location " + std::to_string(location) + "\n") != + std::string::npos; +} + void CheckTessellationProgram(const char *name, u32 ls_stride, u32 hs_stride) { using namespace ShaderRecompiler; const bool multiplied_stride = hs_stride == 112; @@ -43700,8 +43753,7 @@ void CheckTessellationProgram(const char *name, u32 ls_stride, u32 hs_stride) { const u32 relative_patch = multiplied_stride ? 28 : 32; const u32 output_address = multiplied_stride ? 31 : 35; const u32 input_address = multiplied_stride ? 25 : 29; - std::array, 3> code; - auto &local = code[0]; + std::vector local, control, evaluation; // The fused LS addresses one control point using its v3 ordinal. local.push_back(EncodeVop2(0x0b, local_address, 255u, 3)); local.push_back(ls_stride); @@ -43710,7 +43762,6 @@ void CheckTessellationProgram(const char *name, u32 ls_stride, u32 hs_stride) { local.push_back(EncodeSop1(0x20, 0, 6)); local.push_back(0xffffffffu); // Shader footer is not executable after LS handoff. - auto &control = code[1]; AppendVop3(&control, 0x365, 0, 193u, InlineU32(0)); AppendVop3(&control, 0x366, 0, 193u, Vgpr(0)); control.push_back(EncodeSopp(0x0a)); @@ -43769,7 +43820,6 @@ void CheckTessellationProgram(const char *name, u32 ls_stride, u32 hs_stride) { } AppendEnd(&control); - auto &evaluation = code[2]; AppendVop3(&evaluation, 0x169, 24, 255u, Vgpr(7)); evaluation.push_back(3 * hs_stride); evaluation.push_back(EncodeMubuf0(0x0d, 28)); @@ -43790,91 +43840,187 @@ void CheckTessellationProgram(const char *name, u32 ls_stride, u32 hs_stride) { tess.ls_stride == ls_stride && tess.hs_stride == hs_stride, "captured LS and HS address arithmetic must produce distinct strides"); - constexpr std::array stages{ShaderType::Local, ShaderType::TessellationControl, - ShaderType::TessellationEvaluation}; - for (u32 stage = 0; stage < stages.size(); stage++) { - ShaderVertexInputInfo vertex; - vertex.logical_stage = stages[stage]; - vertex.tess = tess; - CompileOptions options; - options.stage = stages[stage]; - options.input_info.vertex = &vertex; - auto translated = TranslateProgram(code[stage], options); - std::set local_offsets, evaluation_offsets, factor_offsets, patch_offsets; - for (const auto *block : translated.program.blocks) { - for (const auto &inst : *block) { - if (inst.GetOpcode() != IR::ValueOpcode::GetTessellationAttribute && - inst.GetOpcode() != IR::ValueOpcode::SetTessellationAttribute) { - continue; - } - if (!inst.Arg(1).IsImmediate()) { - continue; - } - switch (static_cast(inst.Arg(0).U32())) { - case IR::TessellationAttribute::LocalOutput: - local_offsets.insert(inst.Arg(1).U32()); - break; - case IR::TessellationAttribute::EvaluationInput: - evaluation_offsets.insert(inst.Arg(1).U32()); - break; - case IR::TessellationAttribute::Factor: - factor_offsets.insert(inst.Arg(1).U32()); - break; - case IR::TessellationAttribute::PatchOutput: - patch_offsets.insert(inst.Arg(1).U32()); - break; - default: - break; - } - } - } - Require(name, "separate DS offsets", stage != 0 || local_offsets == std::set{4, 12}, - "DS_WRITE2 must preserve both independently encoded offsets"); - Require(name, "offchip component addressing", - stage != 2 || - evaluation_offsets == std::set{28, 2 * hs_stride + 28}, - "TES control point and component offsets must survive lowering"); - Require(name, "triangle factor layout", - stage != 1 || factor_offsets == std::set{0, 4, 8, 12}, - "the four packed guest factors must retain their order"); - Require(name, "predicated patch addresses", - stage != 1 || patch_offsets == std::set{0x7fc0, 0x7fd0, 0x7fe0, 0x7ff0}, - "CP0 patch stores must use the address assigned on their active predicate"); - auto result = CompileProgram(std::move(translated), options, {}); - ValidateSpirv(name, result.spirv); - spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_2); - std::string assembly; - Require(name, "disassembly", tools.Disassemble(result.spirv, &assembly), - "failed to disassemble tessellation shader"); - Require(name, "native local inputs", - stage != 0 || (assembly.find("VertexIndex") != std::string::npos && - assembly.find("InstanceIndex") != std::string::npos), - "LS v2/v5 must read native vertex/instance indices"); - Require(name, "control point lane", - stage != 1 || (assembly.find("InvocationId") != std::string::npos && - assembly.find("SubgroupLocalInvocationId") == std::string::npos), - "HS lane masks must use control point invocation IDs"); - Require(name, "control point barrier", - stage != 1 || (assembly.find("OpMemoryModel Logical GLSL450") != std::string::npos && - assembly.find("OpControlBarrier %uint_2 %uint_4 %uint_0") != std::string::npos), - "HS barriers must match GLSL450 control point synchronization semantics"); - Require(name, "factor builtins", - stage != 1 || (assembly.find("TessLevelOuter") != std::string::npos && - assembly.find("TessLevelInner") != std::string::npos), - "triangle factors require outer[4] and inner[2] Vulkan builtins"); - Require(name, "native domain and winding", - stage != 2 || (assembly.find("TessCoord") != std::string::npos && - assembly.find("Triangles") != std::string::npos && - assembly.find("SpacingFractionalOdd") != std::string::npos && - assembly.find("VertexOrderCw") != std::string::npos), - "TES must preserve guest triangle domain, partitioning, and winding"); - } + Require(name, "patch constant range", tess.patch_base == 0x7fc0u && tess.patch_size == 52u, + "CP0 patch stores must reflect the HS patch constant range"); + + using Attribute = IR::TessellationAttribute; + auto ls = CompileTessellationStage(name, local, ShaderType::Local, tess); + auto hs = CompileTessellationStage(name, control, ShaderType::TessellationControl, tess); + auto ds = CompileTessellationStage(name, evaluation, ShaderType::TessellationEvaluation, tess); + Require(name, "separate DS offsets", ls.addresses[Attribute::LocalOutput] == std::set{4, 12}, + "DS_WRITE2 must preserve both independently encoded offsets"); + Require(name, "offchip component addressing", + ds.addresses[Attribute::EvaluationInput] == std::set{28, 2 * hs_stride + 28}, + "TES control point and component offsets must survive lowering"); + Require(name, "triangle factor layout", + hs.addresses[Attribute::Factor] == std::set{0, 4, 8, 12}, + "the four packed guest factors must retain their order"); + Require(name, "predicated patch addresses", + hs.addresses[Attribute::PatchOutput] == std::set{0x7fc0, 0x7fd0, 0x7fe0, 0x7ff0}, + "CP0 patch stores must use the address assigned on their active predicate"); + Require(name, "native local inputs", + ls.assembly.find("VertexIndex") != std::string::npos && + ls.assembly.find("InstanceIndex") != std::string::npos, + "LS v2/v5 must read native vertex/instance indices"); + Require(name, "control point lane", + hs.assembly.find("InvocationId") != std::string::npos && + hs.assembly.find("SubgroupLocalInvocationId") == std::string::npos, + "HS lane masks must use control point invocation IDs"); + Require(name, "control point barrier", + hs.assembly.find("OpMemoryModel Logical GLSL450") != std::string::npos && + hs.assembly.find("OpControlBarrier %uint_2 %uint_4 %uint_0") != std::string::npos, + "HS barriers must match GLSL450 control point synchronization semantics"); + Require(name, "factor builtins", + hs.assembly.find("TessLevelOuter") != std::string::npos && + hs.assembly.find("TessLevelInner") != std::string::npos, + "triangle factors require outer[4] and inner[2] Vulkan builtins"); + Require(name, "native domain and winding", + ds.assembly.find("TessCoord") != std::string::npos && + ds.assembly.find("Triangles") != std::string::npos && + ds.assembly.find("SpacingFractionalOdd") != std::string::npos && + ds.assembly.find("VertexOrderCw") != std::string::npos, + "TES must preserve guest triangle domain, partitioning, and winding"); std::printf("[host] %-32s ok\n", name); } +// PPSA25468: CP0 writes patch constants below the 32 KiB offchip ring end +// through mul24 addresses that reuse data VGPRs under nested EXEC masks, and +// the TES reads them back. +void CheckTessellationPatchConstants() { + using namespace ShaderRecompiler; + using Attribute = IR::TessellationAttribute; + constexpr const char *name = "TessellationPatchConstants"; + constexpr u32 ls_stride = 48, hs_stride = 80, patch_stride = 0xf0; + std::vector local; + // One 128-bit DS store writes the LS record. + local.push_back(EncodeVop2(0x0b, 20, 255u, 3)); // v_mul_u32_u24 v20, ls_stride, v3 + local.push_back(ls_stride); + local.push_back(EncodeDs0(0xdf, 16)); // ds_write_b128 v20, v[2:5] offset:16 + local.push_back(EncodeDs1(0, 2, 20)); + local.push_back(EncodeSop1(0x20, 0, 6)); + + const auto make_control = [&](bool divergent) { + std::vector control; + AppendVop3(&control, 0x365, 0, 193u, InlineU32(0)); + AppendVop3(&control, 0x366, 0, 193u, Vgpr(0)); + control.push_back(EncodeVopc(0xd1, 249u, 3)); + control.push_back(EncodeVopcSdwa(0, 0, 0, 6, 1, 0, 0, 0, 0, 0, 0, 0, 1)); + const auto skip_control = control.size(); + control.push_back(0u); // s_cbranch_execz + AppendVop3(&control, 0x148, 16, Vgpr(1), InlineU32(8), InlineU32(5)); + control.push_back(EncodeVop2(0x1b, 50, 255u, 1)); // v_and_b32 v50, 0xff, v1 + control.push_back(0xffu); + control.push_back(EncodeVop2(0x0b, 0, 255u, 16)); // v_mul_u32_u24 v0, hs_stride, v16 + control.push_back(hs_stride); + AppendVop3(&control, 0x169, 1, Vgpr(16), 255u); // v_mul_lo_u32 v1, v16, ls_stride + control.push_back(ls_stride); + AppendVop3(&control, 0x143, 17, 255u, Vgpr(50), Vgpr(0)); + control.push_back(3 * hs_stride); + AppendVop3(&control, 0x143, 14, 255u, Vgpr(50), Vgpr(1)); + control.push_back(3 * ls_stride); + // ds_read2_b32 v[10:11], v14 offset0:4 offset1:5 + control.push_back(EncodeDs0(0x37, (5u << 8u) | 4u)); + control.push_back(EncodeDs1(10, 0, 14)); + control.push_back(EncodeMubuf0(0x1d, 16)); + control.push_back(EncodeMubuf1(10, 2, 17, 2)); + // v17 stays affine at the forward branch's join unless the paths diverge. + control.push_back(EncodeVopc(0x01, InlineU32(0), 10)); // v_cmp_lt_f32 vcc, 0, v10 + const auto skip_scale = control.size(); + control.push_back(0u); // s_cbranch_vccz + control.push_back(EncodeVop1(0x01, 12, 242u)); // v_mov_b32 v12, 1.0 + if (divergent) { + control.push_back(EncodeVop2(0x25, 17, InlineU32(4), 17)); // v_add_nc_u32 v17, 4, v17 + } + control[skip_scale] = EncodeSopp(0x06, control.size() - skip_scale - 1u); + control.push_back(EncodeMubuf0(0x1c, 24)); + control.push_back(EncodeMubuf1(12, 2, 17, 2)); + // CP0 reuses data VGPR v10 for the ring address 0x8000 - (patch + 1) * 0xf0. + control.push_back(EncodeVopc(0xd4, InlineU32(1), 16)); // v_cmpx_gt_u32 1, v16 + const auto skip_patch = control.size(); + control.push_back(0u); // s_cbranch_execz + control.push_back(EncodeVop2(0x25, 10, InlineU32(1), 50)); // v_add_nc_u32 v10, 1, v50 + control.push_back(EncodeVop2(0x0b, 10, 255u, 10)); // v_mul_u32_u24 v10, 0xf0, v10 + control.push_back(patch_stride); + control.push_back(EncodeVop2(0x26, 10, 255u, 10)); // v_sub_nc_u32 v10, 0x8000, v10 + control.push_back(0x8000u); + control.push_back(EncodeVopc(0x01, InlineU32(0), 11)); // v_cmp_lt_f32 vcc, 0, v11 + control.push_back(EncodeSop1(0x24, 0, 106)); // s_and_saveexec_b64 s[0:1], vcc + const auto skip_nested = control.size(); + control.push_back(0u); // s_cbranch_execz + control.push_back(EncodeMubuf0(0x1c, 0)); // buffer_store_dword v11, v10, s[8:11], s2 offen + control.push_back(EncodeMubuf1(11, 2, 10, 2)); + control[skip_nested] = EncodeSopp(0x08, control.size() - skip_nested - 1u); + control.push_back(EncodeSop1(0x04, 126, 0)); // s_mov_b64 exec, s[0:1] + // buffer_store_dwordx3 v[11:13], v10, s[8:11], s2 offen offset:16 + control.push_back(EncodeMubuf0(0x1f, 16)); + control.push_back(EncodeMubuf1(11, 2, 10, 2)); + control[skip_control] = EncodeSopp(0x08, control.size() - skip_control - 1u); + control[skip_patch] = EncodeSopp(0x08, control.size() - skip_patch - 1u); + AppendEnd(&control); + return control; + }; + const auto control = make_control(false); + + std::vector evaluation; + AppendVop3(&evaluation, 0x169, 24, 255u, Vgpr(7)); + evaluation.push_back(3 * hs_stride); + evaluation.push_back(EncodeVop2(0x25, 2, InlineU32(1), 7)); + evaluation.push_back(EncodeVop2(0x0b, 2, 255u, 2)); + evaluation.push_back(patch_stride); + evaluation.push_back(EncodeVop2(0x26, 2, 255u, 2)); + evaluation.push_back(0x8000u); + evaluation.push_back(EncodeMubuf0(0x0d, hs_stride + 16)); // CP1 dwords 4-5 + evaluation.push_back(EncodeMubuf1(10, 2, 24, 4)); + evaluation.push_back(EncodeMubuf0(0x0d, 16)); // patch constant dwords 4-5 + evaluation.push_back(EncodeMubuf1(12, 2, 2, 4)); + evaluation.push_back(EncodeVop2(0x03, 10, Vgpr(12), 10)); // v_add_f32 v10, v12, v10 + evaluation.push_back(EncodeVop2(0x03, 11, Vgpr(13), 11)); + evaluation.push_back(EncodeExp0(0x0c, 0xf)); + evaluation.push_back(EncodeExp1(5, 6, 10, 11)); + AppendEnd(&evaluation); + + ShaderTessellationInputInfo tess{.input_control_points = 3, + .output_control_points = 3, + .domain = 1, + .partitioning = 2, + .output_topology = 2}; + AnalyzeTessellationPrograms(local, control, tess); + Require(name, "reflected interface", + tess.ls_stride == ls_stride && tess.hs_stride == hs_stride && + tess.patch_base == 0x7f10u && tess.patch_size == 28u, + "b128 LS records, joined HS paths and CP0 patch stores must reflect the layout"); + + auto ls = CompileTessellationStage(name, local, ShaderType::Local, tess); + auto hs = CompileTessellationStage(name, control, ShaderType::TessellationControl, tess); + auto ds = CompileTessellationStage(name, evaluation, ShaderType::TessellationEvaluation, tess); + Require(name, "128-bit LS record", + ls.addresses[Attribute::LocalOutput] == std::set{16, 20, 24, 28}, + "DS_WRITE_B128 must lower to four consecutive LS outputs"); + Require(name, "predicated patch outputs", + hs.addresses[Attribute::PatchOutput] == std::set{0x7f10, 0x7f20, 0x7f24, 0x7f28}, + "CP0 mul24 addresses under nested EXEC masks must reach patch constants"); + Require(name, "patch inputs", + ds.addresses[Attribute::PatchInput] == std::set{0x7f20, 0x7f24} && + ds.addresses[Attribute::EvaluationInput] == + std::set{hs_stride + 16, hs_stride + 20}, + "TES patch constant reads must not alias control point inputs"); + Require(name, "matching patch interface", + HasPatchInterface(hs.assembly, "Output", 2, 5) && + HasPatchInterface(ds.assembly, "Input", 2, 5), + "HS and TES patch variables must share their Location and type"); + std::printf("[host] %-32s ok\n", name); +#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS + ExpectFatal("TessellationDivergentJoin", [&] { + ShaderTessellationInputInfo info{.input_control_points = 3, .output_control_points = 3}; + AnalyzeTessellationPrograms(local, make_control(true), info); + }); +#endif +} + void CheckTessellationPrograms() { CheckTessellationProgram("TessellationShiftedStride", 124, 128); CheckTessellationProgram("TessellationMultipliedStride", 108, 112); + CheckTessellationPatchConstants(); } void CheckEmbeddedFetchVertexOffset() {