diff --git a/src/graphics/guest_gpu/command_processor/pm4Handlers.cpp b/src/graphics/guest_gpu/command_processor/pm4Handlers.cpp index 83147cdd8..19034daaf 100644 --- a/src/graphics/guest_gpu/command_processor/pm4Handlers.cpp +++ b/src/graphics/guest_gpu/command_processor/pm4Handlers.cpp @@ -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); }; } diff --git a/src/graphics/guest_gpu/hardwareContext.h b/src/graphics/guest_gpu/hardwareContext.h index bbe4fb05e..cb0be0958 100644 --- a/src/graphics/guest_gpu/hardwareContext.h +++ b/src/graphics/guest_gpu/hardwareContext.h @@ -5,6 +5,8 @@ #include "common/common.h" #include "graphics/guest_gpu/gpu_defs.h" +#include + 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& 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 m_user_clip_planes {}; + AaSampleControl m_aa_sample_control; AaConfig m_aa_config; diff --git a/src/graphics/host_gpu/renderer/debug.cpp b/src/graphics/host_gpu/renderer/debug.cpp index 5888fb380..bda659b55 100644 --- a/src/graphics/host_gpu/renderer/debug.cpp +++ b/src/graphics/host_gpu/renderer/debug.cpp @@ -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) { diff --git a/src/graphics/host_gpu/renderer/pipeline/pipelineCache.cpp b/src/graphics/host_gpu/renderer/pipeline/pipelineCache.cpp index 8b5646627..b54834970 100644 --- a/src/graphics/host_gpu/renderer/pipeline/pipelineCache.cpp +++ b/src/graphics/host_gpu/renderer/pipeline/pipelineCache.cpp @@ -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(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); diff --git a/src/graphics/host_gpu/renderer/renderDraw.cpp b/src/graphics/host_gpu/renderer/renderDraw.cpp index ee8bad43e..fada73b93 100644 --- a/src/graphics/host_gpu/renderer/renderDraw.cpp +++ b/src/graphics/host_gpu/renderer/renderDraw.cpp @@ -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 draw_data {}; - const auto draw_stages = vk::ShaderStageFlagBits::eFragment | - (mesh_active ? vk::ShaderStageFlagBits::eMeshEXT - : vk::ShaderStageFlagBits::eVertex); + std::array 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, diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterFlow.cpp b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterFlow.cpp index 74250b80e..17a19ebe3 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterFlow.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterFlow.cpp @@ -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( diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h index db8d43aa8..aa537360a 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h @@ -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; diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterModule.cpp b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterModule.cpp index ed31a752a..29b379c7b 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterModule.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterModule.cpp @@ -244,7 +244,7 @@ void DefineDescriptors(EmitterState& state) { counts[static_cast(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, diff --git a/src/graphics/shader/recompiler/ir/ShaderIR.h b/src/graphics/shader/recompiler/ir/ShaderIR.h index bfcc36e0b..5092eafbf 100644 --- a/src/graphics/shader/recompiler/ir/ShaderIR.h +++ b/src/graphics/shader/recompiler/ir/ShaderIR.h @@ -356,7 +356,14 @@ struct PushData { static constexpr uint32_t NoStart = UINT32_MAX; std::array 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(object_id); } diff --git a/src/graphics/shader/shader.cpp b/src/graphics/shader/shader.cpp index 60fa9260c..f093ed534 100644 --- a/src/graphics/shader/shader.cpp +++ b/src/graphics/shader/shader.cpp @@ -414,6 +414,7 @@ void BuildStageStaticKey(const ShaderVertexInputInfo& info, std::vector(info.clip_space.enabled)); if (info.clip_space.enabled) { for (const float value: info.clip_space.scale) { diff --git a/src/graphics/shader/shader.h b/src/graphics/shader/shader.h index c44e095df..4576eb3df 100644 --- a/src/graphics/shader/shader.h +++ b/src/graphics/shader/shader.h @@ -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;