From 090cecd1b88ab983e18730f52e61096201f3b047 Mon Sep 17 00:00:00 2001 From: bomabomabomaboma <69987043+goldislead@users.noreply.github.com> Date: Sat, 1 Aug 2026 01:13:19 +0000 Subject: [PATCH] [Vulkan] Implement vertex kill (oPts.z) in the translator A non-zero value in bits 0:30 of the kill flag kills the vertex, tested on the integer bits rather than as a float comparison so denormal flushing can't affect the result, matching the DXBC translator. With PA_CL_CLIP_CNTL::vtx_kill_or, the position W is set to NaN, killing the whole primitive if any of its vertices requests the kill, with the "and" operator, a dedicated cull distance after the user clip plane cull distances is set to -1, culling the primitive only when all of its vertices request it. Co-authored-by: Herman S. <429230+has207@users.noreply.github.com> --- src/xenia/gpu/spirv_shader_translator.cc | 57 +++++++++++++++++++ src/xenia/gpu/spirv_shader_translator.h | 7 ++- src/xenia/gpu/vulkan/vulkan_pipeline_cache.cc | 12 ++-- 3 files changed, 71 insertions(+), 5 deletions(-) diff --git a/src/xenia/gpu/spirv_shader_translator.cc b/src/xenia/gpu/spirv_shader_translator.cc index 22af0269d..897ed5bfb 100644 --- a/src/xenia/gpu/spirv_shader_translator.cc +++ b/src/xenia/gpu/spirv_shader_translator.cc @@ -1363,6 +1363,11 @@ void SpirvShaderTranslator::StartVertexOrTessEvalShaderBeforeMain() { } else { clip_distance_count = user_clip_plane_count; } + // Vertex kill with "and" operator writes a dedicated cull distance after + // the user clip plane cull distances. + if (shader_modification.vertex.vertex_kill_and) { + ++cull_distance_count; + } output_per_vertex_clip_distance_member_index_ = 0; output_per_vertex_cull_distance_member_index_ = 0; if (user_clip_plane_count > 0) { @@ -1998,6 +2003,58 @@ void SpirvShaderTranslator::CompleteVertexOrTessEvalShaderInMain() { position_xyz = builder_->createNoContractionBinOp( spv::OpFAdd, type_float3_, position_xyz, ndc_offset_mul_w); + // Apply vertex killing requested via the kill flag (oPts.z) - bits 0:30 of + // the value being non-zero kills. Done after the NDC transform since the kill + // cull distance is just a flag and the position is about to be written. + if (current_shader().writes_point_size_edge_flag_kill_vertex() & 0b100) { + assert_true(var_main_point_size_edge_flag_kill_vertex_ != spv::NoResult); + id_vector_temp_.clear(); + // Z vector component. + id_vector_temp_.push_back(builder_->makeIntConstant(2)); + spv::Id kill_value = builder_->createLoad( + builder_->createAccessChain(spv::StorageClassFunction, + var_main_point_size_edge_flag_kill_vertex_, + id_vector_temp_), + spv::NoPrecision); + // Test the integer bits 0:30 rather than comparing the float to avoid + // denormal flushing affecting the result (matching the Direct3D 12 path). + spv::Id vertex_killed = builder_->createBinOp( + spv::OpINotEqual, type_bool_, + builder_->createBinOp( + spv::OpBitwiseAnd, type_uint_, + builder_->createUnaryOp(spv::OpBitcast, type_uint_, kill_value), + builder_->makeUintConstant(UINT32_C(0x7FFFFFFF))), + const_uint_0_); + if (shader_modification.vertex.vertex_kill_and) { + // "and" operator - write -1 to the dedicated cull distance when killed + // (the primitive is culled only if it's negative for all the vertices). + uint32_t vertex_kill_cull_distance_index = + shader_modification.vertex.user_clip_plane_cull + ? user_clip_plane_count + : 0; + id_vector_temp_.clear(); + id_vector_temp_.push_back(builder_->makeIntConstant( + int(output_per_vertex_cull_distance_member_index_))); + id_vector_temp_.push_back( + builder_->makeIntConstant(int(vertex_kill_cull_distance_index))); + builder_->createStore( + builder_->createTriOp(spv::OpSelect, type_float_, vertex_killed, + builder_->makeFloatConstant(-1.0f), + const_float_0_), + builder_->createAccessChain(spv::StorageClassOutput, + output_per_vertex_, id_vector_temp_)); + } else { + // "or" operator - setting the position W to NaN kills the whole primitive + // if any of its vertices requests the kill. + position_w = builder_->createTriOp( + spv::OpSelect, type_float_, vertex_killed, + builder_->createUnaryOp( + spv::OpBitcast, type_float_, + builder_->makeUintConstant(UINT32_C(0x7FC00000))), + position_w); + } + } + // Write the point size. if (output_point_size_ != spv::NoResult) { spv::Id point_size; diff --git a/src/xenia/gpu/spirv_shader_translator.h b/src/xenia/gpu/spirv_shader_translator.h index 259f638b8..af32a2f10 100644 --- a/src/xenia/gpu/spirv_shader_translator.h +++ b/src/xenia/gpu/spirv_shader_translator.h @@ -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 = 15; + static constexpr uint32_t kVersion = 16; enum class DepthStencilMode : uint32_t { kNoModifiers, @@ -81,6 +81,11 @@ class SpirvShaderTranslator : public ShaderTranslator { // If user_clip_plane_count is non-zero, whether they should be cull // distances instead of clip distances. uint32_t user_clip_plane_cull : 1; + // Vertex kill (oPts.z) with the "and" operator - the primitive is culled + // only when all of its vertices request the kill, emulated with an extra + // 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; } vertex; struct PixelShaderModification { // uint32_t 0. diff --git a/src/xenia/gpu/vulkan/vulkan_pipeline_cache.cc b/src/xenia/gpu/vulkan/vulkan_pipeline_cache.cc index ebc4c0553..71b91117e 100644 --- a/src/xenia/gpu/vulkan/vulkan_pipeline_cache.cc +++ b/src/xenia/gpu/vulkan/vulkan_pipeline_cache.cc @@ -410,6 +410,13 @@ VulkanPipelineCache::GetCurrentVertexShaderModification( modification.vertex.user_clip_plane_cull = uint32_t(user_clip_planes && pa_cl_clip_cntl.ucp_cull_only_ena); + // Vertex kill via the kill flag (oPts.z). The "and" operator (kill only when + // all vertices of the primitive request it) is emulated with a cull distance; + // the "or" operator sets the position to NaN in the translator. + modification.vertex.vertex_kill_and = + uint32_t((shader.writes_point_size_edge_flag_kill_vertex() & 0b100) && + !pa_cl_clip_cntl.vtx_kill_or); + if (host_vertex_shader_type == Shader::HostVertexShaderType::kPointListAsTriangleStrip) { modification.vertex.output_point_parameters = uint32_t(ps_param_gen_used); @@ -1469,12 +1476,9 @@ bool VulkanPipelineCache::GetGeometryShaderKey( } GeometryShaderKey key; key.type = geometry_shader_type; - // TODO(Triang3l): Once all needed inputs and outputs are added, uncomment the - // real counts here. key.interpolator_count = xe::bit_count(vertex_shader_modification.vertex.interpolator_mask); - key.has_vertex_kill_and = - /* vertex_shader_modification.vertex.vertex_kill_and */ 0; + key.has_vertex_kill_and = vertex_shader_modification.vertex.vertex_kill_and; key.has_point_size = vertex_shader_modification.vertex.output_point_parameters; key.has_point_coordinates = pixel_shader_modification.pixel.param_gen_point;