diff --git a/src/graphics/host_gpu/renderer/debug.cpp b/src/graphics/host_gpu/renderer/debug.cpp index e42bca445..79ed9b369 100644 --- a/src/graphics/host_gpu/renderer/debug.cpp +++ b/src/graphics/host_gpu/renderer/debug.cpp @@ -939,7 +939,8 @@ static bool ScissorClipRuleToIntersectionMask(uint16_t rule, uint8_t* mask) { } ScissorRect calc_final_scissor(const HW::ScreenViewport& vp, const HW::ScanModeControl& smc, - vk::Extent2D extent) { + vk::Extent2D extent, uint32_t viewport_index) { + EXIT_IF(viewport_index >= std::size(vp.viewports)); ScissorRect screen {vp.screen_scissor_left, vp.screen_scissor_top, vp.screen_scissor_right, vp.screen_scissor_bottom}; ScissorRect final = screen; @@ -972,7 +973,7 @@ ScissorRect calc_final_scissor(const HW::ScreenViewport& vp, const HW::ScanModeC final = ScissorRectIntersect(final, generic); } - const auto& viewport = vp.viewports[0]; + const auto& viewport = vp.viewports[viewport_index]; ScissorRect viewport_scissor {viewport.viewport_scissor_left, viewport.viewport_scissor_top, viewport.viewport_scissor_right, viewport.viewport_scissor_bottom}; diff --git a/src/graphics/host_gpu/renderer/debug.h b/src/graphics/host_gpu/renderer/debug.h index d0b969761..9928a0834 100644 --- a/src/graphics/host_gpu/renderer/debug.h +++ b/src/graphics/host_gpu/renderer/debug.h @@ -39,7 +39,7 @@ void hw_print(const CommandBuffer& buffer); void hw_check(const CommandBuffer& buffer); void LogDrawPhase(const char* draw_name, const char* phase); ScissorRect calc_final_scissor(const HW::ScreenViewport& vp, const HW::ScanModeControl& smc, - vk::Extent2D extent); + vk::Extent2D extent, uint32_t viewport_index); } // namespace Libs::Graphics diff --git a/src/graphics/host_gpu/renderer/pipeline/shaders.cpp b/src/graphics/host_gpu/renderer/pipeline/shaders.cpp index 7f99b00d6..77384f32a 100644 --- a/src/graphics/host_gpu/renderer/pipeline/shaders.cpp +++ b/src/graphics/host_gpu/renderer/pipeline/shaders.cpp @@ -573,9 +573,7 @@ void CreatePipelineInternal( depth_clip_control.negativeOneToOne = (static_params.negative_one_to_one ? VK_TRUE : VK_FALSE); vk::PipelineViewportStateCreateInfo viewport_state {}; - viewport_state.pNext = &depth_clip_control; - viewport_state.viewportCount = 1; - viewport_state.scissorCount = 1; + viewport_state.pNext = &depth_clip_control; vk::CullModeFlags cull_mode = vk::CullModeFlagBits::eNone; if (static_params.cull_back) { @@ -704,8 +702,8 @@ void CreatePipelineInternal( depth_stencil_info.maxDepthBounds = static_params.depth_max_bounds; const vk::DynamicState dynamic_states[] = { - vk::DynamicState::eViewport, - vk::DynamicState::eScissor, + vk::DynamicState::eViewportWithCount, + vk::DynamicState::eScissorWithCount, vk::DynamicState::eLineWidth, vk::DynamicState::eDepthTestEnable, vk::DynamicState::eDepthWriteEnable, diff --git a/src/graphics/host_gpu/renderer/renderDraw.cpp b/src/graphics/host_gpu/renderer/renderDraw.cpp index 623211640..07b398233 100644 --- a/src/graphics/host_gpu/renderer/renderDraw.cpp +++ b/src/graphics/host_gpu/renderer/renderDraw.cpp @@ -210,7 +210,7 @@ static void LogDrawTargetState(const char* draw_name, const RenderColorInfo& col }); const auto extent = color.Extent(); - const auto sc = calc_final_scissor(vp, ctx.GetScanModeControl(), extent); + const auto sc = calc_final_scissor(vp, ctx.GetScanModeControl(), extent, 0); LOGF( "DrawTargetState[%u]: frame=%d %s target=%s addr=0x%010" PRIx64 @@ -315,6 +315,7 @@ static void LogDrawInputState(const CommandBuffer& buffer, const RenderColorInfo } static void SetGraphicsDynamicParams(const CommandBuffer& buffer, vk::CommandBuffer vk_buffer, + const ShaderVertexInputInfo& vs_input_info, const RenderColorInfo* colors, uint32_t color_count, const RenderDepthInfo& depth) { KYTY_PROFILER_FUNCTION(); @@ -333,31 +334,46 @@ static void SetGraphicsDynamicParams(const CommandBuffer& buffer, vk::CommandBuf framebuffer_extent = {limits.maxFramebufferWidth, limits.maxFramebufferHeight}; } - const auto final_scissor = calc_final_scissor(vp, ctx.GetScanModeControl(), framebuffer_extent); + const auto& outputs = vs_input_info.stage.program->info.outputs; + const bool indexed_viewports = + std::any_of(outputs.begin(), outputs.end(), [](const auto& output) { + return output.kind == ShaderRecompiler::IR::StageOutputKind::ViewportIndex; + }); + constexpr uint32_t viewport_slots = std::size(HW::ScreenViewport {}.viewports); + std::array viewports {}; + std::array scissors {}; + const uint32_t viewport_count = indexed_viewports ? viewport_slots : 1; + for (uint32_t i = 0; i < viewport_count; i++) { + const auto& guest = vp.viewports[i]; + auto& viewport = viewports[i]; + if (ctx.GetClipControl().clip_disable) { + const auto& limits = buffer.GetGraphics().GetPhysicalDeviceProperties().limits; + viewport.width = static_cast(std::min(limits.maxViewportDimensions[0], 16384u)); + viewport.height = static_cast(std::min(limits.maxViewportDimensions[1], 16384u)); + } else { + viewport.x = guest.xoffset - guest.xscale; + viewport.y = guest.yoffset - guest.yscale; + viewport.width = guest.xscale * 2.0f; + viewport.height = guest.yscale * 2.0f; + } + viewport.minDepth = + guest.zoffset - (ctx.GetClipControl().dx_clip_space ? 0.0f : guest.zscale); + viewport.maxDepth = guest.zscale + guest.zoffset; - vk::Viewport viewport {}; - if (ctx.GetClipControl().clip_disable) { - const auto& limits = buffer.GetGraphics().GetPhysicalDeviceProperties().limits; - viewport.x = 0.0f; - viewport.y = 0.0f; - viewport.width = static_cast(std::min(limits.maxViewportDimensions[0], 16384u)); - viewport.height = static_cast(std::min(limits.maxViewportDimensions[1], 16384u)); - } else { - viewport.x = vp.viewports[0].xoffset - vp.viewports[0].xscale; - viewport.y = vp.viewports[0].yoffset - vp.viewports[0].yscale; - viewport.width = vp.viewports[0].xscale * 2.0f; - viewport.height = vp.viewports[0].yscale * 2.0f; + const auto final_scissor = + calc_final_scissor(vp, ctx.GetScanModeControl(), framebuffer_extent, i); + auto& scissor = scissors[i]; + scissor.offset = {final_scissor.left, final_scissor.top}; + scissor.extent = {static_cast(final_scissor.right - final_scissor.left), + static_cast(final_scissor.bottom - final_scissor.top)}; + if (viewport.width == 0.0f) { + // Keep empty slots at their guest index; Vulkan requires a positive viewport width. + viewport.width = 1.0f; + scissor.extent = {0, 0}; + } } - viewport.minDepth = vp.viewports[0].zoffset - - (ctx.GetClipControl().dx_clip_space ? 0.0f : vp.viewports[0].zscale); - viewport.maxDepth = vp.viewports[0].zscale + vp.viewports[0].zoffset; - vk_buffer.setViewport(0, 1, &viewport); - - vk::Rect2D scissor {}; - scissor.offset = {final_scissor.left, final_scissor.top}; - scissor.extent = {static_cast(final_scissor.right - final_scissor.left), - static_cast(final_scissor.bottom - final_scissor.top)}; - vk_buffer.setScissor(0, 1, &scissor); + vk_buffer.setViewportWithCount(viewport_count, viewports.data()); + vk_buffer.setScissorWithCount(viewport_count, scissors.data()); float line_width = ctx.GetLineWidth(); if (line_width != 1.0f) { @@ -1125,8 +1141,8 @@ void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buff CommitIndexBuffer(vk_buffer, index_binding); } - SetGraphicsDynamicParams(buffer, vk_buffer, state.color_info, state.color_count, - state.depth_info); + SetGraphicsDynamicParams(buffer, vk_buffer, state.vs_input_info, state.color_info, + state.color_count, state.depth_info); LogDrawPhase(draw.name, "BeginRendering"); if (set_auto_debug) { diff --git a/src/graphics/presentation/window/vulkanWindow.cpp b/src/graphics/presentation/window/vulkanWindow.cpp index 5dff4bdb8..a8c6b5946 100644 --- a/src/graphics/presentation/window/vulkanWindow.cpp +++ b/src/graphics/presentation/window/vulkanWindow.cpp @@ -65,12 +65,13 @@ struct VulkanExtensions { vk::PhysicalDeviceVulkan12Features WindowContext::RequiredVulkan12Features() noexcept { vk::PhysicalDeviceVulkan12Features features {}; - features.sType = vk::StructureType::ePhysicalDeviceVulkan12Features; - features.samplerMirrorClampToEdge = VK_TRUE; - features.timelineSemaphore = VK_TRUE; - features.shaderOutputLayer = VK_TRUE; - features.bufferDeviceAddress = VK_TRUE; - features.shaderBufferInt64Atomics = VK_TRUE; + features.sType = vk::StructureType::ePhysicalDeviceVulkan12Features; + features.samplerMirrorClampToEdge = VK_TRUE; + features.timelineSemaphore = VK_TRUE; + features.shaderOutputLayer = VK_TRUE; + features.shaderOutputViewportIndex = VK_TRUE; + features.bufferDeviceAddress = VK_TRUE; + features.shaderBufferInt64Atomics = VK_TRUE; return features; } @@ -276,6 +277,11 @@ static void VulkanFindPhysicalDevice(vk::Instance instance, vk::SurfaceKHR surfa LOGF("shaderOutputLayer is not supported\n"); skip_device = true; } + if (required_features12.shaderOutputViewportIndex == VK_TRUE && + features12.shaderOutputViewportIndex != VK_TRUE) { + LOGF("shaderOutputViewportIndex is not supported\n"); + skip_device = true; + } if (required_features12.bufferDeviceAddress == VK_TRUE && features12.bufferDeviceAddress != VK_TRUE) { LOGF("bufferDeviceAddress is not supported\n"); @@ -320,6 +326,10 @@ static void VulkanFindPhysicalDevice(vk::Instance instance, vk::SurfaceKHR surfa LOGF("largePoints is not supported\n"); skip_device = true; } + if (device_features2.features.multiViewport != VK_TRUE) { + LOGF("multiViewport is not supported\n"); + skip_device = true; + } if (device_features2.features.fragmentStoresAndAtomics != VK_TRUE) { LOGF("fragmentStoresAndAtomics is not supported\n"); skip_device = true; @@ -636,10 +646,13 @@ static vk::Device VulkanCreateDevice(vk::PhysicalDevice physical_device, const V EXIT_NOT_IMPLEMENTED(supported_features2.features.shaderClipDistance != VK_TRUE); EXIT_NOT_IMPLEMENTED(supported_features2.features.shaderCullDistance != VK_TRUE); EXIT_NOT_IMPLEMENTED(supported_features2.features.largePoints != VK_TRUE); + EXIT_NOT_IMPLEMENTED(supported_features2.features.multiViewport != VK_TRUE); EXIT_NOT_IMPLEMENTED(supported_features2.features.shaderInt64 != VK_TRUE); EXIT_NOT_IMPLEMENTED(supported_features2.features.vertexPipelineStoresAndAtomics != VK_TRUE); EXIT_NOT_IMPLEMENTED(required_features12.shaderOutputLayer == VK_TRUE && supported_features12.shaderOutputLayer != VK_TRUE); + EXIT_NOT_IMPLEMENTED(required_features12.shaderOutputViewportIndex == VK_TRUE && + supported_features12.shaderOutputViewportIndex != VK_TRUE); EXIT_NOT_IMPLEMENTED(required_features12.bufferDeviceAddress == VK_TRUE && supported_features12.bufferDeviceAddress != VK_TRUE); EXIT_NOT_IMPLEMENTED(required_features12.shaderBufferInt64Atomics == VK_TRUE && @@ -663,6 +676,7 @@ static vk::Device VulkanCreateDevice(vk::PhysicalDevice physical_device, const V device_features.shaderClipDistance = VK_TRUE; device_features.shaderCullDistance = VK_TRUE; device_features.largePoints = VK_TRUE; + device_features.multiViewport = VK_TRUE; device_features.vertexPipelineStoresAndAtomics = VK_TRUE; graphics.sample_rate_shading_enabled = true; device_features.shaderInt64 = VK_TRUE; diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterFlow.cpp b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterFlow.cpp index 1726e415b..56cb4125d 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterFlow.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterFlow.cpp @@ -338,15 +338,24 @@ void EmitAuxPositionExport(ValueEmitContext& ctx, uint32_t data, const IR::Expor } const auto output = IR::DecodePositionExportComponent( state.input_info.vertex->pa_cl_vs_out_cntl, exp.index, component); - if (output.layer) { - const auto raw = ExportRawComponent(ctx, data, component); - const auto layer = state.builder.AllocateId(); - state.builder.AddFunction({OpBitwiseAnd, TypeU32(state), layer, raw, - ConstantU32(state, 0x7ffu)}); - const auto pointer = state.stage == ShaderType::Mesh - ? MeshOutputPointer(state, IR::StageOutputKind::Layer) - : state.layer_variable; - state.builder.AddFunction({OpStore, pointer, layer}); + if (output.layer || output.viewport) { + const auto raw = ExportRawComponent(ctx, data, component); + if (output.layer) { + const auto layer = state.builder.AllocateId(); + state.builder.AddFunction({OpBitwiseAnd, TypeU32(state), layer, raw, + ConstantU32(state, 0x7ffu)}); + const auto pointer = state.stage == ShaderType::Mesh + ? MeshOutputPointer(state, IR::StageOutputKind::Layer) + : state.layer_variable; + state.builder.AddFunction({OpStore, pointer, layer}); + } + if (output.viewport) { + // GFX10 MISC.z packs the viewport index in bits 16..19 alongside the layer. + const auto viewport = state.builder.AllocateId(); + state.builder.AddFunction({OpBitFieldUExtract, TypeU32(state), viewport, raw, + ConstantU32(state, 16), ConstantU32(state, 4)}); + state.builder.AddFunction({OpStore, state.viewport_index_variable, viewport}); + } continue; } if (!output.point_size && output.clip_distance == UINT32_MAX && diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h index b7e16504e..1ce883531 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h @@ -52,6 +52,7 @@ enum : uint32_t { CapabilityGroupNonUniformBallot = 64, CapabilityGroupNonUniformShuffle = 65, CapabilityShaderLayer = 69, + CapabilityShaderViewportIndex = 70, CapabilitySignedZeroInfNanPreserve = 4466, CapabilityFragmentBarycentricKHR = 5284, CapabilityComputeDerivativeGroupQuadsKHR = 5288, @@ -91,6 +92,7 @@ enum : uint32_t { BuiltInClipDistance = 3, BuiltInCullDistance = 4, BuiltInLayer = 9, + BuiltInViewportIndex = 10, BuiltInFragCoord = 15, BuiltInFrontFacing = 17, BuiltInSampleId = 18, @@ -391,6 +393,7 @@ struct EmitterState { uint32_t clip_distance_variable = 0; uint32_t cull_distance_variable = 0; uint32_t layer_variable = 0; + uint32_t viewport_index_variable = 0; uint32_t clip_distance_count = 0; uint32_t cull_distance_count = 0; uint32_t depth_variable = 0; diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterModule.cpp b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterModule.cpp index e48ed03a1..93b9bd1ad 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterModule.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterModule.cpp @@ -490,6 +490,10 @@ void AllocateOutputVariables(EmitterState& state) { case IR::StageOutputKind::Layer: binding.variable_id = AllocateSharedOutputVariable(state, state.layer_variable); break; + case IR::StageOutputKind::ViewportIndex: + binding.variable_id = + AllocateSharedOutputVariable(state, state.viewport_index_variable); + break; case IR::StageOutputKind::Depth: binding.variable_id = AllocateSharedOutputVariable(state, state.depth_variable); break; @@ -583,6 +587,7 @@ void AddOutputAnnotationsAndNames(EmitterState& state) { AddBuiltIn(state.clip_distance_variable, "gl_ClipDistance", BuiltInClipDistance); AddBuiltIn(state.cull_distance_variable, "gl_CullDistance", BuiltInCullDistance); AddBuiltIn(state.layer_variable, "gl_Layer", BuiltInLayer); + AddBuiltIn(state.viewport_index_variable, "gl_ViewportIndex", BuiltInViewportIndex); if (state.depth_variable != 0) { state.builder.AddName(state.depth_variable, "gl_FragDepth"); state.builder.AddAnnotation( @@ -712,6 +717,10 @@ void DefineModule(EmitterState& state) { state.builder.RequireVersion(0x00010500u); state.builder.RequireCapability(CapabilityShaderLayer); } + if (state.viewport_index_variable != 0) { + state.builder.RequireVersion(0x00010500u); + state.builder.RequireCapability(CapabilityShaderViewportIndex); + } if (InputVariableForKind(state, IR::StageInputKind::SampleId) != 0) { state.builder.RequireCapability(CapabilitySampleRateShading); } @@ -874,6 +883,11 @@ void DefineModule(EmitterState& state) { state.layer_variable, TypePointer(state, StorageClassOutput, TypeU32(state)), StorageClassOutput); } + if (state.viewport_index_variable != 0) { + state.builder.DefineGlobalVariable( + state.viewport_index_variable, TypePointer(state, StorageClassOutput, TypeU32(state)), + StorageClassOutput); + } for (const auto& binding: state.outputs) { if (binding.kind == IR::StageOutputKind::Parameter || binding.kind == IR::StageOutputKind::Mrt) { diff --git a/src/graphics/shader/recompiler/ir/ShaderIR.h b/src/graphics/shader/recompiler/ir/ShaderIR.h index 9cefb95d0..933f1a976 100644 --- a/src/graphics/shader/recompiler/ir/ShaderIR.h +++ b/src/graphics/shader/recompiler/ir/ShaderIR.h @@ -181,7 +181,8 @@ enum class StageOutputKind { PointSize, ClipDistance, CullDistance, - Layer + Layer, + ViewportIndex }; struct PositionExportComponent { diff --git a/src/graphics/shader/recompiler/ir/passes/ShaderInfoCollection.cpp b/src/graphics/shader/recompiler/ir/passes/ShaderInfoCollection.cpp index 3237672d9..db5bcf79b 100644 --- a/src/graphics/shader/recompiler/ir/passes/ShaderInfoCollection.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ShaderInfoCollection.cpp @@ -324,7 +324,7 @@ void CollectOutputs(const Program& program, const ShaderVertexInputInfo* vertex, const auto output = DecodePositionExportComponent( vertex->pa_cl_vs_out_cntl, export_info.index, component); if (output.viewport) { - return Fail("vertex viewport-index export is unsupported"); + AddOutput(info, StageOutputKind::ViewportIndex, 0, 0, "gl_ViewportIndex"); } if (output.point_size) { AddOutput(info, StageOutputKind::PointSize, 0, 0, "gl_PointSize"); diff --git a/tests/shaderCfgTests.cpp b/tests/shaderCfgTests.cpp index 7a57d04bc..771847861 100644 --- a/tests/shaderCfgTests.cpp +++ b/tests/shaderCfgTests.cpp @@ -37,6 +37,7 @@ #include #include #include +#include #include #include #include @@ -907,12 +908,12 @@ SpirvStoredBuiltInElements(const std::vector &binary, return elements; } -bool SpirvBuiltInStoreUsesAndConstant(const std::vector &binary, - uint32_t builtin, - uint32_t constant) { +bool SpirvBuiltInStoreUsesOperation(const std::vector &binary, + uint32_t builtin, uint32_t operation, + std::initializer_list literals) { std::unordered_set variables; std::unordered_map constants; - std::unordered_map> bitwise_ands; + std::unordered_map> operations; std::unordered_map stores; for (size_t i = 5; i < binary.size();) { const uint32_t opcode = binary[i] & 0xffffu; @@ -925,8 +926,8 @@ bool SpirvBuiltInStoreUsesAndConstant(const std::vector &binary, variables.insert(binary[i + 1]); } else if (opcode == 43u && count >= 4u) { constants[binary[i + 2]] = binary[i + 3]; - } else if (opcode == 199u && count == 5u) { - bitwise_ands[binary[i + 2]] = {binary[i + 3], binary[i + 4]}; + } else if (opcode == operation && count == 4u + literals.size()) { + operations[binary[i + 2]] = {binary.begin() + i + 4, binary.begin() + i + count}; } else if (opcode == 62u && count >= 3u) { stores[binary[i + 1]] = binary[i + 2]; } @@ -937,15 +938,16 @@ bool SpirvBuiltInStoreUsesAndConstant(const std::vector &binary, if (store == stores.end()) { continue; } - const auto value = bitwise_ands.find(store->second); - if (value == bitwise_ands.end()) { + const auto value = operations.find(store->second); + if (value == operations.end()) { continue; } - for (const auto operand : value->second) { - const auto literal = constants.find(operand); - if (literal != constants.end() && literal->second == constant) { - return true; - } + if (std::equal(value->second.begin(), value->second.end(), literals.begin(), + [&](uint32_t id, uint32_t expected) { + const auto literal = constants.find(id); + return literal != constants.end() && literal->second == expected; + })) { + return true; } } return false; @@ -9722,7 +9724,7 @@ void TestNewShaderRecompilerAuxPositionExports() { const auto all_source = DisassembleSpirvBinary(all.spirv); Check(all.spirv[1] == 0x00010500u && !Common::ContainsStr(all_source, "SPV_EXT_shader_viewport_index_layer") && - SpirvBuiltInStoreUsesAndConstant(all.spirv, 9u, 0x7ffu), + SpirvBuiltInStoreUsesOperation(all.spirv, 9u, 199u, {0x7ffu}), "layer export module version or GFX10 layer mask is incorrect"); const auto positions = std::count_if( all.program.info.outputs.begin(), all.program.info.outputs.end(), @@ -9776,25 +9778,23 @@ void TestNewShaderRecompilerAuxPositionExports() { ShaderRecompiler::IR::StageOutputKind::Position, "unmapped auxiliary position export was not ignored"); -#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS - const auto rejects = [](uint32_t control, uint32_t en, - const char *message) { - const uint32_t shader[] = { - EncodeExp0(0x0c, 0xf, false), EncodeExp1(0, 1, 2, 3), - EncodeExp0(0x0d, en), EncodeExp1(4, 5, 6, 7), - 0xbf810000u, - }; - ShaderVertexInputInfo vertex{}; - vertex.pa_cl_vs_out_cntl = control; - auto options = MakeCompileOptions(ShaderType::Vertex); - options.input_info.vertex = &vertex; - ExpectFatal([&] { (void)RecompileForTest(shader, options); }, - message); - }; - rejects(0x00280000u, 0x4u, - "viewport-index auxiliary position export did not terminate " - "compilation"); -#endif + // R-Type Final 3 packs a dynamic viewport index into POS1.z bits 16..19. + // The same component can carry an independent render-target layer in bits 0..10. + for (const uint32_t control : {0x01280000u, 0x012c0000u}) { + const auto indexed = compile(unmapped, control); + CheckSpirvBinaryValidates(indexed.spirv); + Check(indexed.spirv[1] == 0x00010500u && + SpirvContainsCapability(indexed.spirv, 70u) && + SpirvBuiltInStoreUsesOperation(indexed.spirv, 10u, 203u, {16u, 4u}), + "viewport export did not store the GFX10 four-bit index"); + const bool layered = (control & (1u << 18u)) != 0; + Check(SpirvBuiltInStoreUsesOperation(indexed.spirv, 9u, 199u, {0x7ffu}) == layered, + "packed layer/viewport export dropped or invented a layer store"); + Check(std::ranges::any_of(indexed.program.info.outputs, [](const auto &output) { + return output.kind == ShaderRecompiler::IR::StageOutputKind::ViewportIndex; + }), + "viewport export was omitted from shader reflection"); + } ShaderVertexInputInfo key0{}; ShaderVertexInputInfo key1{};