gpu: implement PA_CL_UCP user clip planes through draw push constants

This commit is contained in:
Mr. Big Britches
2026-10-20 07:00:55 +02:00
committed by nmzik
parent 18e6747dbf
commit feed075a9f
11 changed files with 109 additions and 45 deletions
@@ -291,10 +291,6 @@ static void HwCtxIgnoreAaMaskRegister([[maybe_unused]] uint32_t cmd_offset,
static void HwCtxIgnoreAlphaToMaskRegister([[maybe_unused]] uint32_t value) {}
static void HwCtxIgnoreDisabledUserClipPlane(CommandProcessor& cp, uint32_t value) {
EXIT_NOT_IMPLEMENTED(value != 0 || cp.GetCtx().GetClipControl().user_clip_planes != 0);
}
static void HwCtxIgnorePrimitiveIdReset([[maybe_unused]] uint32_t value) {}
static void HwCtxIgnoreVtxControl([[maybe_unused]] uint32_t value) {}
@@ -2899,7 +2895,7 @@ void GraphicsInitJmpTablesCxIndirect() {
for (auto cmd_offset = Pm4::PA_CL_UCP_0_X; cmd_offset <= Pm4::PA_CL_UCP_5_W; cmd_offset++) {
g_hw_ctx_indirect_func[cmd_offset] = [](KYTY_HW_CTX_INDIRECT_ARGS) {
HwCtxIgnoreDisabledUserClipPlane(cp, value);
cp.GetCtx().SetUserClipPlane(cmd_offset - Pm4::PA_CL_UCP_0_X, value);
};
}
+9
View File
@@ -5,6 +5,8 @@
#include "common/common.h"
#include "graphics/guest_gpu/gpu_defs.h"
#include <array>
namespace Libs::Graphics::HW {
struct ColorBase {
@@ -824,6 +826,11 @@ public:
void SetBlendColor(const BlendColor& color) { m_blend_color = color; }
[[nodiscard]] const ClipControl& GetClipControl() const { return m_clip_control; }
void SetClipControl(const ClipControl& control) { m_clip_control = control; }
// PA_CL_UCP_0_X..PA_CL_UCP_5_W as raw float bits, four per plane.
[[nodiscard]] const std::array<uint32_t, 24>& GetUserClipPlanes() const {
return m_user_clip_planes;
}
void SetUserClipPlane(uint32_t index, uint32_t value) { m_user_clip_planes[index] = value; }
[[nodiscard]] const RenderControl& GetRenderControl() const { return m_render_control; }
void SetRenderControl(const RenderControl& control) { m_render_control = control; }
[[nodiscard]] const DepthRenderOverride& GetDepthRenderOverride() const {
@@ -924,6 +931,8 @@ private:
ColorControl m_color_control;
ScanModeControl m_scan_mode_control;
std::array<uint32_t, 24> m_user_clip_planes {};
AaSampleControl m_aa_sample_control;
AaConfig m_aa_config;
+3 -4
View File
@@ -373,10 +373,9 @@ static void ClipPrint(const char* func, const HW::ClipControl& c) {
static void ClipCheck(const HW::ClipControl& c) {
// dx_linear_attr_clip_enable preserves linear (noperspective) attributes at clip-generated
// vertices, which Vulkan provides as part of clipping and interpolation.
EXIT_NOT_IMPLEMENTED(c.user_clip_planes != 0 || c.user_clip_plane_mode != 0 ||
c.vertex_kill_any || c.user_clip_plane_negate_y ||
c.user_clip_plane_cull_only || c.cull_on_clipping_error_disable ||
c.force_viewport_index_from_vs_enable);
EXIT_NOT_IMPLEMENTED(c.user_clip_plane_mode != 0 || c.vertex_kill_any ||
c.user_clip_plane_negate_y || c.user_clip_plane_cull_only ||
c.cull_on_clipping_error_disable || c.force_viewport_index_from_vs_enable);
}
static void RcPrint(const char* func, const HW::RenderControl& c) {
@@ -780,10 +780,12 @@ PipelineCache::GraphicsPrograms PipelineCache::GetGraphicsPrograms(
}
}
}
if (context.GetClipControl().clip_disable) {
auto& last_stage = vertex_info[tess_active ? 2u : 0u];
const auto& clip_control = context.GetClipControl();
if (clip_control.clip_disable) {
const auto& viewport = context.GetScreenViewport().viewports[0];
const auto& limits = m_graphics.GetPhysicalDeviceProperties().limits;
auto& clip = vertex_info[tess_active ? 2u : 0u].clip_space;
auto& clip = last_stage.clip_space;
clip.scale[0] = viewport.xscale;
clip.scale[1] = viewport.yscale;
clip.offset[0] = viewport.xoffset;
@@ -793,9 +795,16 @@ PipelineCache::GraphicsPrograms PipelineCache::GetGraphicsPrograms(
clip.half_extent[1] =
static_cast<float>(std::min(limits.maxViewportDimensions[1], 16384u)) * 0.5f;
clip.enabled = true;
} else if (clip_control.user_clip_planes != 0) {
// CLIP_DISABLE also disables user planes. How they combine with shader clip and cull
// distances is unverified; draw push constants only reach VS/mesh and PS, and the
// generated RECTLIST stages do not forward clip distances.
EXIT_NOT_IMPLEMENTED((last_stage.pa_cl_vs_out_cntl & 0xffffu) != 0 || tess_active ||
Prospero::IsRectList(user_config.GetPrimType()));
last_stage.user_clip_planes = clip_control.user_clip_planes;
}
uint32_t push_data_cursor = ShaderRecompiler::IR::PushData::GraphicsDrawDwords(
mesh_active, object_primitive_id);
mesh_active, object_primitive_id, last_stage.user_clip_planes);
GraphicsPrograms result;
if (pixel_active) {
result.pixel = m_program_cache->Get(pixel_params, pixel_info, push_data_cursor);
+13 -6
View File
@@ -1193,13 +1193,14 @@ void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buff
if (!mesh_active) {
CommitIndexBuffer(vk_buffer, index_binding);
}
const auto draw_dwords = ShaderRecompiler::IR::PushData::GraphicsDrawDwords(
mesh_active, state.ps_input_info.object_primitive_id);
using ShaderRecompiler::IR::PushData;
const auto draw_stages =
vk::ShaderStageFlagBits::eFragment |
(mesh_active ? vk::ShaderStageFlagBits::eMeshEXT : vk::ShaderStageFlagBits::eVertex);
const auto draw_dwords =
PushData::GraphicsDrawDwords(mesh_active, state.ps_input_info.object_primitive_id);
if (draw_dwords != 0) {
std::array<uint32_t, ShaderRecompiler::IR::PushData::MeshDrawDwordCount> draw_data {};
const auto draw_stages = vk::ShaderStageFlagBits::eFragment |
(mesh_active ? vk::ShaderStageFlagBits::eMeshEXT
: vk::ShaderStageFlagBits::eVertex);
std::array<uint32_t, PushData::MeshDrawDwordCount> draw_data {};
if (mesh_active) {
draw_data = {
draw.index_count,
@@ -1213,6 +1214,12 @@ void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buff
vk_buffer.pushConstants(pipeline.pipeline_layout, draw_stages, 0,
draw_dwords * sizeof(uint32_t), draw_data.data());
}
if (const auto planes = vertex_stages.back().user_clip_planes; planes != 0) {
vk_buffer.pushConstants(pipeline.pipeline_layout, draw_stages,
PushData::UserClipPlaneDword * sizeof(uint32_t),
std::bit_width(planes) * 4u * sizeof(uint32_t),
buffer.GetRegisters().GetUserClipPlanes().data());
}
auto& dynamic_state = m_context.GetCommandScheduler().GetDynamicState();
UpdateGraphicsDynamicState(buffer, dynamic_state, vertex_stages.back(), state.depth_info,
@@ -349,6 +349,38 @@ uint32_t ExportVector(ValueEmitContext& ctx, uint32_t data, const IR::ExportInfo
return vector;
}
uint32_t DistancePointer(EmitterState& state, IR::StageOutputKind kind, uint32_t index) {
if (state.program.stage == ShaderType::Mesh) {
return MeshOutputPointer(state, kind, index);
}
const auto variable = kind == IR::StageOutputKind::ClipDistance ? state.clip_distance_variable
: state.cull_distance_variable;
const auto pointer = state.builder.AllocateId();
state.builder.AddFunction(spv::OpAccessChain,
TypePointer(state, spv::StorageClassOutput, TypeF32(state)), pointer,
variable, ConstantU32(state, index));
return pointer;
}
// The clipper keeps the half space where dot(plane, position) >= 0 for each UCP_ENA plane.
void EmitUserClipDistances(EmitterState& state, uint32_t position) {
uint32_t index = 0;
for (auto mask = state.user_clip_planes; mask != 0; mask &= mask - 1u) {
const auto dword = IR::PushData::UserClipPlaneDword + 4u * std::countr_zero(mask);
uint32_t components[4] {};
for (uint32_t i = 0; i < 4; i++) {
components[i] = Unary(state, spv::OpBitcast, TypeF32(state),
EmitPushDataLoad(state, ConstantU32(state, dword + i)));
}
const auto plane = state.builder.AllocateId();
state.builder.AddFunction(spv::OpCompositeConstruct, TypeF32Vector(state, 4), plane,
components[0], components[1], components[2], components[3]);
state.builder.AddFunction(
spv::OpStore, DistancePointer(state, IR::StageOutputKind::ClipDistance, index++),
Binary(state, spv::OpDot, TypeF32(state), plane, position));
}
}
void EmitAuxPositionExport(ValueEmitContext& ctx, uint32_t data, const IR::ExportInfo& exp) {
auto& state = ctx.state;
for (uint32_t component = 0; component < 4; component++) {
@@ -389,23 +421,13 @@ void EmitAuxPositionExport(ValueEmitContext& ctx, uint32_t data, const IR::Expor
state.builder.AddFunction(spv::OpStore, state.point_size_variable, f32);
continue;
}
auto StoreDistance = [&](IR::StageOutputKind kind, uint32_t variable, uint32_t index) {
if (index == UINT32_MAX) {
return;
auto StoreDistance = [&](IR::StageOutputKind kind, uint32_t index) {
if (index != UINT32_MAX) {
state.builder.AddFunction(spv::OpStore, DistancePointer(state, kind, index), f32);
}
uint32_t pointer;
if (state.program.stage == ShaderType::Mesh) {
pointer = MeshOutputPointer(state, kind, index);
} else {
pointer = state.builder.AllocateId();
state.builder.AddFunction(spv::OpAccessChain,
TypePointer(state, spv::StorageClassOutput, TypeF32(state)),
pointer, variable, ConstantU32(state, index));
}
state.builder.AddFunction(spv::OpStore, pointer, f32);
};
StoreDistance(IR::StageOutputKind::ClipDistance, state.clip_distance_variable, output.clip_distance);
StoreDistance(IR::StageOutputKind::CullDistance, state.cull_distance_variable, output.cull_distance);
StoreDistance(IR::StageOutputKind::ClipDistance, output.clip_distance);
StoreDistance(IR::StageOutputKind::CullDistance, output.cull_distance);
}
}
@@ -596,6 +618,9 @@ void EmitSetAttribute(ValueEmitContext& ctx, const IR::Inst& inst) {
state.input_info.vertex->clip_space.enabled) {
value = ConvertPositionToClipSpace(state, value);
}
if (exp.kind == IR::ExportTargetKind::Position && state.user_clip_planes != 0) {
EmitUserClipDistances(state, value);
}
if (state.program.stage == ShaderType::Mesh) {
const auto kind = exp.kind == IR::ExportTargetKind::Position
? IR::StageOutputKind::Position
@@ -608,7 +633,6 @@ void EmitSetAttribute(ValueEmitContext& ctx, const IR::Inst& inst) {
const auto equal = state.builder.AllocateId();
const auto invalid = state.builder.AllocateId();
const auto distance = state.builder.AllocateId();
const auto distance_pointer = state.builder.AllocateId();
state.builder.AddFunction(spv::OpFOrdEqual, TypeBoolVector(state, 4), equal,
value, zero);
state.builder.AddFunction(spv::OpAll, TypeBool(state), invalid, equal);
@@ -617,11 +641,10 @@ void EmitSetAttribute(ValueEmitContext& ctx, const IR::Inst& inst) {
state.builder.AddFunction(spv::OpSelect, TypeF32(state), distance, invalid,
ConstantF32Value(state, -1.0f),
ConstantF32Value(state, 0.0f));
state.builder.AddFunction(
spv::OpAccessChain, TypePointer(state, spv::StorageClassOutput, TypeF32(state)),
distance_pointer, state.clip_distance_variable,
ConstantU32(state, state.invalid_position_clip_distance));
state.builder.AddFunction(spv::OpStore, distance_pointer, distance);
state.builder.AddFunction(spv::OpStore,
DistancePointer(state, IR::StageOutputKind::ClipDistance,
state.invalid_position_clip_distance),
distance);
static std::atomic_bool logged = false;
if (!logged.exchange(true, std::memory_order_relaxed)) {
Log::WriteToConsoleAndLog(
@@ -148,6 +148,7 @@ struct EmitterState {
uint32_t point_size_variable = 0;
uint32_t clip_distance_variable = 0;
uint32_t invalid_position_clip_distance = UINT32_MAX;
uint32_t user_clip_planes = 0;
uint32_t cull_distance_variable = 0;
uint32_t layer_variable = 0;
uint32_t viewport_index_variable = 0;
@@ -244,7 +244,7 @@ void DefineDescriptors(EmitterState& state) {
counts[static_cast<size_t>(Kind::ShaderData)] =
bindings.ShaderDataDwords() != 0 && !bindings.UsesPushData();
if (state.program.bindings.UsesPushData() || state.program.stage == ShaderType::Mesh ||
state.object_primitive_id_variable != 0) {
state.object_primitive_id_variable != 0 || state.user_clip_planes != 0) {
const auto type = PushConstantBlockType(state);
state.push_constant_variable = state.builder.DefineGlobalVariable(
TypePointer(state, spv::StorageClassPushConstant, type), spv::StorageClassPushConstant);
@@ -600,14 +600,24 @@ void DefineOutputs(EmitterState& state) {
cull_distance_count = std::max(cull_distance_count, output.index + 1);
}
}
const bool position = std::ranges::any_of(state.outputs, [](const OutputBinding& output) {
return output.kind == IR::StageOutputKind::Position;
});
if (position && state.input_info.vertex->user_clip_planes != 0) {
// PipelineCache rejects UCP_ENA with shader clip or cull distances.
EXIT_IF(clip_distance_count + cull_distance_count != 0);
state.user_clip_planes = state.input_info.vertex->user_clip_planes;
for (auto mask = state.user_clip_planes; mask != 0; mask &= mask - 1u) {
state.outputs.push_back(
{{IR::StageOutputKind::ClipDistance, clip_distance_count++, 0, "gl_ClipDistance"}});
}
}
if (state.program.stage == ShaderType::Mesh) {
DefineMeshOutputs(state, clip_distance_count, cull_distance_count);
return;
}
if (state.program.stage == ShaderType::Vertex && clip_distance_count + cull_distance_count < 8u &&
std::ranges::any_of(state.outputs, [](const OutputBinding& output) {
return output.kind == IR::StageOutputKind::Position;
})) {
if (state.program.stage == ShaderType::Vertex &&
clip_distance_count + cull_distance_count < 8u && position) {
// Reserve one plane for the enabled PA_CL_CLIP_CNTL clipping-error cull.
state.invalid_position_clip_distance = clip_distance_count++;
state.outputs.push_back({{IR::StageOutputKind::ClipDistance,
+8 -1
View File
@@ -356,7 +356,14 @@ struct PushData {
static constexpr uint32_t NoStart = UINT32_MAX;
std::array<uint32_t, DwordCount> dwords {};
[[nodiscard]] static constexpr uint32_t GraphicsDrawDwords(bool mesh, bool object_id) {
// User clip planes 0..N follow the mesh draw parameters or object ID, four dwords each.
static constexpr uint32_t UserClipPlaneDword = MeshDrawDwordCount;
[[nodiscard]] static constexpr uint32_t GraphicsDrawDwords(bool mesh, bool object_id,
uint32_t user_clip_planes = 0) {
if (user_clip_planes != 0) {
return UserClipPlaneDword + 4u * std::bit_width(user_clip_planes);
}
return mesh ? MeshDrawDwordCount : static_cast<uint32_t>(object_id);
}
+1
View File
@@ -414,6 +414,7 @@ void BuildStageStaticKey(const ShaderVertexInputInfo& info, std::vector<uint32_t
key.push_back(info.scratch_size_dwords);
key.push_back(info.lds_size_dwords);
key.push_back(info.pa_cl_vs_out_cntl);
key.push_back(info.user_clip_planes);
key.push_back(static_cast<uint32_t>(info.clip_space.enabled));
if (info.clip_space.enabled) {
for (const float value: info.clip_space.scale) {
+2
View File
@@ -152,6 +152,8 @@ struct ShaderVertexInputInfo {
// LDS_SIZE of the GS stage a non-NGG vertex shader runs on; usually zero.
uint32_t lds_size_dwords = 0;
uint32_t pa_cl_vs_out_cntl = 0;
// PA_CL_CLIP_CNTL.UCP_ENA; set only on the last pre-rasterization stage.
uint32_t user_clip_planes = 0;
ShaderClipSpaceTransform clip_space;
ShaderMeshInputInfo mesh;
ShaderTessellationInputInfo tess;