From f7226ca7dc6b27c2e5937c33c955c0c6f4918437 Mon Sep 17 00:00:00 2001 From: nmzik Date: Thu, 15 Oct 2026 21:03:21 +0200 Subject: [PATCH] graphics: emit PS5 register object and primitive IDs to RT7 --- .../command_processor/pm4Handlers.cpp | 26 +-- src/graphics/guest_gpu/hardwareContext.h | 8 + .../renderer/pipeline/pipelineCache.cpp | 16 +- src/graphics/host_gpu/renderer/renderDraw.cpp | 39 ++-- .../backend/spirv/spirvEmitterFlow.cpp | 8 +- .../backend/spirv/spirvEmitterInternal.h | 2 + .../spirv/spirvEmitterMemoryHelpers.cpp | 19 +- .../backend/spirv/spirvEmitterModule.cpp | 15 +- .../backend/spirv/spirvEmitterProgram.cpp | 8 + src/graphics/shader/recompiler/ir/ShaderIR.h | 5 + .../ir/passes/ShaderInfoCollection.cpp | 9 + src/graphics/shader/shader.cpp | 1 + src/graphics/shader/shader.h | 1 + tests/ShaderRecompilerComputeTests.cpp | 183 +++++++++++++----- tests/shaderCfgTests.cpp | 52 +++++ 15 files changed, 291 insertions(+), 101 deletions(-) diff --git a/src/graphics/guest_gpu/command_processor/pm4Handlers.cpp b/src/graphics/guest_gpu/command_processor/pm4Handlers.cpp index 6aed00ea9..a8186d3cc 100644 --- a/src/graphics/guest_gpu/command_processor/pm4Handlers.cpp +++ b/src/graphics/guest_gpu/command_processor/pm4Handlers.cpp @@ -295,10 +295,6 @@ static void HwCtxIgnoreDisabledUserClipPlane(CommandProcessor& cp, uint32_t valu EXIT_NOT_IMPLEMENTED(value != 0 || cp.GetCtx().GetClipControl().user_clip_planes != 0); } -static void HwCtxIgnoreDrawPayloadControl([[maybe_unused]] uint32_t value) {} - -static void HwCtxIgnoreObjprimIdControl([[maybe_unused]] uint32_t value) {} - static void HwCtxIgnorePrimitiveIdReset([[maybe_unused]] uint32_t value) {} static void HwCtxIgnoreVtxControl([[maybe_unused]] uint32_t value) {} @@ -454,23 +450,13 @@ KYTY_HW_CTX_PARSER(HwCtxSetAlphaToMask) { } KYTY_HW_CTX_PARSER(HwCtxSetDrawPayloadControl) { - auto num_values = KYTY_PM4_LEN(cmd_id) - 2u; - - for (uint32_t i = 0; i < num_values; i++) { - HwCtxIgnoreDrawPayloadControl(buffer[i]); - } - - return num_values; + cp.GetCtx().SetDrawPayloadControl(buffer[0]); + return KYTY_PM4_LEN(cmd_id) - 2u; } KYTY_HW_CTX_PARSER(HwCtxSetObjprimIdControl) { - auto num_values = KYTY_PM4_LEN(cmd_id) - 2u; - - for (uint32_t i = 0; i < num_values; i++) { - HwCtxIgnoreObjprimIdControl(buffer[i]); - } - - return num_values; + cp.GetCtx().SetObjprimIdControl(buffer[0]); + return KYTY_PM4_LEN(cmd_id) - 2u; } KYTY_HW_CTX_PARSER(HwCtxSetPrimitiveIdReset) { @@ -3106,7 +3092,7 @@ void GraphicsInitJmpTablesCxIndirect() { cp.GetCtx().SetTfParam(value); }; g_hw_ctx_indirect_func[Pm4::VGT_DRAW_PAYLOAD_CNTL] = [](KYTY_HW_CTX_INDIRECT_ARGS) { - HwCtxIgnoreDrawPayloadControl(value); + cp.GetCtx().SetDrawPayloadControl(value); }; g_hw_ctx_indirect_func[Pm4::VGT_PRIMITIVEID_RESET] = [](KYTY_HW_CTX_INDIRECT_ARGS) { HwCtxIgnorePrimitiveIdReset(value); @@ -3115,7 +3101,7 @@ void GraphicsInitJmpTablesCxIndirect() { cp.GetCtx().SetShaderZFormat(value); }; g_hw_ctx_indirect_func[Pm4::PA_CL_OBJPRIM_ID_CNTL] = [](KYTY_HW_CTX_INDIRECT_ARGS) { - HwCtxIgnoreObjprimIdControl(value); + cp.GetCtx().SetObjprimIdControl(value); }; for (auto cmd_offset = Pm4::PA_SC_CLIPRECT_RULE; cmd_offset < Pm4::PA_SC_CLIPRECT_0_TL + 8u; diff --git a/src/graphics/guest_gpu/hardwareContext.h b/src/graphics/guest_gpu/hardwareContext.h index 82c68baa3..bbe4fb05e 100644 --- a/src/graphics/guest_gpu/hardwareContext.h +++ b/src/graphics/guest_gpu/hardwareContext.h @@ -556,6 +556,8 @@ struct ShaderRegisters { uint32_t m_vgtGsMaxVertOut = 0; uint32_t m_vgtGsOutPrimType = 0; uint32_t m_vgtPrimitiveIdEn = 0; + uint32_t m_vgtDrawPayloadCntl = 0; + uint32_t m_paClObjprimIdCntl = 0; uint32_t m_vgtReuseOff = 0; uint32_t m_vgtTessDistribution = 0; uint32_t m_vgtLsHsConfig = 0; @@ -679,6 +681,10 @@ public: void SetRenderTargetMask(uint32_t mask) { m_render_target_mask = mask; } [[nodiscard]] uint32_t GetRenderTargetMask() const { return m_render_target_mask; } + [[nodiscard]] bool HasObjectPrimitiveIdOutput() const { + return (m_sh_regs.m_vgtDrawPayloadCntl & 1u) != 0 && + (m_render_target_mask & 0xf0000000u) != 0; + } void SetShaderStages(uint32_t flags) { m_shader_stages = flags; } [[nodiscard]] uint32_t GetShaderStages() const { return m_shader_stages; } @@ -877,6 +883,8 @@ public: void SetGsMaxVertOut(uint32_t value) { m_sh_regs.m_vgtGsMaxVertOut = value; } void SetGsOutPrimType(uint32_t value) { m_sh_regs.m_vgtGsOutPrimType = value; } void SetPrimitiveIdEn(uint32_t value) { m_sh_regs.m_vgtPrimitiveIdEn = value; } + void SetDrawPayloadControl(uint32_t value) { m_sh_regs.m_vgtDrawPayloadCntl = value; } + void SetObjprimIdControl(uint32_t value) { m_sh_regs.m_paClObjprimIdCntl = value; } void SetReuseOff(uint32_t value) { m_sh_regs.m_vgtReuseOff = value; } void SetTessDistribution(uint32_t value) { m_sh_regs.m_vgtTessDistribution = value; } void SetLsHsConfig(uint32_t value) { m_sh_regs.m_vgtLsHsConfig = value; } diff --git a/src/graphics/host_gpu/renderer/pipeline/pipelineCache.cpp b/src/graphics/host_gpu/renderer/pipeline/pipelineCache.cpp index 86212bca1..d2c4a7d96 100644 --- a/src/graphics/host_gpu/renderer/pipeline/pipelineCache.cpp +++ b/src/graphics/host_gpu/renderer/pipeline/pipelineCache.cpp @@ -678,6 +678,17 @@ PipelineCache::GraphicsPrograms PipelineCache::GetGraphicsPrograms( vertex_params[0] = PrepareProgram(vertex_regs, context, user_config, vertex_info[0]); } const bool mesh_active = vertex_info[0].logical_stage == ShaderType::Mesh; + const bool object_primitive_id = context.HasObjectPrimitiveIdOutput(); + if (object_primitive_id) { + // CxObjectIdMode: register source plus a 32-bit pipelined primitive-ID addition. + EXIT_NOT_IMPLEMENTED(sh.m_paClObjprimIdCntl != 2u || mesh_active || tess_active || + !pixel_active || (sh.m_vgtDrawPayloadCntl & 8u) == 0 || + user_config.GetPrimType() != Prospero::PrimitiveType::kTriList); + const auto& target = context.GetRenderTarget(7); + EXIT_NOT_IMPLEMENTED(Prospero::ResolveRenderTargetFormat(target.info.format, + target.info.channel_type) + .buffer_format != Prospero::BufferFormat::k32UInt); + } if (mesh_active) { EXIT_NOT_IMPLEMENTED(!m_graphics.mesh_shader_enabled); auto& mesh = vertex_info[0].mesh; @@ -699,6 +710,7 @@ PipelineCache::GraphicsPrograms PipelineCache::GetGraphicsPrograms( ShaderParams pixel_params; if (pixel_active) { pixel_params = PrepareProgram(pixel_regs, sh, target_export_mapping, pixel_info); + pixel_info.object_primitive_id = object_primitive_id; for (uint32_t export_index = 0; export_index < 8u; export_index++) { const auto slot = ShaderPixelExportTarget(pixel_info.target_shader_mask, export_index); if (slot >= 8u || pixel_info.target_output_mode[export_index] == 0u) continue; @@ -777,8 +789,8 @@ PipelineCache::GraphicsPrograms PipelineCache::GetGraphicsPrograms( static_cast(std::min(limits.maxViewportDimensions[1], 16384u)) * 0.5f; clip.enabled = true; } - uint32_t push_data_cursor = - mesh_active ? ShaderRecompiler::IR::PushData::MeshDrawDwordCount : 0; + uint32_t push_data_cursor = ShaderRecompiler::IR::PushData::GraphicsDrawDwords( + mesh_active, object_primitive_id); 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 180194a20..20547b178 100644 --- a/src/graphics/host_gpu/renderer/renderDraw.cpp +++ b/src/graphics/host_gpu/renderer/renderDraw.cpp @@ -585,7 +585,7 @@ RenderState RenderExecutor::AcquireRenderTargets(CommandBuffer& buffer, RenderCo static uint32_t DrawColorOutputMask(const HW::Context& ctx) { const auto& sh_regs = ctx.GetShaderRegisters(); const auto write_mask = ctx.GetRenderTargetMask() & sh_regs.m_cbShaderMask; - uint32_t output_mask = 0; + uint32_t output_mask = ctx.HasObjectPrimitiveIdOutput() ? 1u << 7u : 0; for (uint32_t index = 0; index < RENDER_COLOR_ATTACHMENTS_MAX; index++) { const auto slot = ShaderPixelExportTarget(sh_regs.m_cbShaderMask, index); if (slot >= RENDER_COLOR_ATTACHMENTS_MAX) { @@ -905,7 +905,8 @@ bool RenderExecutor::PrepareDrawRenderState(CommandBuffer& buffer, const DrawCal uint32_t mrt_mask = 0; if (state.ps_active) { for (const auto& output: state.ps_input_info.stage.program->info.outputs) { - if (output.kind == ShaderRecompiler::IR::StageOutputKind::Mrt) { + if (output.kind == ShaderRecompiler::IR::StageOutputKind::Mrt || + output.kind == ShaderRecompiler::IR::StageOutputKind::ObjectPrimitiveId) { mrt_mask |= 1u << output.location; } } @@ -1182,21 +1183,29 @@ void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buff SetDrawDebugPhase(buffer, submit_id, draw, 0x300u); } CommitBindings(buffer, vk::PipelineBindPoint::eGraphics, pipeline, stages); - if (mesh_active) { - const uint32_t draw_data[] { - draw.index_count, - draw.IsIndexed() ? static_cast(emit.vertex_offset) : emit.first_vertex, - emit.first_instance, index_source.guest_element_size, - static_cast(index_source.address), - static_cast(index_source.address >> 32u)}; - static_assert(std::size(draw_data) == ShaderRecompiler::IR::PushData::MeshDrawDwordCount); - vk_buffer.pushConstants(pipeline.pipeline_layout, - vk::ShaderStageFlagBits::eMeshEXT | - vk::ShaderStageFlagBits::eFragment, - 0, sizeof(draw_data), draw_data); - } else { + 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); + if (draw_dwords != 0) { + std::array draw_data {}; + const auto draw_stages = vk::ShaderStageFlagBits::eFragment | + (mesh_active ? vk::ShaderStageFlagBits::eMeshEXT + : vk::ShaderStageFlagBits::eVertex); + if (mesh_active) { + draw_data = { + draw.index_count, + draw.IsIndexed() ? static_cast(emit.vertex_offset) : emit.first_vertex, + emit.first_instance, index_source.guest_element_size, + static_cast(index_source.address), + static_cast(index_source.address >> 32u)}; + } else { + draw_data[0] = buffer.GetUserConfig().GetObjectId(); + } + vk_buffer.pushConstants(pipeline.pipeline_layout, draw_stages, 0, + draw_dwords * sizeof(uint32_t), draw_data.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 09d0ac7c5..e36e43b37 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterFlow.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterFlow.cpp @@ -676,17 +676,11 @@ uint32_t EmitLaneId(EmitterState& state) { uint32_t EmitMeshDrawParameter(ValueEmitContext& ctx, const IR::Inst& inst) { auto& state = ctx.state; - const auto result = state.builder.AllocateId(); const auto index = inst.Arg(0).U32(); if (state.program.stage != ShaderType::Mesh || index >= IR::PushData::MeshDrawDwordCount) { ctx.Fail(inst, "invalid mesh draw parameter"); } - const auto pointer = state.builder.AllocateId(); - state.builder.AddFunction(spv::OpAccessChain, TypePushConstantElementPointer(state), pointer, - state.push_constant_variable, ConstantU32(state, 0), - ConstantU32(state, index)); - state.builder.AddFunction(spv::OpLoad, TypeU32(state), result, pointer); - return result; + return EmitPushDataLoad(state, ConstantU32(state, index)); } uint32_t EmitGetUserData(EmitterState& state, IR::ScalarReg reg) { diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h index 25347d7a6..6dc28ad2e 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h @@ -152,6 +152,7 @@ struct EmitterState { uint32_t layer_variable = 0; uint32_t viewport_index_variable = 0; uint32_t depth_variable = 0; + uint32_t object_primitive_id_variable = 0; uint32_t sample_mask_variable = 0; std::vector inputs; std::vector outputs; @@ -403,6 +404,7 @@ inline constexpr auto EmitAddU32 = EmitNative(Kind::ShaderData)] = bindings.ShaderDataDwords() != 0 && !bindings.UsesPushData(); - if (state.program.bindings.UsesPushData() || state.program.stage == ShaderType::Mesh) { + if (state.program.bindings.UsesPushData() || state.program.stage == ShaderType::Mesh || + state.object_primitive_id_variable != 0) { const auto type = PushConstantBlockType(state); state.push_constant_variable = state.builder.DefineGlobalVariable( TypePointer(state, spv::StorageClassPushConstant, type), spv::StorageClassPushConstant); @@ -546,6 +547,11 @@ void DefineInputs(EmitterState& state) { } input.variable_id = DefineInterfaceVariable(state, type, spv::StorageClassInput, input.debug_name.c_str()); + if (state.program.stage == ShaderType::Pixel && input.kind == IR::StageInputKind::PrimitiveId) { + // Fragment PrimitiveId accepts Tessellation, which the device already enables. + state.builder.RequireCapability(spv::CapabilityTessellation); + state.builder.AddAnnotation(spv::OpDecorate, input.variable_id, spv::DecorationFlat); + } if (input.kind == IR::StageInputKind::Layer || input.kind == IR::StageInputKind::SampleId) { state.builder.AddAnnotation(spv::OpDecorate, input.variable_id, spv::DecorationFlat); } @@ -657,6 +663,13 @@ void DefineOutputs(EmitterState& state) { BuiltIn(state.sample_mask_variable, SampleMaskArrayType(state), "gl_SampleMask", spv::BuiltInSampleMask); break; + case IR::StageOutputKind::ObjectPrimitiveId: + binding.variable_id = DefineInterfaceVariable( + state, TypeU32(state), spv::StorageClassOutput, binding.debug_name.c_str()); + state.object_primitive_id_variable = binding.variable_id; + state.builder.AddAnnotation(spv::OpDecorate, binding.variable_id, + spv::DecorationLocation, binding.location); + break; case IR::StageOutputKind::Parameter: case IR::StageOutputKind::Mrt: { const bool uint_output = diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterProgram.cpp b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterProgram.cpp index 9877a95a2..63d5519c6 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterProgram.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterProgram.cpp @@ -85,6 +85,14 @@ void StoreDispatcherPhiEdge(ValueEmitContext& ctx, const DispatcherFunctionState void EmitReturn(ValueEmitContext& ctx) { EmitKillIfPixelValidMaskInactive(ctx.state); + if (ctx.state.object_primitive_id_variable != 0) { + auto& state = ctx.state; + const auto primitive = EmitGetBuiltin( + ctx, IR::Value {static_cast(IR::StageInputKind::PrimitiveId)}, IR::Value {0u}); + const auto object = EmitPushDataLoad(state, ConstantU32(state, 0)); + state.builder.AddFunction(spv::OpStore, state.object_primitive_id_variable, + EmitAddU32(state, object, primitive)); + } ctx.state.builder.AddFunction(spv::OpReturn); } diff --git a/src/graphics/shader/recompiler/ir/ShaderIR.h b/src/graphics/shader/recompiler/ir/ShaderIR.h index 450995080..9b52a565b 100644 --- a/src/graphics/shader/recompiler/ir/ShaderIR.h +++ b/src/graphics/shader/recompiler/ir/ShaderIR.h @@ -245,6 +245,7 @@ enum class StageOutputKind { Position, Parameter, Mrt, + ObjectPrimitiveId, Depth, SampleMask, PointSize, @@ -350,6 +351,10 @@ struct PushData { static constexpr uint32_t NoStart = UINT32_MAX; std::array dwords {}; + [[nodiscard]] static constexpr uint32_t GraphicsDrawDwords(bool mesh, bool object_id) { + return mesh ? MeshDrawDwordCount : static_cast(object_id); + } + [[nodiscard]] static constexpr bool CanFit(uint32_t start, uint32_t size) { return size != 0 && start <= DwordCount && size <= DwordCount - start; } diff --git a/src/graphics/shader/recompiler/ir/passes/ShaderInfoCollection.cpp b/src/graphics/shader/recompiler/ir/passes/ShaderInfoCollection.cpp index ad90aa3be..b2df912f8 100644 --- a/src/graphics/shader/recompiler/ir/passes/ShaderInfoCollection.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ShaderInfoCollection.cpp @@ -599,6 +599,15 @@ void CollectShaderInfo(Program& program, ShaderStageInputInfo input_info) { case ShaderType::Mesh: break; case ShaderType::Pixel: CollectPixelInputs(input_info.pixel, info, inputs.per_vertex, inputs.components); + if (input_info.pixel->object_primitive_id) { + if (std::ranges::any_of(info.outputs, [](const StageOutput& output) { + return output.kind == StageOutputKind::Mrt && output.location == 7u; + })) { + return Fail("explicit MRT7 export overlaps automatic object/primitive ID"); + } + AddBuiltinInput(info, StageInputKind::PrimitiveId); + AddOutput(info, StageOutputKind::ObjectPrimitiveId, 0, 7, "out_object_primitive_id"); + } break; case ShaderType::Compute: CollectComputeInputs(input_info.compute, info); break; default: return Fail("unsupported shader stage for info collection"); diff --git a/src/graphics/shader/shader.cpp b/src/graphics/shader/shader.cpp index 6c60b2587..2e5c42753 100644 --- a/src/graphics/shader/shader.cpp +++ b/src/graphics/shader/shader.cpp @@ -673,6 +673,7 @@ void BuildStageStaticKey(const ShaderPixelInputInfo& info, std::vector key.push_back(static_cast(info.ps_sample_mask_export_enable)); key.push_back(static_cast(info.ps_early_z)); key.push_back(static_cast(info.dual_source_blending)); + key.push_back(static_cast(info.object_primitive_id)); key.push_back(static_cast(info.alpha_blend_source)); key.insert(key.end(), std::begin(info.target_output_mode), std::end(info.target_output_mode)); key.push_back(info.target_packed_unorm_mask | (uint32_t {info.target_round_mode} << 8u)); diff --git a/src/graphics/shader/shader.h b/src/graphics/shader/shader.h index 7006aa57d..9294421e9 100644 --- a/src/graphics/shader/shader.h +++ b/src/graphics/shader/shader.h @@ -204,6 +204,7 @@ struct ShaderPixelInputInfo { bool ps_sample_mask_export_enable = false; bool ps_sample_shading = false; bool dual_source_blending = false; + bool object_primitive_id = false; // Export logical alpha or per-channel source factors through MRT1 after channel swizzling. ShaderAlphaBlendSource alpha_blend_source = ShaderAlphaBlendSource::None; bool ps_early_z = false; diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index 8fa132448..63940c81c 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -1391,6 +1391,8 @@ struct GraphicsCase { u32 pixel_perspective_sample_vgpr = UINT32_MAX; u32 pixel_perspective_centroid_vgpr = UINT32_MAX; u32 pixel_custom_interpolation_mask = 0; + u32 width = 1; + std::vector object_ids; }; struct CompiledShader { @@ -1838,6 +1840,7 @@ CompiledShader CompileFragmentCase(const GraphicsCase &test) { pixel_info.ps_perspective_sample_vgpr = test.pixel_perspective_sample_vgpr; pixel_info.ps_perspective_centroid_vgpr = test.pixel_perspective_centroid_vgpr; pixel_info.custom_interpolation_mask = test.pixel_custom_interpolation_mask; + pixel_info.object_primitive_id = !test.object_ids.empty(); for (u32 i = 0; i < std::size(pixel_info.interpolator_settings); i++) { pixel_info.interpolator_settings[i] = i; } @@ -1869,7 +1872,8 @@ CompiledShader CompileFragmentCase(const GraphicsCase &test) { resource_plan, runtime, resources, specialization), "translated resources could not be materialized"); auto result = ShaderRecompiler::CompileProgram( - std::move(translated), options, specialization); + std::move(translated), options, specialization, + static_cast(pixel_info.object_primitive_id)); Require(test.name, "SPIR-V emit", !result.spirv.empty(), "recompiler returned empty SPIR-V"); ValidateSpirv(test.name, result.spirv); @@ -18282,12 +18286,13 @@ public: std::vector RenderFragment(const GraphicsCase &test, const CompiledShader &fragment) { + const bool object_id = !test.object_ids.empty(); const auto vertex_spirv = TestSpv::MakePassthroughVertexSpirv(test.layers > 1, test.vertex_clip_w); ValidateSpirv(test.name, vertex_spirv); Image target = - CreateImageMips(test.name, 1, 1, vk::Format::eR32G32B32A32Sfloat, + CreateImageMips(test.name, test.width, 1, vk::Format::eR32G32B32A32Sfloat, vk::ImageUsageFlagBits::eColorAttachment, {}, 4, vk::ImageLayout::eGeneral, vk::ImageType::e2D, test.layers > 1 ? vk::ImageViewType::e2DArray : vk::ImageViewType::e2D, @@ -18295,11 +18300,20 @@ public: Image resolved; if (test.samples != vk::SampleCountFlagBits::e1) { resolved = CreateImageMips( - test.name, 1, 1, target.format, vk::ImageUsageFlagBits::eColorAttachment, + test.name, test.width, 1, target.format, vk::ImageUsageFlagBits::eColorAttachment, {}, 4, vk::ImageLayout::eGeneral, vk::ImageType::e2D, test.layers > 1 ? vk::ImageViewType::e2DArray : vk::ImageViewType::e2D, test.layers); } + Image primitive_target; + if (object_id) { + primitive_target = CreateImageMips( + test.name, test.width, 1, vk::Format::eR32Uint, + vk::ImageUsageFlagBits::eColorAttachment, {}, 1, + vk::ImageLayout::eGeneral, vk::ImageType::e2D, + test.layers > 1 ? vk::ImageViewType::e2DArray : vk::ImageViewType::e2D, + test.layers); + } const std::vector default_vertices = { 0xbf800000u, 0xbf800000u, 0x3e800000u, 0x3f000000u, 0x3f400000u, 0x3f800000u, 0x40400000u, 0xbf800000u, 0x3e800000u, 0x3f000000u, @@ -18308,8 +18322,8 @@ public: }; const auto &vertices = test.vertices.empty() ? default_vertices : test.vertices; - Require(test.name, "graphics", vertices.size() == 18u, - "graphics vertex buffer must contain three pos2/color4 vertices"); + Require(test.name, "graphics", !vertices.empty() && vertices.size() % 18u == 0, + "graphics vertex buffer must contain pos2/color4 triangle lists"); auto vertex_buffer = CreateHostBuffer(test.name, vertices.size() * sizeof(u32), vk::BufferUsageFlagBits::eVertexBuffer, vertices); @@ -18324,8 +18338,9 @@ public: test.push_constants.size() * sizeof(u32) == fragment_bind.ShaderDataDwords() * sizeof(u32), "fragment push constant data size does not match reflection"); + } + if (fragment_bind.UsesPushData() || object_id) { push_constant_range.stageFlags = vk::ShaderStageFlagBits::eFragment; - push_constant_range.offset = 0; push_constant_range.size = ShaderRecompiler::IR::NativePushConstantSize; } @@ -18379,12 +18394,12 @@ public: vk::Viewport viewport{}; viewport.x = 0.0f; viewport.y = 0.0f; - viewport.width = 1.0f; + viewport.width = static_cast(test.width); viewport.height = 1.0f; viewport.minDepth = 0.0f; viewport.maxDepth = 1.0f; vk::Rect2D scissor{}; - scissor.extent.width = 1; + scissor.extent.width = test.width; scissor.extent.height = 1; vk::PipelineViewportStateCreateInfo viewport_state{}; viewport_state.sType = vk::StructureType::ePipelineViewportStateCreateInfo; @@ -18410,21 +18425,25 @@ public: multisample.sType = vk::StructureType::ePipelineMultisampleStateCreateInfo; multisample.rasterizationSamples = test.samples; - vk::PipelineColorBlendAttachmentState color_attachment{}; - color_attachment.colorWriteMask = - vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | - vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA; + std::array color_attachments{}; + for (auto &attachment : color_attachments) { + attachment.colorWriteMask = + vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | + vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA; + } vk::PipelineColorBlendStateCreateInfo color_blend{}; color_blend.sType = vk::StructureType::ePipelineColorBlendStateCreateInfo; - color_blend.attachmentCount = 1; - color_blend.pAttachments = &color_attachment; + color_blend.attachmentCount = object_id ? 8 : 1; + color_blend.pAttachments = color_attachments.data(); vk::GraphicsPipelineCreateInfo pipeline_info{}; - const vk::Format color_format = vk::Format::eR32G32B32A32Sfloat; + std::array color_formats{}; + color_formats[0] = target.format; + color_formats[7] = primitive_target.format; vk::PipelineRenderingCreateInfo rendering_pipeline{}; rendering_pipeline.sType = vk::StructureType::ePipelineRenderingCreateInfo; - rendering_pipeline.colorAttachmentCount = 1; - rendering_pipeline.pColorAttachmentFormats = &color_format; + rendering_pipeline.colorAttachmentCount = color_blend.attachmentCount; + rendering_pipeline.pColorAttachmentFormats = color_formats.data(); pipeline_info.sType = vk::StructureType::eGraphicsPipelineCreateInfo; pipeline_info.pNext = &rendering_pipeline; pipeline_info.stageCount = 2; @@ -18442,48 +18461,78 @@ public: nullptr, &pipeline), "vkCreateGraphicsPipelines"); - vk::CommandBuffer cmd = BeginCommands(test.name, "graphics"); - vk::RenderingAttachmentInfo color{}; - color.sType = vk::StructureType::eRenderingAttachmentInfo; - color.imageView = target.view; - color.imageLayout = vk::ImageLayout::eGeneral; - color.loadOp = vk::AttachmentLoadOp::eClear; - color.storeOp = vk::AttachmentStoreOp::eStore; + std::array colors{}; + colors[0].imageView = target.view; + colors[0].imageLayout = vk::ImageLayout::eGeneral; + colors[0].loadOp = vk::AttachmentLoadOp::eClear; + colors[0].storeOp = vk::AttachmentStoreOp::eStore; if (resolved.image != nullptr) { - color.resolveMode = vk::ResolveModeFlagBits::eAverage; - color.resolveImageView = resolved.view; - color.resolveImageLayout = vk::ImageLayout::eGeneral; + colors[0].resolveMode = vk::ResolveModeFlagBits::eAverage; + colors[0].resolveImageView = resolved.view; + colors[0].resolveImageLayout = vk::ImageLayout::eGeneral; + } + if (object_id) { + colors[7].imageView = primitive_target.view; + colors[7].imageLayout = vk::ImageLayout::eGeneral; + colors[7].loadOp = vk::AttachmentLoadOp::eClear; + colors[7].storeOp = vk::AttachmentStoreOp::eStore; + colors[7].clearValue.color.uint32[0] = 0xfeedfaceu; } vk::RenderingInfo rendering{}; rendering.sType = vk::StructureType::eRenderingInfo; - rendering.renderArea.extent = {1, 1}; + rendering.renderArea.extent = {test.width, 1}; rendering.layerCount = test.layers; - rendering.colorAttachmentCount = 1; - rendering.pColorAttachments = &color; - cmd.beginRendering(rendering); - cmd.bindPipeline(vk::PipelineBindPoint::eGraphics, pipeline); - if (push_constant_range.size != 0) { - ShaderRecompiler::IR::PushData push_data; - std::copy(test.push_constants.begin(), test.push_constants.end(), - push_data.dwords.begin() + fragment_bind.push_data_start_dword); - cmd.pushConstants(pipeline_layout, vk::ShaderStageFlagBits::eFragment, 0, - sizeof(push_data), push_data.dwords.data()); + rendering.colorAttachmentCount = color_blend.attachmentCount; + rendering.pColorAttachments = colors.data(); + std::vector pixel; + // Reuse the same shader and pipeline while changing only dynamic object ID. + for (size_t draw = 0; draw < std::max(test.object_ids.size(), 1); ++draw) { + vk::CommandBuffer cmd = BeginCommands(test.name, "graphics"); + for (auto *image : {&target, &primitive_target}) { + if (image->image != nullptr && image->layout != vk::ImageLayout::eGeneral) { + AddImageBarrier(cmd, image->image, image->layout, + vk::ImageLayout::eGeneral, + vk::PipelineStageFlagBits::eTransfer, + vk::PipelineStageFlagBits::eColorAttachmentOutput, + vk::AccessFlagBits::eTransferRead, + vk::AccessFlagBits::eColorAttachmentWrite, + image->mip_levels, image->layers); + image->layout = vk::ImageLayout::eGeneral; + } + } + cmd.beginRendering(rendering); + cmd.bindPipeline(vk::PipelineBindPoint::eGraphics, pipeline); + if (push_constant_range.size != 0) { + ShaderRecompiler::IR::PushData push_data; + if (object_id) { + push_data.dwords[0] = test.object_ids[draw]; + } + if (fragment_bind.UsesPushData()) { + std::copy(test.push_constants.begin(), test.push_constants.end(), + push_data.dwords.begin() + fragment_bind.push_data_start_dword); + } + cmd.pushConstants(pipeline_layout, vk::ShaderStageFlagBits::eFragment, 0, + sizeof(push_data), push_data.dwords.data()); + } + vk::DeviceSize offset = 0; + cmd.bindVertexBuffers(0, 1, &vertex_buffer.buffer, &offset); + cmd.draw(static_cast(vertices.size() / 6), test.layers, 0, 0); + cmd.endRendering(); + EndSubmitAndFree(test.name, "graphics", cmd); + auto color_words = ReadImage(test.name, resolved.image != nullptr ? &resolved : &target); + pixel.insert(pixel.end(), color_words.begin(), color_words.end()); + if (object_id) { + auto id_words = ReadImage(test.name, &primitive_target); + pixel.insert(pixel.end(), id_words.begin(), id_words.end()); + } } - vk::DeviceSize offset = 0; - cmd.bindVertexBuffers(0, 1, &vertex_buffer.buffer, &offset); - cmd.draw(3, test.layers, 0, 0); - cmd.endRendering(); - EndSubmitAndFree(test.name, "graphics", cmd); - target.layout = vk::ImageLayout::eGeneral; - - auto pixel = ReadImage(test.name, resolved.image != nullptr ? &resolved : &target); - pixel.resize(4 * test.layers); m_device.destroyPipeline(pipeline, nullptr); m_device.destroyPipelineLayout(pipeline_layout, nullptr); m_device.destroyShaderModule(fragment_module, nullptr); m_device.destroyShaderModule(vertex_module, nullptr); DestroyBuffer(&vertex_buffer); + DestroyImage(&primitive_target); DestroyImage(&resolved); DestroyImage(&target); return pixel; @@ -41460,6 +41509,38 @@ GraphicsCase GraphicsBranchPathFinalVmExportDiscardsInactiveExec() { O::S_CBRANCH_SCC1, O::S_MOV_B32, O::S_ENDPGM}}; } +GraphicsCase GraphicsObjectPrimitiveId(bool discard) { + auto test = discard ? GraphicsNullVmExportDiscardsInactiveExec() + : GraphicsDirectSgprPushConstantExport(); + test.name = discard ? "GraphicsObjectPrimitiveIdDiscard" + : "GraphicsObjectPrimitiveId"; + test.width = 2; + test.layers = 2; + test.object_ids = {0x0001ffffu, 0xffffffffu}; + // Each triangle covers one pixel; the second instance renders the same pair + // into layer 1, making primitive progression and instance reset observable. + for (const auto &position : std::array, 6>{{ + {-1.0f, -1.0f}, {0.0f, -1.0f}, {-0.5f, 1.0f}, + {0.0f, -1.0f}, {1.0f, -1.0f}, {0.5f, 1.0f}}}) { + test.vertices.insert(test.vertices.end(), + {std::bit_cast(position[0]), + std::bit_cast(position[1]), 0, 0, 0, 0x3f800000u}); + } + const auto color = test.expected_pixel; + test.expected_pixel.clear(); + for (u32 object_id : test.object_ids) { + for (u32 pixel = 0; pixel < test.width * test.layers; ++pixel) { + test.expected_pixel.insert(test.expected_pixel.end(), color.begin(), color.end()); + } + // RT7 is sparse and scalar uint, not a remapped or float PS export. + for (u32 layer = 0; layer < test.layers; ++layer) { + test.expected_pixel.push_back(discard ? 0xfeedfaceu : object_id); + test.expected_pixel.push_back(discard ? 0xfeedfaceu : object_id + 1u); + } + } + return test; +} + TestCase MultipleWorkitemsGlobalId() { using O = ShaderOpcode; @@ -42106,6 +42187,8 @@ std::vector MakeGraphicsCases() { GraphicsMrt0OffVmExportDiscardsInactiveExec(), GraphicsFinalVmExportSupersedesEarlierVmMask(), GraphicsBranchPathFinalVmExportDiscardsInactiveExec(), + GraphicsObjectPrimitiveId(false), + GraphicsObjectPrimitiveId(true), }; } @@ -47846,6 +47929,12 @@ int main(int argc, char **argv) { RunGraphicsCase(&vulkan, GraphicsPositionWExport()); return 0; } + if (argc == 2 && std::strcmp(argv[1], "--object-primitive-id-only") == 0) { + VulkanHarness vulkan; + RunGraphicsCase(&vulkan, GraphicsObjectPrimitiveId(false)); + RunGraphicsCase(&vulkan, GraphicsObjectPrimitiveId(true)); + return 0; + } if (argc == 2 && std::strcmp(argv[1], "--centroid-only") == 0) { VulkanHarness vulkan; RunGraphicsCase(&vulkan, GraphicsPackedHalfCentroid()); diff --git a/tests/shaderCfgTests.cpp b/tests/shaderCfgTests.cpp index 5bb86ad6f..79d69e921 100644 --- a/tests/shaderCfgTests.cpp +++ b/tests/shaderCfgTests.cpp @@ -13469,6 +13469,57 @@ void TestNewShaderRecompilerExpPixelOutputs() { CheckSpirvBinaryValidates(unorm16_ba_result.spirv); } +void TestAutomaticObjectPrimitiveId() { + using namespace ShaderRecompiler::IR; + HW::Context context; + context.SetRenderTargetMask(0xf000000fu); + Check(!context.HasObjectPrimitiveIdOutput(), "disabled object ID enabled RT7"); + context.SetDrawPayloadControl(9); + context.SetObjprimIdControl(2); + Check(context.HasObjectPrimitiveIdOutput(), "object ID propagation omitted RT7"); + context.SetRenderTargetMask(0xf); + Check(!context.HasObjectPrimitiveIdOutput(), "masked RT7 remained an object ID output"); + + const uint32_t shader[] = { + EncodeVop1(0x01, 0, 0), EncodeExp0(0, 1), EncodeExp1(0, 0, 0, 0), 0xbf810000u}; + const std::array user_data {0x3f800000u}; + ShaderPixelInputInfo pixel; + pixel.target_shader_mask = 0xf; + pixel.target_output_mode[0] = 4; + const auto ordinary_key = MakeStageStaticKey(pixel); + pixel.object_primitive_id = true; + Check(MakeStageStaticKey(pixel) != ordinary_key, + "automatic object ID was omitted from the shader identity"); + auto options = MakeCompileOptions(ShaderType::Pixel); + options.input_info.pixel = &pixel; + options.user_data = user_data; + const auto prefix = PushData::GraphicsDrawDwords(false, true); + Check(prefix == 1 && PushData::GraphicsDrawDwords(false, false) == 0 && + PushData::GraphicsDrawDwords(true, false) == PushData::MeshDrawDwordCount, + "graphics draw data reserved an incorrect prefix"); + for (const uint32_t start : {prefix, PushData::DwordCount}) { + const auto result = RecompileForTest(shader, options, nullptr, nullptr, start); + Check(result.program.bindings.UsesPushData() == (start == prefix), + "object ID changed shader-data push/spill selection"); + Check(std::ranges::any_of(result.program.info.outputs, [](const StageOutput& output) { + return output.kind == StageOutputKind::Mrt && output.location == 0; + }) && std::ranges::any_of(result.program.info.outputs, [](const StageOutput& output) { + return output.kind == StageOutputKind::ObjectPrimitiveId && output.location == 7; + }), "automatic RT7 displaced or compacted the guest MRT0 export"); + const auto source = DisassembleSpirvBinary(result.spirv); + Check(source.find("BuiltIn PrimitiveId") != std::string::npos && + source.find("OpVariable %_ptr_Output_uint Output") != std::string::npos && + source.find("OpIAdd %uint") != std::string::npos, + "automatic RT7 lost its integer primitive ID addition"); + CheckSpirvBinaryValidates(result.spirv); + } +#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS + pixel.target_shader_mask = 0xf0000000u; + ExpectFatal([&] { (void)RecompileForTest(shader, options, nullptr, nullptr, prefix); }, + "automatic RT7 silently overlapped an explicit MRT7 export"); +#endif +} + void TestRenderTargetReverseExportMapping() { const auto format = TextureGetRenderTargetFormat( Prospero::ChannelLayout::k16_16_16_16, Prospero::ChannelType::kFloat, @@ -16270,6 +16321,7 @@ int main() { TestNewShaderRecompilerPerInvocationMasksWithoutMirrors(); TestNewShaderRecompilerPerInvocationU64Complement(); TestNewShaderRecompilerExpPixelOutputs(); + TestAutomaticObjectPrimitiveId(); TestRenderTargetReverseExportMapping(); TestBlendMappingClassification(); TestLogicalAlphaBlendExport();