[Vulkan] Fix adaptive tessellation issues

The domain shader reads the patch index from the hull shader output instead of gl_PrimitiveID, which bypassed the endian swap, offset, wrap and clamp already applied upstream.

The tessellator winds clockwise now. Clip space Y is not flipped on Vulkan, so counterclockwise winding inverted the facing and guest backface culling removed whole surfaces.

Co-authored-by: Herman S. <429230+has207@users.noreply.github.com>
This commit is contained in:
goldislead
2026-08-01 15:12:00 -07:00
committed by Radosław Gliński
parent aed81ca93a
commit 66779fb873
5 changed files with 173 additions and 46 deletions

View File

@@ -119,7 +119,7 @@ void SpirvShaderTranslator::Reset() {
// Vertex shader inputs.
input_vertex_index_ = spv::NoResult;
// Tessellation evaluation shader inputs.
input_primitive_id_ = spv::NoResult;
input_control_point_index_ = spv::NoResult;
input_tess_coord_ = spv::NoResult;
// Pixel shader inputs.
input_point_coordinates_ = spv::NoResult;
@@ -795,16 +795,22 @@ std::vector<uint8_t> SpirvShaderTranslator::CompleteTranslation() {
assert_unhandled_case(host_type);
break;
}
// Tessellation spacing - fractional_even for continuous mode, equal
// (integer) for discrete mode. The actual mode is determined by the TCS
// (hull shader), but we use fractional_even here as the default since it
// provides smooth results. The TCS sets the actual tessellation levels.
// For now, use fractional_even as it's more compatible.
// Tessellation spacing. In SPIR-V the spacing is part of the domain
// shader rather than the hull shader. Match the Direct3D 12 hull shader
// partitioning. Integer (equal) for discrete, fractional even for
// continuous and adaptive.
builder_->addExecutionMode(function_main_,
spv::ExecutionModeSpacingFractionalEven);
// Vertex ordering - counter-clockwise (Vulkan default for front face).
shader_modification.vertex.tessellation_mode ==
xenos::TessellationMode::kDiscrete
? spv::ExecutionModeSpacingEqual
: spv::ExecutionModeSpacingFractionalEven);
// Vertex ordering. Xenia does not flip the clip space Y on Vulkan
// (origin_bottom_left is false, so ndc_scale.y keeps the guest sign), so
// the tessellator must wind the same way as the Direct3D 12 hull shaders,
// which use triangle_cw. Counter-clockwise here inverts the facing and
// the guest backface culling removes the whole surface.
builder_->addExecutionMode(function_main_,
spv::ExecutionModeVertexOrderCcw);
spv::ExecutionModeVertexOrderCw);
} else {
execution_model = spv::ExecutionModelVertex;
}
@@ -1273,11 +1279,33 @@ void SpirvShaderTranslator::EnsureBuildPointAvailable() {
void SpirvShaderTranslator::StartVertexOrTessEvalShaderBeforeMain() {
// Create the inputs.
if (IsSpirvTessEvalShader()) {
input_primitive_id_ = builder_->createVariable(
spv::NoPrecision, spv::StorageClassInput, type_int_, "gl_PrimitiveID");
builder_->addDecoration(input_primitive_id_, spv::DecorationBuiltIn,
static_cast<int>(spv::BuiltIn::PrimitiveId));
main_interface_.push_back(input_primitive_id_);
// Per-control-point index input from the hull shader, mirroring the control
// point input read by the Direct3D 12 domain shader. The hull shader has
// already applied the endian swap, the vertex index offset, the low 24-bit
// wrap and the min/max clamp, so this is the index the guest expects rather
// than the raw gl_PrimitiveID. The array size matches the hull shader's
// output control point count for the domain type.
uint32_t control_point_count = 1;
switch (GetSpirvShaderModification().vertex.host_vertex_shader_type) {
case Shader::HostVertexShaderType::kTriangleDomainCPIndexed:
control_point_count = 3;
break;
case Shader::HostVertexShaderType::kQuadDomainCPIndexed:
control_point_count = 4;
break;
default:
// Patch-indexed (and line) domains output a single control point.
control_point_count = 1;
break;
}
input_control_point_index_ = builder_->createVariable(
spv::NoPrecision, spv::StorageClassInput,
builder_->makeArrayType(
type_float_, builder_->makeUintConstant(control_point_count), 0),
"xe_in_control_point_index");
builder_->addDecoration(input_control_point_index_, spv::DecorationLocation,
0);
main_interface_.push_back(input_control_point_index_);
// Tessellation coordinates (barycentric coordinates for the tessellated
// vertex within the patch).
@@ -1562,32 +1590,56 @@ void SpirvShaderTranslator::StartVertexOrTessEvalShaderInMain() {
break;
}
case Shader::HostVertexShaderType::kQuadDomainCPIndexed: {
// Quad domain requires at least 2 registers (r0 for domain location,
// r1 for control point indices).
// Quad domain requires at least 2 registers (r0 for the domain
// location and the first control point index, r1 for the other
// three).
assert_true(register_count() >= 2);
// Quad domain CP-indexed: gl_TessCoord.xy -> r0.yz, r0.x = 0, r0.w =
// 1 XY swizzle according to the ground shader in 4D5307F2.
uint_vector_temp_.clear();
uint_vector_temp_.push_back(0); // x -> r0.y
uint_vector_temp_.push_back(1); // y -> r0.z
spv::Id tess_coord_xy = builder_->createRvalueSwizzle(
spv::NoPrecision, type_float2_, tess_coord, uint_vector_temp_);
// Store to r0
// Quad domain CP-indexed, matching the Direct3D 12 domain shader:
// r0.xy = domain location, r0.z = control point index 0,
// r1.xyz = control point indices 1, 2, 3 (already endian swapped and
// converted to float by the host vertex and hull shaders).
spv::Id tess_coord_x =
builder_->createCompositeExtract(tess_coord, type_float_, 0);
spv::Id tess_coord_y =
builder_->createCompositeExtract(tess_coord, type_float_, 1);
// Load control point index 0.
id_vector_temp_.clear();
id_vector_temp_.push_back(const_int_0_);
spv::Id control_point_index_0 = builder_->createLoad(
builder_->createAccessChain(spv::StorageClassInput,
input_control_point_index_,
id_vector_temp_),
spv::NoPrecision);
// Store r0 = (domain.x, domain.y, control point index 0, 0).
id_vector_temp_.clear();
id_vector_temp_.push_back(const_int_0_);
spv::Id r0_ptr = builder_->createAccessChain(
spv::StorageClassFunction, var_main_registers_, id_vector_temp_);
// Build float4 with x=0, yz from tess coord, w=1
id_vector_temp_.clear();
id_vector_temp_.push_back(tess_coord_x);
id_vector_temp_.push_back(tess_coord_y);
id_vector_temp_.push_back(control_point_index_0);
id_vector_temp_.push_back(const_float_0_);
id_vector_temp_.push_back(
builder_->createCompositeExtract(tess_coord_xy, type_float_, 0));
id_vector_temp_.push_back(
builder_->createCompositeExtract(tess_coord_xy, type_float_, 1));
id_vector_temp_.push_back(const_float_1_);
builder_->createStore(
builder_->createCompositeConstruct(type_float4_, id_vector_temp_),
r0_ptr);
// Store r1.xyz = control point indices 1, 2, 3.
for (uint32_t i = 1; i <= 3; ++i) {
id_vector_temp_.clear();
id_vector_temp_.push_back(builder_->makeIntConstant(int(i)));
spv::Id control_point_index = builder_->createLoad(
builder_->createAccessChain(spv::StorageClassInput,
input_control_point_index_,
id_vector_temp_),
spv::NoPrecision);
id_vector_temp_.clear();
id_vector_temp_.push_back(builder_->makeIntConstant(1));
id_vector_temp_.push_back(builder_->makeIntConstant(int(i - 1)));
builder_->createStore(control_point_index,
builder_->createAccessChain(
spv::StorageClassFunction,
var_main_registers_, id_vector_temp_));
}
break;
}
case Shader::HostVertexShaderType::kQuadDomainPatchIndexed: {
@@ -1602,11 +1654,17 @@ void SpirvShaderTranslator::StartVertexOrTessEvalShaderInMain() {
uint_vector_temp_.push_back(1); // y -> r0.z
spv::Id tess_coord_xy = builder_->createRvalueSwizzle(
spv::NoPrecision, type_float2_, tess_coord, uint_vector_temp_);
// Load primitive ID (patch index) and convert to float.
spv::Id primitive_id =
builder_->createLoad(input_primitive_id_, spv::NoPrecision);
spv::Id patch_index_float = builder_->createUnaryOp(
spv::OpConvertSToF, type_float_, primitive_id);
// Read the patch index from control point 0 (already endian swapped,
// offset, wrapped and clamped by the host vertex and hull shaders),
// matching the Direct3D 12 domain shader, rather than using the raw
// gl_PrimitiveID.
id_vector_temp_.clear();
id_vector_temp_.push_back(const_int_0_);
spv::Id patch_index_float = builder_->createLoad(
builder_->createAccessChain(spv::StorageClassInput,
input_control_point_index_,
id_vector_temp_),
spv::NoPrecision);
// Store to r0: x = patch index, yz = tess coord, w = 1
id_vector_temp_.clear();
id_vector_temp_.push_back(const_int_0_);
@@ -1652,11 +1710,17 @@ void SpirvShaderTranslator::StartVertexOrTessEvalShaderInMain() {
if (register_count() >= 2) {
if (host_type ==
Shader::HostVertexShaderType::kTriangleDomainPatchIndexed) {
// Load primitive ID (patch index) and convert to float.
spv::Id primitive_id =
builder_->createLoad(input_primitive_id_, spv::NoPrecision);
spv::Id patch_index_float = builder_->createUnaryOp(
spv::OpConvertSToF, type_float_, primitive_id);
// Read the patch index from control point 0 (already endian swapped,
// offset, wrapped and clamped by the host vertex and hull shaders),
// matching the Direct3D 12 domain shader, rather than using the raw
// gl_PrimitiveID.
id_vector_temp_.clear();
id_vector_temp_.push_back(const_int_0_);
spv::Id patch_index_float = builder_->createLoad(
builder_->createAccessChain(spv::StorageClassInput,
input_control_point_index_,
id_vector_temp_),
spv::NoPrecision);
// Store patch index to r1.x
id_vector_temp_.clear();
id_vector_temp_.push_back(builder_->makeIntConstant(1));
@@ -1675,6 +1739,27 @@ void SpirvShaderTranslator::StartVertexOrTessEvalShaderInMain() {
builder_->createAccessChain(
spv::StorageClassFunction,
var_main_registers_, id_vector_temp_));
} else if (host_type ==
Shader::HostVertexShaderType::kTriangleDomainCPIndexed) {
// Store the three control point indices (already endian swapped and
// converted to float by the host vertex and hull shaders) to r1.xyz,
// matching the Direct3D 12 domain shader.
for (uint32_t i = 0; i < 3; ++i) {
id_vector_temp_.clear();
id_vector_temp_.push_back(builder_->makeIntConstant(int(i)));
spv::Id control_point_index = builder_->createLoad(
builder_->createAccessChain(spv::StorageClassInput,
input_control_point_index_,
id_vector_temp_),
spv::NoPrecision);
id_vector_temp_.clear();
id_vector_temp_.push_back(builder_->makeIntConstant(1));
id_vector_temp_.push_back(builder_->makeIntConstant(int(i)));
builder_->createStore(control_point_index,
builder_->createAccessChain(
spv::StorageClassFunction,
var_main_registers_, id_vector_temp_));
}
}
}
} else if (IsSpirvVertexShader()) {

View File

@@ -34,7 +34,7 @@ class SpirvShaderTranslator : public ShaderTranslator {
// TODO(Triang3l): Change to 0xYYYYMMDD once it's out of the rapid
// prototyping stage (easier to do small granular updates with an
// incremental counter).
static constexpr uint32_t kVersion = 17;
static constexpr uint32_t kVersion = 18;
enum class DepthStencilMode : uint32_t {
kNoModifiers,
@@ -86,6 +86,11 @@ class SpirvShaderTranslator : public ShaderTranslator {
// cull distance written after the user clip plane cull distances. The
// "or" operator sets the position to NaN instead and needs no bit here.
uint32_t vertex_kill_and : 1;
// The tessellation mode for domain shaders, selecting the tessellation
// evaluation shader spacing (Direct3D 12 sets it in the hull shaders, but
// in SPIR-V the spacing lives in the domain shader). Discrete uses equal
// spacing, continuous and adaptive use fractional even.
xenos::TessellationMode tessellation_mode : 2;
} vertex;
struct PixelShaderModification {
// uint32_t 0.
@@ -1024,8 +1029,9 @@ class SpirvShaderTranslator : public ShaderTranslator {
// VS as VS only - int.
spv::Id input_vertex_index_;
// VS as TES only - int.
spv::Id input_primitive_id_;
// VS as TES only - per-control-point float array carrying the patch/control
// point index computed by the host vertex and hull shaders.
spv::Id input_control_point_index_;
// VS as TES only - float3 (barycentric coordinates).
spv::Id input_tess_coord_;
// PS, only when needed - float2.

View File

@@ -1402,6 +1402,17 @@ void VulkanCommandProcessor::WriteRegister(uint32_t index, uint32_t value) {
texture_cache_->TextureFetchConstantWritten(
(index - XE_GPU_REG_SHADER_CONSTANT_FETCH_00_0) / 6);
}
} else if (index == XE_GPU_REG_VGT_MAX_VTX_INDX ||
index == XE_GPU_REG_VGT_MIN_VTX_INDX ||
index == XE_GPU_REG_VGT_INDX_OFFSET ||
index == XE_GPU_REG_VGT_DMA_SIZE ||
index == XE_GPU_REG_VGT_HOS_MAX_TESS_LEVEL ||
index == XE_GPU_REG_VGT_HOS_MIN_TESS_LEVEL) {
// Source registers for the tessellation constant buffer. Invalidate it so
// the factor range and index parameters are refreshed per draw instead of
// staying stale from the first draw of the submission.
current_constant_buffers_up_to_date_ &=
~(UINT32_C(1) << SpirvShaderTranslator::kConstantBufferTessellation);
}
}
void VulkanCommandProcessor::WriteRegistersFromMem(uint32_t start_index,
@@ -2749,6 +2760,18 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type,
primitive_processing_result.host_vertex_shader_type ==
Shader::HostVertexShaderType::kVertex;
// The tessellation vertex index endian is a per-draw value from the
// primitive processor rather than a register, kNone for auto draws whose
// factors were already converted on the host. A change between draws must
// invalidate the tessellation constant buffer explicitly.
if (current_tessellation_index_endian_ !=
primitive_processing_result.host_shader_index_endian) {
current_tessellation_index_endian_ =
primitive_processing_result.host_shader_index_endian;
current_constant_buffers_up_to_date_ &=
~(UINT32_C(1) << SpirvShaderTranslator::kConstantBufferTessellation);
}
// Update system constants before uploading them.
UpdateSystemConstantValues(
primitive_polygonal, primitive_processing_result, shader_32bit_index_dma,
@@ -5490,10 +5513,13 @@ bool VulkanCommandProcessor::UpdateBindings(const VulkanShader* vertex_shader,
tessellation_constants.tessellation_factor_range[1] = tess_factor_max;
tessellation_constants.padding0[0] = 0.0f;
tessellation_constants.padding0[1] = 0.0f;
// Vertex index processing parameters for tessellation shaders.
auto vgt_dma_size = regs.Get<reg::VGT_DMA_SIZE>();
// Vertex index processing parameters for tessellation shaders. The
// endian comes from the primitive processor, not raw
// VGT_DMA_SIZE.swap_mode. Auto draws read factors the host has already
// byte swapped, so swapping them again in the hull shader corrupted every
// adaptive factor.
tessellation_constants.vertex_index_endian =
static_cast<uint32_t>(vgt_dma_size.swap_mode);
static_cast<uint32_t>(current_tessellation_index_endian_);
tessellation_constants.vertex_index_offset =
regs[XE_GPU_REG_VGT_INDX_OFFSET];
tessellation_constants.vertex_index_min_max[0] =

View File

@@ -788,6 +788,10 @@ class VulkanCommandProcessor final : public CommandProcessor {
// Whether up-to-date data has been written to constant (uniform) buffers, and
// the buffer infos in current_constant_buffer_infos_ point to them.
uint32_t current_constant_buffers_up_to_date_;
// The index endian the tessellation constant buffer was filled with, from the
// primitive processor for the current draw. Not a register, so changes
// between draws invalidate the buffer separately from WriteRegister.
xenos::Endian current_tessellation_index_endian_ = xenos::Endian::kNone;
VkDescriptorSet current_graphics_descriptor_sets_
[SpirvShaderTranslator::kDescriptorSetCount];
// Whether descriptor sets in current_graphics_descriptor_sets_ point to

View File

@@ -409,6 +409,12 @@ VulkanPipelineCache::GetCurrentVertexShaderModification(
modification.vertex.interpolator_mask = interpolator_mask;
// Tessellation mode selects the domain shader spacing.
if (Shader::IsHostVertexShaderTypeDomain(host_vertex_shader_type)) {
modification.vertex.tessellation_mode =
regs.Get<reg::VGT_HOS_CNTL>().tess_mode;
}
// User clip planes.
auto pa_cl_clip_cntl = regs.Get<reg::PA_CL_CLIP_CNTL>();
uint32_t user_clip_planes =