Merge pull request #4114 from Celegans12/fix/agc-tessellation-num-patches

fix(agc): bound tessellation patch records by the draw's patch count
This commit is contained in:
Adrià 2026-10-13 15:40:48 +02:00 committed by GitHub
commit 2cf23fcc56
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 62 additions and 20 deletions

View File

@ -26,7 +26,7 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr<Vulk
mix(graphics.stages.tessellation.has_value());
if (graphics.stages.tessellation) {
const auto& tess = *graphics.stages.tessellation;
for (const auto value : {tess.inputControlPoints, tess.outputControlPoints, tess.domain, tess.partitioning, tess.outputTopology}) mix(value);
for (const auto value : {tess.inputControlPoints, tess.outputControlPoints, tess.domain, tess.partitioning, tess.outputTopology, tess.patchesPerGroup}) mix(value);
}
mix(graphics.rectList);
mix(programs.size());

View File

@ -118,7 +118,7 @@ bool Driver::sameDecode(const DrawDecode& a, const DrawDecode& b) {
const auto sameTess = [](const std::optional<ShaderRecompiler::TessellationConfiguration>& x, const std::optional<ShaderRecompiler::TessellationConfiguration>& y) {
if (x.has_value() != y.has_value()) return false;
if (!x) return true;
return x->inputControlPoints == y->inputControlPoints && x->outputControlPoints == y->outputControlPoints && x->domain == y->domain && x->partitioning == y->partitioning && x->outputTopology == y->outputTopology;
return x->inputControlPoints == y->inputControlPoints && x->outputControlPoints == y->outputControlPoints && x->domain == y->domain && x->partitioning == y->partitioning && x->outputTopology == y->outputTopology && x->patchesPerGroup == y->patchesPerGroup;
};
if (s.stages.path != t.stages.path || s.stages.registerValue != t.stages.registerValue || s.stages.vertexWaveSize != t.stages.vertexWaveSize || s.stages.fragmentWaveSize != t.stages.fragmentWaveSize || !sameMesh(s.stages.mesh, t.stages.mesh) || !sameTess(s.stages.tessellation, t.stages.tessellation)) return false;
if (!sameColor(s.color, t.color) || s.colors.size() != t.colors.size() || s.blends.size() != t.blends.size()) return false;

View File

@ -665,6 +665,7 @@ bool ValidationKey(const Context& context, std::span<const CompiledShader> shade
add(tessellation.domain);
add(tessellation.partitioning);
add(tessellation.outputTopology);
add(tessellation.patchesPerGroup);
}
}
return keyed;

View File

@ -827,6 +827,7 @@ void pipelineKey(std::vector<std::byte>& key, const Context& context, const Stat
append(key, tessellation.domain);
append(key, tessellation.partitioning);
append(key, tessellation.outputTopology);
append(key, tessellation.patchesPerGroup);
}
}

View File

@ -612,8 +612,9 @@ ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup) {
validate((value & ~0x0207ed0du) == 0 && (value & 0x02002000u) == 0x02002000u && (value & 3u) == 1u && ((value >> 3u) & 3u) == 1u, "unsupported tessellation routing");
const auto config = read(queue.context, 0x2d6);
const auto parameters = read(queue.context, 0x2db);
ShaderRecompiler::TessellationConfiguration tess{(config >> 8u) & 0x3fu, (config >> 14u) & 0x3fu, parameters & 3u, (parameters >> 2u) & 7u, (parameters >> 5u) & 7u};
ShaderRecompiler::TessellationConfiguration tess{(config >> 8u) & 0x3fu, (config >> 14u) & 0x3fu, parameters & 3u, (parameters >> 2u) & 7u, (parameters >> 5u) & 7u, config & 0xffu};
validate(tess.inputControlPoints != 0 && tess.inputControlPoints <= 32 && tess.outputControlPoints != 0 && tess.outputControlPoints <= 32, "invalid tessellation control-point counts");
validate(tess.patchesPerGroup != 0, "VGT_LS_HS_CONFIG.NUM_PATCHES is zero");
if ((parameters & ~0x000600ffu) != 0) Require(false, zeroMessage(0x2db, parameters, "VGT_TF_PARAM donut, factor detection, memory type or reserved state"));
validate((tess.domain == 1 || tess.domain == 2) && tess.partitioning == 2 && (tess.outputTopology == 2 || tess.outputTopology == 3), "only triangle and quad domains with fractional-odd partitioning and triangle output are supported by the reference path");
result.minTessellationLevel = readFloat(queue.context, 0x287);

View File

@ -599,7 +599,7 @@ void ShaderStageTests() {
}
Require(stages.path == AgcDriver::Graphics::ShaderPath::Tessellation && stages.tessellation, "tessellation routing changed");
const auto& tessellation = *stages.tessellation;
Require(tessellation.inputControlPoints == 3 && tessellation.outputControlPoints == 3 && tessellation.domain == 1 && tessellation.partitioning == 2 && tessellation.outputTopology == 2 && stages.minTessellationLevel == 1.0f && stages.maxTessellationLevel == 64.0f, "the passthrough triangle-patch configuration decoded differently");
Require(tessellation.inputControlPoints == 3 && tessellation.outputControlPoints == 3 && tessellation.domain == 1 && tessellation.partitioning == 2 && tessellation.outputTopology == 2 && stages.minTessellationLevel == 1.0f && stages.maxTessellationLevel == 64.0f && tessellation.patchesPerGroup == 85, "the passthrough triangle-patch configuration decoded differently");
for (const auto [parameters, reason] : {std::pair{0x44049u, "VGT_TF_PARAM"}, std::pair{0x140049u, "VGT_TF_PARAM"}, std::pair{0x2040049u, "VGT_TF_PARAM"}, std::pair{0x40059u, "fractional-odd partitioning"}, std::pair{0x4004du, "fractional-odd partitioning"}, std::pair{0x40048u, "fractional-odd partitioning"}, std::pair{0x40029u, "fractional-odd partitioning"}, std::pair{0x4004bu, "fractional-odd partitioning"}}) {
queue.context[0x2db] = parameters;
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, reason);
@ -609,7 +609,7 @@ void ShaderStageTests() {
queue.context[0x286] = 0x41100000;
const auto quadStages = AgcDriver::Graphics::DecodeState(queue).stages;
const auto& quads = quadStages.tessellation;
Require(quads && quads->inputControlPoints == 4 && quads->outputControlPoints == 4 && quads->domain == 2 && quads->partitioning == 2 && quads->outputTopology == 3 && quadStages.minTessellationLevel == 1.0f && quadStages.maxTessellationLevel == 9.0f, "counter-clockwise quad patches with HOS levels [1, 9] decoded differently");
Require(quads && quads->inputControlPoints == 4 && quads->outputControlPoints == 4 && quads->domain == 2 && quads->partitioning == 2 && quads->outputTopology == 3 && quadStages.minTessellationLevel == 1.0f && quadStages.maxTessellationLevel == 9.0f && quads->patchesPerGroup == 63, "counter-clockwise quad patches with HOS levels [1, 9] and 63 patches per group decoded differently");
queue.context[0x2db] = 0x40069;
Require(AgcDriver::Graphics::DecodeState(queue).stages.tessellation->outputTopology == 3, "counter-clockwise triangle patches decoded differently");
for (const auto [maximum, minimum, reason] : {std::tuple{0x3f800000u, 0x40000000u, "tessellation level clamps"}, std::tuple{0x41100000u, 0xbf800000u, "tessellation level clamps"}, std::tuple{0x3f000000u, 0x00000000u, "tessellation level clamps"}, std::tuple{0x42800000u, 0x42800000u, "tessellation level clamps"}, std::tuple{0x7fc00000u, 0x3f800000u, "non-finite register"}, std::tuple{0x7f800000u, 0x3f800000u, "non-finite register"}}) {
@ -626,6 +626,8 @@ void ShaderStageTests() {
queue.context[0x2d5] = 0x0200210d;
queue.context[0x2d6] = 0;
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "control-point counts");
queue.context[0x2d6] = 0xc300;
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "NUM_PATCHES is zero");
queue = makeState();
for (const auto value : {0x2003u, 0x2018u, 0x20c0u, 0x80002000u}) {
queue.context[0x2d5] = value;

View File

@ -1686,6 +1686,23 @@ void verifyMeshConfiguration() {
other.graphics = GraphicsCompileContext{0u, {}, otherMesh, std::nullopt, {}};
RecompileCacheKey::Build(other, key);
require(key != first && RecompileCacheKey::ContextHash(request) != RecompileCacheKey::ContextHash(other), "the cache keys ignore the mesh configuration");
RecompileRequest hull{};
hull.shader = {ShaderStage::TessellationControl, 0x10000u, code, 0, {}};
hull.context.waveSize = 64;
hull.graphics = GraphicsCompileContext{0u, {}, std::nullopt, TessellationConfiguration{4u, 4u, 2u, 2u, 3u, 63u}, {}};
const auto hullReplay = RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(hull));
const auto* replayed = hullReplay.request.graphics && hullReplay.request.graphics->tessellation ? &*hullReplay.request.graphics->tessellation : nullptr;
require(replayed != nullptr && replayed->domain == 2u && replayed->outputTopology == 3u && replayed->patchesPerGroup == 63u, "the quad tessellation configuration or its patches per group were lost in serialization");
auto grouped = hull;
grouped.graphics->tessellation->patchesPerGroup = 62u;
for (const auto stage : {ShaderStage::Local, ShaderStage::TessellationControl, ShaderStage::TessellationEvaluation}) {
hull.shader.stage = stage;
grouped.shader.stage = stage;
RecompileCacheKey::Build(hull, key);
const auto patchKey = key;
RecompileCacheKey::Build(grouped, key);
require((key != patchKey) == (stage != ShaderStage::Local), "only the hull and domain cache keys may depend on the patches per group, which bound their relative patch index");
}
const std::array<std::uint32_t, 8> users{};
request.context.waveSize = 32u;
request.context.userData = users;
@ -1826,13 +1843,13 @@ void verifyPixelRequestSerialization() {
minimal.context.pixel = ShaderPixelStageInfo{};
minimal.context.pixel->rasterPrimitive = RasterPrimitive::Rectangle;
const auto encoded = serializer.Serialize(minimal);
require(requestPrefix(encoded, 8u) == "NVNQQRUAAAA=", "new requests did not use serialization version 21");
require(requestPrefix(encoded, 8u) == "NVNQQRYAAAA=", "new requests did not use serialization version 22");
require(serializer.Deserialize(encoded).request.context.pixel->rasterPrimitive == RasterPrimitive::Rectangle, "the pixel primitive class did not round-trip");
constexpr std::size_t mappingOffset = 8u + 37u + 18u + 163u;
for (std::size_t bytes = 0; bytes < 21u; ++bytes) {
expectFailure([&] { static_cast<void>(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source, alpha-to-coverage, stencil reference export flag or primitive class was accepted");
}
for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQRYAAAA="}) {
for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQRcAAAA="}) {
expectFailure([&] { static_cast<void>(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted");
}
static constexpr std::string_view version18Request{

View File

@ -64,6 +64,16 @@ alignas(256) constexpr std::array<std::uint32_t, 29> LocalIndexAsData{
0x0000030a, 0xd8340010, 0x0000070a, 0xbf8cc07f, 0xbefd2106,
};
alignas(256) constexpr std::array<std::uint32_t, 50> HullDescendingRecords{
0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014,
0x02151101, 0x362402ff, 0x000000ff, 0x160028a0, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0000,
0x02000015, 0x162c2890, 0xd5430016, 0x045a24ff, 0x00000030, 0xbf8cc07f, 0xe0781000, 0x02020216,
0x7da82881, 0xbe94047e, 0xb06a0060, 0x162602f9, 0x0086066a, 0xd8d80008, 0x06000013, 0xd8dc1008,
0x07000013, 0xbf8cc07f, 0x7e300306, 0x7e320306, 0x7e340306, 0x7e360306, 0x34142484, 0xbefe0414,
0xe0781000, 0x0403180a, 0x4a162481, 0x161616ff, 0x000000d0, 0x4c1616ff, 0x00008000, 0xe0741000,
0x0202070b, 0xbf810000,
};
alignas(256) constexpr std::array<std::uint32_t, 46> HullUnguardedFactors{
0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014,
0x02151101, 0x362402ff, 0x000000ff, 0x160028a0, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0000,
@ -297,24 +307,24 @@ std::uint32_t CoveredPixels(std::uint32_t patch, bool interior) {
return count;
}
void RequirePatches(AgcDriver::VulkanDevice& device, const ShaderRecompiler::SpirvTarget& target, std::span<const std::uint32_t> hull, const std::string& program) {
void RequirePatches(AgcDriver::VulkanDevice& device, const ShaderRecompiler::SpirvTarget& target, std::span<const std::uint32_t> hull, const std::string& program, const PatchDraw& patches = Triangles) {
std::uint32_t cursor = 0;
const Stages stages{
Compile(target, ShaderStage::Local, LocalCode, cursor),
Compile(target, ShaderStage::TessellationControl, hull, cursor),
Compile(target, ShaderStage::TessellationEvaluation, DomainCode, cursor),
Compile(target, ShaderStage::Fragment, PixelCode, cursor),
Compile(target, ShaderStage::Local, LocalCode, cursor, patches),
Compile(target, ShaderStage::TessellationControl, hull, cursor, patches),
Compile(target, ShaderStage::TessellationEvaluation, DomainCode, cursor, patches),
Compile(target, ShaderStage::Fragment, PixelCode, cursor, patches),
};
const auto center = Row(-0.5f + 1.0f / 3.0f);
for (const auto cull : {VK_CULL_MODE_NONE, VK_CULL_MODE_BACK_BIT}) {
Draw(device, stages, cull, VK_FRONT_FACE_COUNTER_CLOCKWISE);
Draw(device, stages, cull, VK_FRONT_FACE_COUNTER_CLOCKWISE, patches);
const std::string name = program + (cull == VK_CULL_MODE_NONE ? ", unculled" : ", back-face culled");
Require(CoveredPixels(0, false) > 100 && CoveredPixels(0, true) == 0, name + ": the level-1 patch was not drawn as one flat triangle, centre " + PixelText(PixelAt(Column(0, 0.25f), center)));
Require(CoveredPixels(1, false) == 0, name + ": the patch with level 0 was not culled");
Require(CoveredPixels(2, false) > 100 && PixelAt(Column(2, 0.25f), center)[1] > 32, name + ": the level-5 patch has no interior domain points at its centre, " + PixelText(PixelAt(Column(2, 0.25f), center)));
Require(PixelAt(0, 0) == Background && PixelAt(Width - 1, Height - 1) == Background, name + ": the corners changed");
}
Draw(device, stages, VK_CULL_MODE_BACK_BIT, VK_FRONT_FACE_CLOCKWISE);
Draw(device, stages, VK_CULL_MODE_BACK_BIT, VK_FRONT_FACE_CLOCKWISE, patches);
Require(CoveredPixels(0, false) == 0 && CoveredPixels(2, false) == 0, program + ": tessellated triangles did not keep the control points' winding");
}
@ -443,10 +453,13 @@ int main() {
ExpectRefusal(target, ShaderStage::TessellationEvaluation, DomainCulledPrimitive, "passthrough primitive");
ExpectRefusal(target, ShaderStage::Local, LocalIndexAsData, "outside a tessellation ring or LDS address");
ExpectRefusal(target, ShaderStage::TessellationControl, HullCode, "outside its patch's quad factors", {{3u, 3u, 2u, 2u, 2u}, Indices});
ExpectRefusal(target, ShaderStage::TessellationControl, HullDescendingRecords, "not affine");
ExpectRefusal(target, ShaderStage::TessellationControl, HullDescendingRecords, "not affine", {{3u, 3u, 1u, 2u, 2u, 158u}, Indices});
RequirePatches(*device, target, HullCode, "hull program");
RequirePatches(*device, target, HullRestoredExecSaved, "hull program saving its restored exec");
RequirePatches(*device, target, HullDynamicIndex, "hull program reading a control point at a computed index");
RequirePatches(*device, target, HullDescendingRecords, "hull program with descending patch records in 157-patch groups", {{3u, 3u, 1u, 2u, 2u, 157u}, Indices});
RequireCounterClockwise(*device, target);
RequireLevelClamps(*device, target);
RequireQuads(*device, target);

View File

@ -106,6 +106,8 @@ private:
append(key, tessellation != nullptr);
if (tessellation == nullptr) return;
for (const auto value : {tessellation->inputControlPoints, tessellation->outputControlPoints, tessellation->domain, tessellation->partitioning, tessellation->outputTopology}) append(key, value);
if (request.shader.stage == ShaderStage::Local) return;
append(key, tessellation->patchesPerGroup);
}
template<typename TValue>

View File

@ -520,15 +520,17 @@ void writeTessellationConfiguration(Writer& writer, const TessellationConfigurat
writer.WriteU32(configuration.domain);
writer.WriteU32(configuration.partitioning);
writer.WriteU32(configuration.outputTopology);
writer.WriteU32(configuration.patchesPerGroup);
}
TessellationConfiguration readTessellationConfiguration(Reader& reader) {
TessellationConfiguration readTessellationConfiguration(Reader& reader, std::uint32_t version) {
TessellationConfiguration configuration{};
configuration.inputControlPoints = reader.ReadU32();
configuration.outputControlPoints = reader.ReadU32();
configuration.domain = reader.ReadU32();
configuration.partitioning = reader.ReadU32();
configuration.outputTopology = reader.ReadU32();
if (version >= 22u) configuration.patchesPerGroup = reader.ReadU32();
return configuration;
}
@ -731,7 +733,7 @@ GraphicsCompileContext readGraphicsCompileContext(Reader& reader, DeserializedGr
graphics.mesh = storage.mesh;
}
if (reader.ReadBool()) {
storage.tessellation = readTessellationConfiguration(reader);
storage.tessellation = readTessellationConfiguration(reader, version);
graphics.tessellation = storage.tessellation;
}
storage.draw.indexAddress = reader.ReadU64();
@ -748,7 +750,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const
std::string buffer;
Writer writer(buffer);
writer.WriteU32(0x41505335u);
writer.WriteU32(21u);
writer.WriteU32(22u);
writeShaderBinary(writer, request.shader);
writeGuestContext(writer, request.context);
writeSpirvTarget(writer, request.target);
@ -780,7 +782,7 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const
Reader reader(decoded);
if (reader.ReadU32() != 0x41505335u) throw std::runtime_error("invalid recompile request signature");
const auto version = reader.ReadU32();
if (version < 1u || version > 21u) throw std::runtime_error("unsupported recompile request serialization version");
if (version < 1u || version > 22u) throw std::runtime_error("unsupported recompile request serialization version");
DeserializedRequest result{};
result.request.shader = readShaderBinary(reader, result.shaderCode, result.shaderHeader);
result.request.context = readGuestContext(reader, result, version);

View File

@ -112,6 +112,7 @@ struct ShaderTessellationInputInfo {
std::uint32_t domain = 0;
std::uint32_t partitioning = 0;
std::uint32_t outputTopology = 0;
std::uint32_t patchesPerGroup = 256;
};
struct ShaderVertexInputInfo {

View File

@ -17,7 +17,6 @@
namespace ShaderRecompiler {
namespace {
constexpr std::int64_t PatchIndexCount = 256;
constexpr std::int64_t LocalVertexIndexCount = 8192;
constexpr std::int64_t TriangleFactorBytes = 16;
constexpr std::int64_t QuadFactorBytes = 24;
@ -418,7 +417,7 @@ std::uint32_t ComponentCount(IrOpcode opcode) {
class Lowering {
public:
Lowering(IrProgram& program, const ShaderTessellationInputInfo& tessellation)
: program(program), tessellation(tessellation), stage(program.Resources().stage), evaluator(stage == IrShaderStage::TessellationControl ? tessellation.outputControlPoints : 0u, stage == IrShaderStage::Local ? LocalVertexIndexCount : PatchIndexCount) {}
: program(program), tessellation(tessellation), stage(program.Resources().stage), evaluator(stage == IrShaderStage::TessellationControl ? tessellation.outputControlPoints : 0u, stage == IrShaderStage::Local ? LocalVertexIndexCount : static_cast<std::int64_t>(tessellation.patchesPerGroup)) {}
void Run() {
for (IrBlock* block : program.BlockOrder()) {

View File

@ -287,6 +287,7 @@ struct TessellationConfiguration {
std::uint32_t domain;
std::uint32_t partitioning;
std::uint32_t outputTopology;
std::uint32_t patchesPerGroup = 256;
};
inline constexpr std::uint32_t MeshDrawPushOffsetBytes = 104;

View File

@ -181,12 +181,14 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues
if (tessellation == nullptr) throw std::runtime_error("ShaderInputInfoBuilder: tessellation configuration is missing");
if (tessellation->inputControlPoints == 0u || tessellation->inputControlPoints > 32u || tessellation->outputControlPoints == 0u || tessellation->outputControlPoints > 32u) throw std::runtime_error("ShaderInputInfoBuilder: invalid tessellation control-point counts");
if ((tessellation->domain != 1u && tessellation->domain != 2u) || tessellation->partitioning != 2u || (tessellation->outputTopology != 2u && tessellation->outputTopology != 3u)) throw std::runtime_error("ShaderInputInfoBuilder: unsupported tessellation configuration");
if (tessellation->patchesPerGroup == 0u || tessellation->patchesPerGroup > 256u) throw std::runtime_error("ShaderInputInfoBuilder: invalid tessellation patches per group");
auto& target = vertexStorage.tess;
target.inputControlPoints = tessellation->inputControlPoints;
target.outputControlPoints = tessellation->outputControlPoints;
target.domain = tessellation->domain;
target.partitioning = tessellation->partitioning;
target.outputTopology = tessellation->outputTopology;
target.patchesPerGroup = tessellation->patchesPerGroup;
}
if (stage == ShaderStageKind::Mesh) {
if (mesh == nullptr) throw std::runtime_error("ShaderInputInfoBuilder: a mesh-stage program has no mesh configuration");