feat(agc): read hull patch constants in tessellation domain programs

TessellationLowering refused every domain-program read of the off-chip
ring outside its patch's control points, so domain programs that read
the hull's patch constants, like the quad terrain domain in Ghost of
Yotei at 0x8000 - 208 * (rel + 1), aborted.

Such reads now lower to a new PatchInput kind and become Patch inputs.
Hull outputs and domain inputs place a constant by its off-chip address
modulo the record stride, in a record-sized array at location
ceil(hsStride / 16); each program derives both on its own, so the two
interfaces match although they compile separately. Domain reads at an
address that does not move with the relative patch, across more than
one record or inside the control-point region still throw, as does a
domain program that reads patch constants but no control point. Hull
patch-constant writes now take the same bound: above the control points
of all NUM_PATCHES patches, not just one patch's.
This commit is contained in:
Celegans12 2026-10-12 02:06:59 +02:00
parent 25a83722c7
commit 247162dc4f
7 changed files with 115 additions and 46 deletions

View File

@ -74,6 +74,16 @@ alignas(256) constexpr std::array<std::uint32_t, 50> HullDescendingRecords{
0x0202070b, 0xbf810000,
};
alignas(256) constexpr std::array<std::uint32_t, 52> HullPatchConstant{
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, 0xe0701064, 0x0202060b, 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,
@ -122,12 +132,39 @@ alignas(256) constexpr std::array<std::uint32_t, 49> HullAndSaveexec{
0xbf810000,
};
alignas(256) constexpr std::array<std::uint32_t, 36> DomainPatchRead{
alignas(256) constexpr std::array<std::uint32_t, 39> DomainPatchRead{
0xf8000941, 0x00000000, 0x34020e84, 0x16020283, 0xe0341000, 0x04030a01, 0xe0341010, 0x04030c01,
0xe0341020, 0x04030e01, 0x4a060e81, 0x16060690, 0x4c0606ff, 0x00008000, 0xe0341000, 0x04030c03,
0xbf8c3f70, 0x08040af2, 0x08040d02, 0x10201505, 0x3e201906, 0x3e201d02, 0x10221705, 0x3e221b06,
0x3e221f02, 0xd5510012, 0x040a0d05, 0x102424ff, 0x40400000, 0x7e260280, 0x7e2802f2, 0xf80008cf,
0x14131110, 0xf800020f, 0x14131214, 0xbf810000,
0xe0341020, 0x04030e01, 0x4a060e81, 0x160606ff, 0x000000d0, 0x4c0606ff, 0x00008000, 0xe0301064,
0x04031503, 0xbf8c3f70, 0x08040af2, 0x08040d02, 0x10201505, 0x3e201906, 0x3e201d02, 0x10221705,
0x3e221b06, 0x3e221f02, 0xd5510012, 0x040a0d05, 0x102424ff, 0x40400000, 0x102a2aff, 0x3e000000,
0x7e260280, 0x7e2802f2, 0xf80008cf, 0x14131110, 0xf800020f, 0x14151214, 0xbf810000,
};
alignas(256) constexpr std::array<std::uint32_t, 42> DomainNeighbourRecord{
0xf8000941, 0x00000000, 0x34020e84, 0x16020283, 0xe0341000, 0x04030a01, 0xe0341010, 0x04030c01,
0xe0341020, 0x04030e01, 0x4a060e81, 0x160606ff, 0x000000d0, 0x4c0606ff, 0x00008000, 0xe0301064,
0x04031503, 0xe0301134, 0x04031603, 0xbf8c3f70, 0x062a2b16, 0x08040af2, 0x08040d02, 0x10201505,
0x3e201906, 0x3e201d02, 0x10221705, 0x3e221b06, 0x3e221f02, 0xd5510012, 0x040a0d05, 0x102424ff,
0x40400000, 0x102a2aff, 0x3e000000, 0x7e260280, 0x7e2802f2, 0xf80008cf, 0x14131110, 0xf800020f,
0x14151214, 0xbf810000,
};
alignas(256) constexpr std::array<std::uint32_t, 42> DomainFixedPatchRead{
0xf8000941, 0x00000000, 0x34020e84, 0x16020283, 0xe0341000, 0x04030a01, 0xe0341010, 0x04030c01,
0xe0341020, 0x04030e01, 0x4a060e81, 0x160606ff, 0x000000d0, 0x4c0606ff, 0x00008000, 0xe0301064,
0x04031503, 0xe0300064, 0x04031600, 0xbf8c3f70, 0x062a2b16, 0x08040af2, 0x08040d02, 0x10201505,
0x3e201906, 0x3e201d02, 0x10221705, 0x3e221b06, 0x3e221f02, 0xd5510012, 0x040a0d05, 0x102424ff,
0x40400000, 0x102a2aff, 0x3e000000, 0x7e260280, 0x7e2802f2, 0xf80008cf, 0x14131110, 0xf800020f,
0x14151214, 0xbf810000,
};
alignas(256) constexpr std::array<std::uint32_t, 42> DomainNeighbourPoint{
0xf8000941, 0x00000000, 0x34020e84, 0x16020283, 0xe0341000, 0x04030a01, 0xe0341010, 0x04030c01,
0xe0341020, 0x04030e01, 0x4a060e81, 0x160606ff, 0x000000d0, 0x4c0606ff, 0x00008000, 0xe0301064,
0x04031503, 0xe0301030, 0x04031601, 0xbf8c3f70, 0x062a2b16, 0x08040af2, 0x08040d02, 0x10201505,
0x3e201906, 0x3e201d02, 0x10221705, 0x3e221b06, 0x3e221f02, 0xd5510012, 0x040a0d05, 0x102424ff,
0x40400000, 0x102a2aff, 0x3e000000, 0x7e260280, 0x7e2802f2, 0xf80008cf, 0x14131110, 0xf800020f,
0x14151214, 0xbf810000,
};
alignas(256) constexpr std::array<std::uint32_t, 32> DomainCulledPrimitive{
@ -362,6 +399,25 @@ void RequireLevelClamps(AgcDriver::VulkanDevice& device, const ShaderRecompiler:
Require(CoveredPixels(1, false) == 0, "VGT_HOS_MIN_TESS_LEVEL 5 raised the patch with level 0 instead of culling it");
}
const PatchDraw RecordPatches{{3u, 3u, 1u, 2u, 2u, 63u}, Indices};
void RequirePatchConstants(AgcDriver::VulkanDevice& device, const ShaderRecompiler::SpirvTarget& target) {
std::uint32_t cursor = 0;
const Stages stages{
Compile(target, ShaderStage::Local, LocalCode, cursor, RecordPatches),
Compile(target, ShaderStage::TessellationControl, HullPatchConstant, cursor, RecordPatches),
Compile(target, ShaderStage::TessellationEvaluation, DomainPatchRead, cursor, RecordPatches),
Compile(target, ShaderStage::Fragment, PixelCode, cursor, RecordPatches),
};
Draw(device, stages, VK_CULL_MODE_NONE, VK_FRONT_FACE_COUNTER_CLOCKWISE, RecordPatches);
const auto center = Row(-0.5f + 1.0f / 3.0f);
for (const auto& [patch, blue] : {std::pair{0u, 32}, std::pair{2u, 159}}) {
const auto pixel = PixelAt(Column(patch, 0.25f), center);
Require(pixel[0] == 255 && std::abs(pixel[2] - blue) <= 1, "patch " + std::to_string(patch) + " did not take its blue channel from the patch constant its hull wrote, " + PixelText(pixel));
}
Require(PixelAt(Column(1, 0.25f), center) == Background, "the patch with level 0 was not culled");
}
void RequireQuads(AgcDriver::VulkanDevice& device, const ShaderRecompiler::SpirvTarget& target) {
const auto center = Row(0.0f);
std::array<bool, 2> frontFacing{};
@ -449,20 +505,24 @@ int main() {
ExpectRefusal(target, ShaderStage::TessellationControl, HullSquaredAddress, "not a whole number of control points");
ExpectRefusal(target, ShaderStage::TessellationControl, HullHalfSlotIndex, "not a whole number of control points");
ExpectRefusal(target, ShaderStage::TessellationControl, HullAndSaveexec, "lane mask");
ExpectRefusal(target, ShaderStage::TessellationEvaluation, DomainPatchRead, "patch constants");
ExpectRefusal(target, ShaderStage::TessellationEvaluation, DomainNeighbourRecord, "more than one patch-constant record", RecordPatches);
ExpectRefusal(target, ShaderStage::TessellationEvaluation, DomainFixedPatchRead, "not indexed by its relative patch", RecordPatches);
ExpectRefusal(target, ShaderStage::TessellationEvaluation, DomainNeighbourPoint, "neither its patch's control points nor a patch-constant record", RecordPatches);
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});
ExpectRefusal(target, ShaderStage::TessellationControl, HullDescendingRecords, "outside its control points and patch constants", {{3u, 3u, 1u, 2u, 2u, 129u}, 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});
RequirePatches(*device, target, HullDescendingRecords, "hull program with descending patch records in 128-patch groups", {{3u, 3u, 1u, 2u, 2u, 128u}, Indices});
RequireCounterClockwise(*device, target);
RequireLevelClamps(*device, target);
RequireQuads(*device, target);
RequirePatchConstants(*device, target);
device.reset();
RequireRegisteredStages();
std::puts("Tessellation tests passed");

View File

@ -30,6 +30,7 @@ struct IrProgramMetadata {
std::uint32_t vectorWindowSize = 0;
bool vectorWindowIndexedWrites = false;
std::uint32_t tessellationControlStride = 0;
std::uint32_t tessellationPatchStride = 0;
};
}

View File

@ -13,7 +13,8 @@ enum class TessellationAttribute {
ControlOutput,
EvaluationInput,
PatchOutput,
Factor
Factor,
PatchInput
};
enum class TessellationBaseKind {

View File

@ -449,6 +449,7 @@ public:
deadCodeEliminator.Eliminate(program);
program.Metadata().tessellationLocalStride = localStride;
program.Metadata().tessellationControlStride = controlStride;
program.Metadata().tessellationPatchStride = patchStride;
}
private:
@ -512,19 +513,29 @@ private:
void derive() {
const auto inputs = static_cast<std::int64_t>(tessellation.inputControlPoints);
const auto outputs = static_cast<std::int64_t>(tessellation.outputControlPoints);
for (const auto& access : accesses) {
for (auto& access : accesses) {
if (access.kind == TessellationAttribute::LocalOutput) agree(localStride, access.address.relative, access.pc);
if (access.kind == TessellationAttribute::ControlInput && access.address.vertex != 0) agree(localStride, access.address.vertex, access.pc);
if (access.kind == TessellationAttribute::ControlInput && access.address.dynamic && access.address.relative > 0 && access.address.relative % inputs == 0) agree(localStride, access.address.relative / inputs, access.pc);
if (access.kind == TessellationAttribute::ControlOutput && access.address.vertex != 0) agree(controlStride, access.address.vertex, access.pc);
if (access.kind == TessellationAttribute::EvaluationInput) {
if (access.address.relative <= 0 || access.address.relative % outputs != 0) Fail("domain shader reads of patch constants or other patches are unsupported", access.pc);
agree(controlStride, access.address.relative / outputs, access.pc);
const auto end = access.address.constant + 4 * static_cast<std::int64_t>(access.components);
if (access.address.relative > 0 && access.address.relative % outputs == 0 && access.address.constant >= 0 && end <= access.address.relative) {
agree(controlStride, access.address.relative / outputs, access.pc);
} else {
access.kind = TessellationAttribute::PatchInput;
}
}
}
std::optional<std::int64_t> patchStride;
std::optional<std::int64_t> recordStride;
std::int64_t patchLowest = AddressLimit;
std::int64_t patchHighest = 0;
const auto record = [&](const Access& access, std::int64_t end) {
if (recordStride && *recordStride != access.address.relative) Fail("inconsistent patch-constant stride", access.pc);
recordStride = access.address.relative;
patchLowest = std::min(patchLowest, access.address.constant);
patchHighest = std::max(patchHighest, end);
};
for (auto& access : accesses) {
const auto& address = access.address;
const auto end = address.constant + 4 * static_cast<std::int64_t>(access.components);
@ -539,11 +550,8 @@ private:
break;
case TessellationAttribute::ControlOutput:
if (address.vertex == 0) {
if (controlStride == 0u || address.constant < outputs * controlStride) Fail("the hull shader writes off-chip memory outside its control points and patch constants", access.pc);
if (patchStride && *patchStride != address.relative) Fail("inconsistent patch-constant stride", access.pc);
patchStride = address.relative;
patchLowest = std::min(patchLowest, address.constant);
patchHighest = std::max(patchHighest, end);
if (controlStride == 0u || evaluator.Lowest(address) < static_cast<std::int64_t>(tessellation.patchesPerGroup) * outputs * controlStride) Fail("the hull shader writes off-chip memory outside its control points and patch constants", access.pc);
record(access, end);
access.kind = TessellationAttribute::PatchOutput;
requireFirstInvocation(access);
} else if (address.relative != outputs * controlStride || address.constant < 0 || end > controlStride) {
@ -560,11 +568,19 @@ private:
requireFirstInvocation(access);
break;
}
case TessellationAttribute::PatchInput:
if (controlStride == 0u) Fail("the domain shader reads patch constants but no control point, so their locations are unknown", access.pc);
if (address.relative == 0) Fail("the domain shader reads off-chip memory that is not indexed by its relative patch", access.pc);
if (evaluator.Lowest(address) < static_cast<std::int64_t>(tessellation.patchesPerGroup) * outputs * controlStride) Fail("the domain shader reads off-chip memory that is neither its patch's control points nor a patch-constant record", access.pc);
record(access, end);
break;
case TessellationAttribute::PatchOutput:
break;
}
}
if (patchStride && (*patchStride == 0 || std::abs(*patchStride) < patchHighest - patchLowest)) Fail("patch-constant records of neighbouring patches overlap", 0u);
if (!recordStride) return;
if (*recordStride == 0 || std::abs(*recordStride) < patchHighest - patchLowest) Fail(stage == IrShaderStage::TessellationEvaluation ? "the domain shader reads more than one patch-constant record" : "patch-constant records of neighbouring patches overlap", 0u);
patchStride = static_cast<std::uint32_t>(std::abs(*recordStride));
}
bool dynamicInputAddress(IrValue* user, IrValue* value) {
@ -625,7 +641,9 @@ private:
IrValue& componentAddress(const Access& access, std::uint32_t component) {
if (access.address.dynamic) return insert(access, IrOpcode::IAdd32, IrType::U32, {access.addressValue, &constant(access.memoryOffset + 4u * component)});
IrValue& offset = constant(static_cast<std::uint32_t>(access.address.constant) + 4u * component);
auto address = access.address.constant + 4 * static_cast<std::int64_t>(component);
if (access.kind == TessellationAttribute::PatchOutput || access.kind == TessellationAttribute::PatchInput) address -= FloorDiv(address, patchStride) * patchStride;
IrValue& offset = constant(static_cast<std::uint32_t>(address));
if (access.kind != TessellationAttribute::ControlInput || access.address.vertex == 0) return offset;
IrValue& invocation = insert(access, IrOpcode::GetBuiltin, IrType::U32, {&constant(static_cast<std::uint32_t>(StageInputKind::InvocationId)), &constant(0u)});
IrValue& scaled = insert(access, IrOpcode::IMul32, IrType::U32, {&invocation, &constant(static_cast<std::uint32_t>(access.address.vertex))});
@ -690,6 +708,7 @@ private:
std::vector<Access> accesses;
std::uint32_t localStride = 0;
std::uint32_t controlStride = 0;
std::uint32_t patchStride = 0;
};
}

View File

@ -81,9 +81,8 @@ struct SpirvEmitterState {
SpirvModule module;
const IrProgram& program;
ShaderStageInputInfo inputInfo;
std::array<std::uint32_t, 6> tessVariables {};
std::array<std::uint32_t, 7> tessVariables {};
std::uint32_t tessInnerVariable = 0;
std::uint32_t tessPatchBase = 0;
SpirvRequirements requirements;
std::uint32_t laneCount = 1;

View File

@ -516,7 +516,7 @@ std::uint32_t TessellationPointer(SpirvValueEmitContext& ctx, const IrValue& ins
if (variable == 0u) {
throw std::runtime_error("tessellation attribute has no interface variable");
}
const bool input = kind == TessellationAttribute::ControlInput || kind == TessellationAttribute::EvaluationInput;
const bool input = kind == TessellationAttribute::ControlInput || kind == TessellationAttribute::EvaluationInput || kind == TessellationAttribute::PatchInput;
const auto storage = input ? spv::StorageClassInput : spv::StorageClassOutput;
const auto pointer = state.module.AllocateId();
if (kind == TessellationAttribute::Factor) {
@ -532,10 +532,8 @@ std::uint32_t TessellationPointer(SpirvValueEmitContext& ctx, const IrValue& ins
state.module.AddFunction(spv::OpAccessChain, TypePointer(state, storage, TypeF32(state)), pointer, outer ? variable : state.tessInnerVariable, ConstantU32(state, outer ? index : index - outerFactors));
return pointer;
}
auto address = ctx.Arg(inst, 1);
if (kind == TessellationAttribute::PatchOutput) {
address = EmitBinaryU32(state, spv::OpISub, address, ConstantU32(state, state.tessPatchBase));
}
const auto address = ctx.Arg(inst, 1);
const bool patch = kind == TessellationAttribute::PatchOutput || kind == TessellationAttribute::PatchInput;
const bool local = kind == TessellationAttribute::LocalOutput || kind == TessellationAttribute::ControlInput;
const auto stride = local ? state.program.Metadata().tessellationLocalStride : state.program.Metadata().tessellationControlStride;
const auto type = TypePointer(state, storage, TypeU32(state));
@ -553,10 +551,10 @@ std::uint32_t TessellationPointer(SpirvValueEmitContext& ctx, const IrValue& ins
state.module.AddFunction(spv::OpAccessChain, type, pointer, variable, vertex, ConstantU32(state, constant % stride / 16u), ConstantU32(state, constant % 16u / 4u));
return pointer;
}
const auto offset = kind == TessellationAttribute::PatchOutput ? address : EmitBinaryU32(state, spv::OpUMod, address, ConstantU32(state, stride));
const auto offset = patch ? address : EmitBinaryU32(state, spv::OpUMod, address, ConstantU32(state, stride));
const auto attribute = EmitBinaryU32(state, spv::OpShiftRightLogical, offset, ConstantU32(state, 4u));
const auto component = EmitBinaryU32(state, spv::OpBitwiseAnd, EmitBinaryU32(state, spv::OpShiftRightLogical, offset, ConstantU32(state, 2u)), ConstantU32(state, 3u));
if (kind == TessellationAttribute::LocalOutput || kind == TessellationAttribute::PatchOutput) {
if (kind == TessellationAttribute::LocalOutput || patch) {
state.module.AddFunction(spv::OpAccessChain, type, pointer, variable, attribute, component);
} else {
const auto vertex = kind == TessellationAttribute::ControlOutput ? EmitLaneId(state) : inPatch(EmitBinaryU32(state, spv::OpUDiv, address, ConstantU32(state, stride)));
@ -568,23 +566,13 @@ std::uint32_t TessellationPointer(SpirvValueEmitContext& ctx, const IrValue& ins
}
void DefineTessellationInterfaces(SpirvEmitterState& state) {
std::array<bool, 6> used {};
std::uint32_t patchBegin = std::numeric_limits<std::uint32_t>::max();
std::uint32_t patchEnd = 0u;
std::array<bool, 7> used {};
for (const IrBlock* block : state.program.BlockOrder()) {
for (const IrValue* inst : block->Instructions()) {
if (inst->Opcode() != IrOpcode::GetTessellationAttribute && inst->Opcode() != IrOpcode::SetTessellationAttribute) {
continue;
}
const auto kind = inst->Argument(0)->ImmediateU32();
used.at(kind) = true;
if (kind == static_cast<std::uint32_t>(TessellationAttribute::PatchOutput)) {
if (!inst->Argument(1)->HasImmediate()) {
throw std::runtime_error("tessellation patch output offset must be immediate");
}
patchBegin = std::min(patchBegin, inst->Argument(1)->ImmediateU32());
patchEnd = std::max(patchEnd, inst->Argument(1)->ImmediateU32() + 4u);
}
used.at(inst->Argument(0)->ImmediateU32()) = true;
}
}
if (std::none_of(used.begin(), used.end(), [](bool value) { return value; })) {
@ -599,14 +587,14 @@ void DefineTessellationInterfaces(SpirvEmitterState& state) {
continue;
}
const auto kind = static_cast<TessellationAttribute>(index);
const bool input = kind == TessellationAttribute::ControlInput || kind == TessellationAttribute::EvaluationInput;
const bool input = kind == TessellationAttribute::ControlInput || kind == TessellationAttribute::EvaluationInput || kind == TessellationAttribute::PatchInput;
const bool patch = kind == TessellationAttribute::PatchOutput || kind == TessellationAttribute::PatchInput;
const auto storage = input ? spv::StorageClassInput : spv::StorageClassOutput;
std::uint32_t type;
if (kind == TessellationAttribute::Factor) {
type = array(TypeF32(state), 4u);
} else if (kind == TessellationAttribute::PatchOutput) {
state.tessPatchBase = patchBegin;
type = array(TypeU32Vector(state, 4u), (patchEnd - patchBegin + 15u) / 16u);
} else if (patch) {
type = array(TypeU32Vector(state, 4u), (state.program.Metadata().tessellationPatchStride + 15u) / 16u);
} else {
const bool local = kind == TessellationAttribute::LocalOutput || kind == TessellationAttribute::ControlInput;
const auto stride = local ? state.program.Metadata().tessellationLocalStride : state.program.Metadata().tessellationControlStride;
@ -623,9 +611,9 @@ void DefineTessellationInterfaces(SpirvEmitterState& state) {
state.module.AddAnnotation(spv::OpDecorate, state.tessInnerVariable, spv::DecorationBuiltIn, spv::BuiltInTessLevelInner);
state.module.AddAnnotation(spv::OpDecorate, state.tessInnerVariable, spv::DecorationPatch);
} else {
state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationLocation, kind == TessellationAttribute::PatchOutput ? (state.program.Metadata().tessellationControlStride + 15u) / 16u : 0u);
state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationLocation, patch ? (state.program.Metadata().tessellationControlStride + 15u) / 16u : 0u);
}
if (kind == TessellationAttribute::Factor || kind == TessellationAttribute::PatchOutput) {
if (kind == TessellationAttribute::Factor || patch) {
state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationPatch);
}
}

View File

@ -84,6 +84,7 @@
- [Hull programs](../../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) start from the hull launch registers (user data, ring bases, v0, v1); on the console they start with what the local program leaves, and that it leaves them unchanged is not checked.
- [Quad tessellation factors](../../../core/shader/recompiler/Optimization/src/TessellationLowering.cpp) are taken as outer[0..3] then inner[0..1], 24 bytes per patch, as [Mesa stores them](https://github.com/mihawk-99/PS5_Mesa/blob/b3588f78dd7fd46fbadc7ecd487e5f14d4213e4b/src/amd/common/nir/ac_nir_lower_tess_io_to_mem.c); unmeasured on the console.
- [VGT_HOS_MIN/MAX_TESS_LEVEL](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) clamp positive tessellation factors (inner ones to at least 1); outer factors that are zero, negative or NaN still cull the patch and such inner factors become max(HOS_MIN, 1). The console's clamp and cull order is not measured.
- [Patch constants](../../../core/shader/recompiler/Optimization/src/TessellationLowering.cpp) that hull programs write and domain programs read are placed by their off-chip address modulo the record stride, as the two compile apart; a domain read of a field the hull never writes, or of only a neighbouring patch's record, is not detected.
- The [output modifier](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) (`mul:2`, `mul:4`, `div:2`) follows the shader's float mode as measured on RDNA2 (gfx1035): the hardware applies it only with IEEE mode off and output denormals of the result's precision flushed (bit 1 of the `FLOAT_MODE` denormal field clear), and ignores it otherwise. On f32 results it is applied as the hardware does with f32 denormals flushed; a shader without a known float mode is translated with IEEE mode off and f32 denormals flushed, the mode titles use. On f16 and f64 results it is ignored, and it throws in the modes where the hardware applies it.
- The [clamp modifier](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follows `DX10_CLAMP`, measured on RDNA2 (gfx1035) for f32, f16, packed f16 and f64 results at IEEE=0 and IEEE=1: with `DX10_CLAMP`=1 (and in shaders without a known float mode) a NaN result clamps to +0, including the quieted NaN that `v_pk_min_f16` and `v_pk_max_f16` return for a signaling NaN operand with IEEE=1 (measured on RDNA2 gfx1036 at both `IEEE_MODE` and both `DX10_CLAMP` settings); with `DX10_CLAMP`=0 a NaN result is written unchanged and every other value clamps to [0, 1] as before. f64 results follow the same rule on hardware, but a clamped f64 result with `DX10_CLAMP`=0 throws until it has an execution test. With `DX10_CLAMP`=0 the NaN's bits are those the translation already produces for the instruction without clamp, so where those are left to the driver (f32 and scalar f16 arithmetic) the payload is the driver's, not the hardware's input propagation. `v_dot2_f32_f16` and the integer clamps are unchanged.
- [FP16_OVFL](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follows the shader's float mode as measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1 with f16 denormals kept and flushed: with `FP16_OVFL`=1 an f16 result that rounds to ±infinity is written as ±0x7bff unless a source operand is infinite, so `v_rcp_f16`, `v_rsq_f16` and `v_log_f16` of ±0 and `v_exp_f16` of 16 or more are clamped while an infinite input passes through, and `v_div_fixup_f16` keeps the infinity it writes for a zero denominator or an infinite numerator and clamps any other infinite quotient, a NaN one included. `v_cvt_pkrtz_f16_f32` rounds toward zero and never overflows. The sources are checked as the f32 values the translation computes with; the rule is applied to `v_fma_mix*_f16` with f32 sources and to `v_interp_p2_f16` without a measurement of infinite f32 sources or parameters. Round modes other than nearest even are not modelled, so the overflow threshold is the nearest-even one (65520).