tests: cover tessellation patch constants, predicated mul24 addresses, and forward-branch joins

This commit is contained in:
nmzik
2026-10-20 08:37:43 +02:00
parent 0957f088ad
commit f3e01aaf1e
2 changed files with 231 additions and 83 deletions
+229 -83
View File
@@ -84,6 +84,7 @@
#include <cstring>
#include <initializer_list>
#include <limits>
#include <map>
#include <memory>
#include <mutex>
#include <numeric>
@@ -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<ShaderRecompiler::IR::TessellationAttribute, std::set<u32>> addresses;
std::string assembly;
};
TessellationStage CompileTessellationStage(const char *name, const std::vector<u32> &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<IR::TessellationAttribute>(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<std::vector<u32>, 3> code;
auto &local = code[0];
std::vector<u32> 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<u32> 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<IR::TessellationAttribute>(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<u32>{4, 12},
"DS_WRITE2 must preserve both independently encoded offsets");
Require(name, "offchip component addressing",
stage != 2 ||
evaluation_offsets == std::set<u32>{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<u32>{0, 4, 8, 12},
"the four packed guest factors must retain their order");
Require(name, "predicated patch addresses",
stage != 1 || patch_offsets == std::set<u32>{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<u32>{4, 12},
"DS_WRITE2 must preserve both independently encoded offsets");
Require(name, "offchip component addressing",
ds.addresses[Attribute::EvaluationInput] == std::set<u32>{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<u32>{0, 4, 8, 12},
"the four packed guest factors must retain their order");
Require(name, "predicated patch addresses",
hs.addresses[Attribute::PatchOutput] == std::set<u32>{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<u32> 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<u32> 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<u32> 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<u32>{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<u32>{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<u32>{0x7f20, 0x7f24} &&
ds.addresses[Attribute::EvaluationInput] ==
std::set<u32>{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() {