[Vulkan] Add support for precise pixel shader interpolation

Uses VK_KHR_fragment_shader_barycentric to to fix noise artifacts on
Nvidia GPUs in games like Perfect Dark and Tenchu Z. Nvidia's hardware
interpolation produces slight variations even when all vertices have
equal values.

When the extension is available, manually interpolate per-vertex data
using barycentric coordinates with a numerically stable "delta-from-average"
algorithm: avg + (v0-avg)*bary.x + (v1-avg)*bary.y + (v2-avg)*bary.z.
When all vertices equal V, the deltas are exactly zero in floating-point,
producing V exactly.
This commit is contained in:
Herman S.
2026-01-21 01:27:09 +09:00
parent 621430444b
commit 155636834b
5 changed files with 264 additions and 9 deletions
+6
View File
@@ -385,6 +385,7 @@ namespace spv {
#define CapabilityDemoteToHelperInvocationEXT \
Capability::DemoteToHelperInvocationEXT
#define CapabilityStencilExportEXT Capability::StencilExportEXT
#define CapabilityFragmentBarycentricKHR Capability::FragmentBarycentricKHR
// Backward compatibility for AddressingModel
#define AddressingModelLogical AddressingModel::Logical
@@ -433,6 +434,7 @@ namespace spv {
#define ExecutionModeLocalSize ExecutionMode::LocalSize
// Backward compatibility for Decoration
#define DecorationPerVertexKHR Decoration::PerVertexKHR
#define DecorationRestrict Decoration::Restrict
#define DecorationNonWritable Decoration::NonWritable
#define DecorationBufferBlock Decoration::BufferBlock
@@ -465,4 +467,8 @@ namespace spv {
// Backward compatibility for Scope
#define ScopeDevice Scope::Device
// Fragment barycentric BuiltIn values
#define BuiltInBaryCoordKHR BuiltIn::BaryCoordKHR
#define BuiltInBaryCoordNoPerspKHR BuiltIn::BaryCoordNoPerspKHR
} // namespace spv
+205 -8
View File
@@ -41,6 +41,14 @@ DEFINE_bool(
"capability.",
"GPU");
DEFINE_bool(
vulkan_precise_interpolation, true,
"Use manual barycentric interpolation in fragment shaders to avoid "
"precision issues on Nvidia GPUs. Fixes noise artifacts in games like "
"Perfect Dark and Tenchu Z that do exact equality comparisons on "
"interpolated values. Requires VK_KHR_fragment_shader_barycentric.",
"GPU");
namespace xe {
namespace gpu {
@@ -119,7 +127,8 @@ SpirvShaderTranslator::Features::Features(bool all)
denorm_flush_to_zero_float32(all),
rounding_mode_rte_float32(all),
fragment_shader_sample_interlock(all),
demote_to_helper_invocation(all) {}
demote_to_helper_invocation(all),
fragment_shader_barycentric(all) {}
SpirvShaderTranslator::Features::Features(
const ui::vulkan::VulkanDevice* const vulkan_device)
@@ -143,7 +152,9 @@ SpirvShaderTranslator::Features::Features(
fragment_shader_sample_interlock(
vulkan_device->properties().fragmentShaderSampleInterlock),
demote_to_helper_invocation(
vulkan_device->properties().shaderDemoteToHelperInvocation) {
vulkan_device->properties().shaderDemoteToHelperInvocation),
fragment_shader_barycentric(
vulkan_device->properties().fragmentShaderBarycentric) {
// Check for SPIR-V version override from CVAR.
const std::string& override_version = cvars::spirv_version_override;
if (override_version == "1.0") {
@@ -199,6 +210,8 @@ uint64_t SpirvShaderTranslator::GetDefaultPixelShaderModification(
Modification shader_modification;
shader_modification.pixel.dynamic_addressable_register_count =
dynamic_addressable_register_count;
shader_modification.pixel.precise_interpolation =
cvars::vulkan_precise_interpolation ? 1 : 0;
return shader_modification.value;
}
@@ -232,6 +245,11 @@ void SpirvShaderTranslator::Reset() {
input_fragment_coordinates_ = spv::NoResult;
input_front_facing_ = spv::NoResult;
input_sample_mask_ = spv::NoResult;
// Barycentric interpolation inputs.
input_barycentric_coord_ = spv::NoResult;
input_barycentric_coord_no_persp_ = spv::NoResult;
std::fill(input_interpolators_per_vertex_.begin(),
input_interpolators_per_vertex_.end(), spv::NoResult);
std::fill(input_output_interpolators_.begin(),
input_output_interpolators_.end(), spv::NoResult);
output_point_coordinates_ = spv::NoResult;
@@ -2291,7 +2309,56 @@ void SpirvShaderTranslator::StartFragmentShaderBeforeMain() {
uint32_t input_location = 0;
// Interpolator inputs.
{
// When fragment_shader_barycentric is enabled, create per-vertex
// interpolator arrays (float4[3]) with PerVertexKHR decoration for manual
// barycentric interpolation. This works around Nvidia driver differences
// in hardware interpolation that can cause noise artifacts in games that
// do exact equality comparisons in shaders (e.g., Perfect Dark, Tenchu Z).
// Skip barycentric for point primitives - barycentric coordinates are only
// meaningful for triangles.
bool use_barycentric_interpolation =
shader_modification.pixel.precise_interpolation &&
features_.fragment_shader_barycentric &&
!shader_modification.pixel.param_gen_point;
if (use_barycentric_interpolation) {
// Add extension and capability for barycentric interpolation.
builder_->addExtension("SPV_KHR_fragment_shader_barycentric");
builder_->addCapability(spv::CapabilityFragmentBarycentricKHR);
// Create gl_BaryCoordKHR builtin input (float3).
input_barycentric_coord_ =
builder_->createVariable(spv::NoPrecision, spv::StorageClassInput,
type_float3_, "gl_BaryCoordKHR");
builder_->addDecoration(input_barycentric_coord_, spv::DecorationBuiltIn,
static_cast<int>(spv::BuiltInBaryCoordKHR));
main_interface_.push_back(input_barycentric_coord_);
// Create per-vertex interpolator inputs as float4[3] arrays with
// PerVertexKHR decoration.
spv::Id type_float4_array_3 = builder_->makeArrayType(
type_float4_, builder_->makeUintConstant(3), 0);
uint32_t interpolators_remaining = GetModificationInterpolatorMask();
uint32_t interpolator_index;
while (
xe::bit_scan_forward(interpolators_remaining, &interpolator_index)) {
interpolators_remaining &= ~(UINT32_C(1) << interpolator_index);
spv::Id interpolator_per_vertex = builder_->createVariable(
spv::NoPrecision, spv::StorageClassInput, type_float4_array_3,
fmt::format("xe_in_interpolator_{}_per_vertex", interpolator_index)
.c_str());
input_interpolators_per_vertex_[interpolator_index] =
interpolator_per_vertex;
builder_->addDecoration(interpolator_per_vertex,
spv::DecorationLocation, int(input_location));
builder_->addDecoration(interpolator_per_vertex,
spv::DecorationPerVertexKHR);
// Note: Centroid decoration is not applicable with PerVertexKHR since
// we're doing manual interpolation.
main_interface_.push_back(interpolator_per_vertex);
++input_location;
}
} else {
// Standard hardware interpolation path.
uint32_t interpolators_remaining = GetModificationInterpolatorMask();
uint32_t interpolator_index;
while (
@@ -2578,18 +2645,148 @@ void SpirvShaderTranslator::StartFragmentShaderInMain() {
// references them after only initializing them conditionally, and copy
// interpolants to GPRs.
uint32_t interpolator_mask = GetModificationInterpolatorMask();
// When barycentric interpolation is enabled, manually compute interpolated
// values using barycentric coordinates to avoid Nvidia driver interpolation
// differences. Skip for point primitives since barycentric coordinates are
// only meaningful for triangles.
Modification shader_modification = GetSpirvShaderModification();
bool use_barycentric_interpolation =
shader_modification.pixel.precise_interpolation &&
features_.fragment_shader_barycentric &&
!shader_modification.pixel.param_gen_point;
spv::Id barycentric_coords = spv::NoResult;
spv::Id bary_x_vec4 = spv::NoResult;
spv::Id bary_y_vec4 = spv::NoResult;
spv::Id bary_z_vec4 = spv::NoResult;
spv::Id one_third_vec4 = spv::NoResult;
if (use_barycentric_interpolation && interpolator_mask) {
// Load barycentric coordinates once for all interpolators.
barycentric_coords =
builder_->createLoad(input_barycentric_coord_, spv::NoPrecision);
// Extract and smear barycentric coordinates to float4 once for all
// interpolators.
spv::Id bary_x =
builder_->createCompositeExtract(barycentric_coords, type_float_, 0);
spv::Id bary_y =
builder_->createCompositeExtract(barycentric_coords, type_float_, 1);
spv::Id bary_z =
builder_->createCompositeExtract(barycentric_coords, type_float_, 2);
id_vector_temp_util_.clear();
id_vector_temp_util_.push_back(bary_x);
id_vector_temp_util_.push_back(bary_x);
id_vector_temp_util_.push_back(bary_x);
id_vector_temp_util_.push_back(bary_x);
bary_x_vec4 =
builder_->createCompositeConstruct(type_float4_, id_vector_temp_util_);
id_vector_temp_util_.clear();
id_vector_temp_util_.push_back(bary_y);
id_vector_temp_util_.push_back(bary_y);
id_vector_temp_util_.push_back(bary_y);
id_vector_temp_util_.push_back(bary_y);
bary_y_vec4 =
builder_->createCompositeConstruct(type_float4_, id_vector_temp_util_);
id_vector_temp_util_.clear();
id_vector_temp_util_.push_back(bary_z);
id_vector_temp_util_.push_back(bary_z);
id_vector_temp_util_.push_back(bary_z);
id_vector_temp_util_.push_back(bary_z);
bary_z_vec4 =
builder_->createCompositeConstruct(type_float4_, id_vector_temp_util_);
// Create the 1/3 constant once for all interpolators.
spv::Id one_third = builder_->makeFloatConstant(1.0f / 3.0f);
id_vector_temp_util_.clear();
id_vector_temp_util_.push_back(one_third);
id_vector_temp_util_.push_back(one_third);
id_vector_temp_util_.push_back(one_third);
id_vector_temp_util_.push_back(one_third);
one_third_vec4 =
builder_->createCompositeConstruct(type_float4_, id_vector_temp_util_);
}
for (uint32_t i = 0; i < register_count(); ++i) {
if (i == param_gen_interpolator) {
continue;
}
id_vector_temp_.clear();
id_vector_temp_.push_back(builder_->makeIntConstant(int(i)));
spv::Id interpolated_value;
if (i < xenos::kMaxInterpolators &&
(interpolator_mask & (UINT32_C(1) << i))) {
if (use_barycentric_interpolation) {
// Delta-from-average barycentric interpolation for numerical stability.
// When all vertices have equal values (v0 = v1 = v2 = V), this produces
// exactly V, avoiding floating-point precision issues that cause noise
// artifacts on Nvidia GPUs.
//
// Formula: avg = (v0 + v1 + v2) / 3
// result = avg + (v0-avg)*bary.x + (v1-avg)*bary.y +
// (v2-avg)*bary.z
//
// When v0 = v1 = v2 = V:
// avg = V, deltas = 0, result = V + 0 = V (exact)
spv::Id per_vertex_array = input_interpolators_per_vertex_[i];
// Load per-vertex values (vertex 0, 1, 2).
id_vector_temp_util_.clear();
id_vector_temp_util_.push_back(builder_->makeIntConstant(0));
spv::Id v0 = builder_->createLoad(
builder_->createAccessChain(spv::StorageClassInput,
per_vertex_array, id_vector_temp_util_),
spv::NoPrecision);
id_vector_temp_util_.clear();
id_vector_temp_util_.push_back(builder_->makeIntConstant(1));
spv::Id v1 = builder_->createLoad(
builder_->createAccessChain(spv::StorageClassInput,
per_vertex_array, id_vector_temp_util_),
spv::NoPrecision);
id_vector_temp_util_.clear();
id_vector_temp_util_.push_back(builder_->makeIntConstant(2));
spv::Id v2 = builder_->createLoad(
builder_->createAccessChain(spv::StorageClassInput,
per_vertex_array, id_vector_temp_util_),
spv::NoPrecision);
// Compute average: avg = (v0 + v1 + v2) * (1/3)
spv::Id sum_v0_v1 =
builder_->createBinOp(spv::OpFAdd, type_float4_, v0, v1);
spv::Id sum_all =
builder_->createBinOp(spv::OpFAdd, type_float4_, sum_v0_v1, v2);
spv::Id avg = builder_->createBinOp(spv::OpFMul, type_float4_, sum_all,
one_third_vec4);
// Compute deltas from average.
spv::Id d0 = builder_->createBinOp(spv::OpFSub, type_float4_, v0, avg);
spv::Id d1 = builder_->createBinOp(spv::OpFSub, type_float4_, v1, avg);
spv::Id d2 = builder_->createBinOp(spv::OpFSub, type_float4_, v2, avg);
// Compute: avg + d0*bary.x + d1*bary.y + d2*bary.z
spv::Id term0 =
builder_->createBinOp(spv::OpFMul, type_float4_, d0, bary_x_vec4);
spv::Id term1 =
builder_->createBinOp(spv::OpFMul, type_float4_, d1, bary_y_vec4);
spv::Id term2 =
builder_->createBinOp(spv::OpFMul, type_float4_, d2, bary_z_vec4);
spv::Id sum_terms =
builder_->createBinOp(spv::OpFAdd, type_float4_, term0, term1);
sum_terms =
builder_->createBinOp(spv::OpFAdd, type_float4_, sum_terms, term2);
interpolated_value =
builder_->createBinOp(spv::OpFAdd, type_float4_, avg, sum_terms);
} else {
// Standard hardware interpolation path.
interpolated_value = builder_->createLoad(
input_output_interpolators_[i], spv::NoPrecision);
}
} else {
interpolated_value = const_float4_0_;
}
builder_->createStore(
(i < xenos::kMaxInterpolators &&
(interpolator_mask & (UINT32_C(1) << i)))
? builder_->createLoad(input_output_interpolators_[i],
spv::NoPrecision)
: const_float4_0_,
interpolated_value,
builder_->createAccessChain(spv::StorageClassFunction,
var_main_registers_, id_vector_temp_));
}
+15 -1
View File
@@ -39,7 +39,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 = 8;
static constexpr uint32_t kVersion = 9;
enum class DepthStencilMode : uint32_t {
kNoModifiers,
@@ -97,6 +97,8 @@ class SpirvShaderTranslator : public ShaderTranslator {
// For host render targets - which color render targets are actually
// bound.
uint32_t color_targets_used : xenos::kMaxColorRenderTargets;
// Whether to use manual barycentric interpolation for precision.
uint32_t precise_interpolation : 1;
} pixel;
uint64_t value = 0;
@@ -392,6 +394,8 @@ class SpirvShaderTranslator : public ShaderTranslator {
bool fragment_shader_sample_interlock;
bool demote_to_helper_invocation;
bool fragment_shader_barycentric;
};
SpirvShaderTranslator(
@@ -941,6 +945,16 @@ class SpirvShaderTranslator : public ShaderTranslator {
// PS, only when needed - int[1].
spv::Id input_sample_mask_;
// PS, barycentric coordinate inputs (when fragment_shader_barycentric is
// enabled) - float3.
spv::Id input_barycentric_coord_;
spv::Id input_barycentric_coord_no_persp_;
// PS, per-vertex interpolator arrays for barycentric interpolation (when
// fragment_shader_barycentric is enabled). Stores the array variable
// (float4[3]) for each interpolator.
std::array<spv::Id, xenos::kMaxInterpolators> input_interpolators_per_vertex_;
// VS output or PS input, only the ones that are needed (spv::NoResult for the
// unneeded interpolators), indexed by the guest interpolator index - float4.
// The Qualcomm Adreno driver has strict requirements for stage linkage - as
+31
View File
@@ -186,6 +186,8 @@ std::unique_ptr<VulkanDevice> VulkanDevice::CreateIfSupported(
bool ext_1_3_KHR_dynamic_rendering = false;
bool ext_EXT_non_seamless_cube_map = false;
bool ext_1_3_EXT_subgroup_size_control = false;
bool ext_KHR_fragment_shader_barycentric = false;
bool ext_NV_fragment_shader_barycentric = false;
if (with_gpu_emulation) {
// #15.
XE_UI_VULKAN_LOCAL_PROMOTED_EXTENSION(KHR_sampler_mirror_clamp_to_edge, 1,
@@ -212,6 +214,10 @@ std::unique_ptr<VulkanDevice> VulkanDevice::CreateIfSupported(
XE_UI_VULKAN_LOCAL_EXTENSION(EXT_non_seamless_cube_map)
// #226.
XE_UI_VULKAN_LOCAL_PROMOTED_EXTENSION(EXT_subgroup_size_control, 1, 3)
// #322 (KHR) / #203 (NV). Barycentric coordinates for manual
// interpolation.
XE_UI_VULKAN_LOCAL_EXTENSION(KHR_fragment_shader_barycentric)
XE_UI_VULKAN_LOCAL_EXTENSION(NV_fragment_shader_barycentric)
}
if (properties.apiVersion >= VK_MAKE_API_VERSION(0, 1, 1, 0)) {
// #237.
@@ -330,6 +336,13 @@ std::unique_ptr<VulkanDevice> VulkanDevice::CreateIfSupported(
VkPhysicalDeviceSubgroupSizeControlFeatures,
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_SIZE_CONTROL_FEATURES>
features_1_3_EXT_subgroup_size_control;
// VK_KHR_fragment_shader_barycentric (#322) /
// VK_NV_fragment_shader_barycentric (#203). KHR and NV share the same feature
// structure type.
VulkanFeatures<
VkPhysicalDeviceFragmentShaderBarycentricFeaturesKHR,
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_SHADER_BARYCENTRIC_FEATURES_KHR>
features_KHR_fragment_shader_barycentric;
if (get_physical_device_properties2_supported) {
if (properties.apiVersion >= VK_MAKE_API_VERSION(0, 1, 2, 0)) {
@@ -379,6 +392,12 @@ std::unique_ptr<VulkanDevice> VulkanDevice::CreateIfSupported(
features_1_3_EXT_subgroup_size_control.Link(supported_features_2,
device_create_info);
}
// VK_KHR_fragment_shader_barycentric / VK_NV_fragment_shader_barycentric.
if (ext_KHR_fragment_shader_barycentric ||
ext_NV_fragment_shader_barycentric) {
features_KHR_fragment_shader_barycentric.Link(supported_features_2,
device_create_info);
}
ifn.vkGetPhysicalDeviceProperties2(physical_device, &properties_2);
ifn.vkGetPhysicalDeviceFeatures2(physical_device, &supported_features_2);
}
@@ -781,6 +800,18 @@ std::unique_ptr<VulkanDevice> VulkanDevice::CreateIfSupported(
device->extensions_.ext_1_3_EXT_subgroup_size_control =
ext_1_3_EXT_subgroup_size_control;
// VK_KHR_fragment_shader_barycentric (#322) /
// VK_NV_fragment_shader_barycentric (#203).
if (ext_KHR_fragment_shader_barycentric ||
ext_NV_fragment_shader_barycentric) {
if (with_gpu_emulation) {
XE_UI_VULKAN_FEATURE_2(features_KHR_fragment_shader_barycentric,
fragmentShaderBarycentric);
}
}
device->extensions_.ext_KHR_fragment_shader_barycentric =
ext_KHR_fragment_shader_barycentric || ext_NV_fragment_shader_barycentric;
#undef XE_UI_VULKAN_LIMIT
#undef XE_UI_VULKAN_ENUM_LIMIT
#undef XE_UI_VULKAN_FEATURE
+7
View File
@@ -163,6 +163,10 @@ class VulkanDevice {
bool nonSeamlessCubeMap = false;
// VK_KHR_fragment_shader_barycentric (#322)
bool fragmentShaderBarycentric = false;
// Vulkan 1.1 Subgroup Properties
uint32_t subgroupSize = 32;
VkShaderStageFlags subgroupSupportedStages = 0;
@@ -202,6 +206,9 @@ class VulkanDevice {
bool ext_1_3_KHR_dynamic_rendering = false;
// VK_EXT_subgroup_size_control (#226, promoted to 1.3)
bool ext_1_3_EXT_subgroup_size_control = false;
// VK_KHR_fragment_shader_barycentric (#322) or
// VK_NV_fragment_shader_barycentric (#203)
bool ext_KHR_fragment_shader_barycentric = false;
#if XE_PLATFORM_WIN32
// VK_EXT_full_screen_exclusive (#256, Windows only)
bool ext_EXT_full_screen_exclusive = false;