[Feat, Test] (MG_Backend/DirectVulkan, MG_Test, MG_IntegrationTest): rasterize the viewport gl_ViewportIndex selects, instead of collapsing all sixteen onto viewport 0

This commit is contained in:
2026-08-13 04:44:23 -04:00
parent 5fbb17f6b9
commit f41403e227
10 changed files with 947 additions and 13 deletions
@@ -206,6 +206,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
XXHASH_VERIFY(
XXH64_update(m_hashState, &payload.primitiveRestartEnable, sizeof(payload.primitiveRestartEnable)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.patchControlPoints, sizeof(payload.patchControlPoints)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.viewportCount, sizeof(payload.viewportCount)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.polygonMode, sizeof(payload.polygonMode)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.cullMode, sizeof(payload.cullMode)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.frontFace, sizeof(payload.frontFace)));
@@ -406,8 +407,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
tessellation.patchControlPoints = payload.patchControlPoints;
VkPipelineViewportStateCreateInfo vpci{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO};
vpci.viewportCount = 1;
vpci.scissorCount = 1;
// Both counts move together: GL has one scissor rectangle per viewport, and Vulkan
// requires viewportCount == scissorCount whenever both are dynamic
// (VUID-VkPipelineViewportStateCreateInfo-scissorCount-04136). The caller has already
// clamped this to the device's multiViewport capability.
vpci.viewportCount = std::max<Uint32>(payload.viewportCount, 1u);
vpci.scissorCount = vpci.viewportCount;
VkPipelineRasterizationStateCreateInfo raster{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO};
raster.polygonMode = payload.polygonMode;
@@ -42,6 +42,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool primitiveRestartEnable = false;
// GL_PATCH_VERTICES; only read for a PATCH_LIST topology.
Uint32 patchControlPoints = 3;
// How many of ARB_viewport_array's viewports this pipeline rasterizes into. 1 for
// every program that never assigns gl_ViewportIndex, which is all of them outside the
// conformance suite - the wide shape costs a longer vkCmdSetViewport/Scissor per state
// change and can cost hardware fast paths, so it is opt-in per program. Baked into the
// pipeline (viewportCount is not dynamic without VK_EXT_extended_dynamic_state) and
// therefore hashed; the DYNAMIC viewport/scissor arrays the draw pushes must have
// exactly this many elements (VUID-vkCmdDraw-viewportCount-03417/-03418).
Uint32 viewportCount = 1;
VkPolygonMode polygonMode = VK_POLYGON_MODE_FILL;
VkCullModeFlags cullMode = VK_CULL_MODE_BACK_BIT;
VkFrontFace frontFace = VK_FRONT_FACE_CLOCKWISE;
@@ -1997,6 +1997,29 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return ReflectedDeclaresInputBuiltin(reflectModule, SpvBuiltInBaseVertex);
}
// gl_ViewportIndex on the last pre-rasterization stage. glslang emits it natively for Vulkan
// (BuiltIn ViewportIndex plus OpCapability MultiViewport), and nothing in the SpirvPasses
// chain touches it, so a plain reflection of the declared output builtins is the whole test.
Bool ProgramFactory::ReflectedWritesViewportIndexBuiltin(const SpvReflectShaderModule& reflectModule) {
return ReflectedDeclaresOutputBuiltin(reflectModule, SpvBuiltInViewportIndex);
}
Bool ProgramFactory::ReflectedDeclaresOutputBuiltin(const SpvReflectShaderModule& reflectModule,
SpvBuiltIn builtin) {
for (Uint32 entryIndex = 0; entryIndex < reflectModule.entry_point_count; ++entryIndex) {
const SpvReflectEntryPoint& entryPoint = reflectModule.entry_points[entryIndex];
for (Uint32 variableIndex = 0; variableIndex < entryPoint.output_variable_count; ++variableIndex) {
const SpvReflectInterfaceVariable* variable = entryPoint.output_variables[variableIndex];
if (variable != nullptr &&
(variable->decoration_flags & SPV_REFLECT_DECORATION_BUILT_IN) != 0 &&
variable->built_in == builtin) {
return true;
}
}
}
return false;
}
Bool ProgramFactory::ReflectedDeclaresInputBuiltin(const SpvReflectShaderModule& reflectModule,
SpvBuiltIn builtin) {
for (Uint32 entryIndex = 0; entryIndex < reflectModule.entry_point_count; ++entryIndex) {
@@ -2339,6 +2362,46 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
// Which pre-rasterization stage assigns gl_ViewportIndex is not fixed: GL 4.1 allows only the
// geometry stage, ARB_shader_viewport_layer_array/GL 4.6 also the vertex and tessellation
// evaluation stages. Rather than guess which one is last, every non-fragment, non-compute
// module is asked - one writer anywhere means this program's draws need a multi-viewport
// pipeline, and a false positive costs only a wider viewportCount.
void ProgramFactory::ReflectViewportIndexUsage(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
const Vector<Vector<Uint>>& spirv,
VkProgramObject& entry) const {
entry.writesViewportIndexBuiltin = false;
for (SizeT moduleIndex = 0; moduleIndex < shaders.size() && moduleIndex < spirv.size(); ++moduleIndex) {
if (!shaders[moduleIndex]) continue;
const ShaderStage stage = shaders[moduleIndex]->GetShaderStage();
if (stage == ShaderStage::Fragment || stage == ShaderStage::Compute) continue;
const auto& module = spirv[moduleIndex];
if (module.empty()) continue;
SpvReflectShaderModule reflectModule{};
const SpvReflectResult createResult =
spvReflectCreateShaderModule(module.size() * sizeof(Uint), module.data(), &reflectModule);
if (createResult != SPV_REFLECT_RESULT_SUCCESS) {
// Fail toward the wide pipeline. Missing a real gl_ViewportIndex writer would
// silently collapse every viewport onto 0 (the exact bug this reflection exists
// to fix); over-declaring costs one extra viewport slot on a program that never
// uses it.
MGLOG_E_ONCE("ProgramFactory::ReflectViewportIndexUsage: reflection failed (result=%d); assuming the "
"program writes gl_ViewportIndex",
static_cast<Int>(createResult));
entry.writesViewportIndexBuiltin = true;
continue;
}
if (ReflectedWritesViewportIndexBuiltin(reflectModule)) {
entry.writesViewportIndexBuiltin = true;
}
spvReflectDestroyShaderModule(&reflectModule);
}
}
void ProgramFactory::ReflectFragmentOutputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
const Vector<Vector<Uint>>& spirv,
VkProgramObject& entry) const {
@@ -3189,6 +3252,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
ValidateRasterizationStageInterface(shaders, moduleSpirvs, entry, program.GetExternalIndex());
#endif
ReflectVertexInputs(shaders, moduleSpirvs, entry);
ReflectViewportIndexUsage(shaders, moduleSpirvs, entry);
ReflectFragmentOutputs(shaders, moduleSpirvs, entry);
ReflectPassthroughTessControlNeed(shaders, moduleSpirvs, entry);
ReflectLayout(program, moduleSpirvs, entry);
@@ -151,6 +151,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// PROGRAM rather than of the variant: the zeroed variant leaves the variable
// declared, so both variants answer the same and the draw path can ask either.
Bool readsBaseVertexBuiltin = false;
// Some pre-rasterization stage assigns gl_ViewportIndex. Its pipeline declares
// viewportCount = the renderer's rasterizable viewport count instead of 1, and its
// draws push the whole viewport/scissor array; every other program keeps the
// single-viewport fast path untouched. Part of the program's identity (folded into
// the pipeline hash through programHash), so no memo can serve the wrong shape.
Bool writesViewportIndexBuiltin = false;
// This program has a tessellation EVALUATION stage and no tessellation CONTROL
// stage. GL allows that (4.6 core 11.2.2: with no control shader the input patch
// is passed through unmodified, the output patch size is PATCH_VERTICES, and the
@@ -400,6 +406,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Shared by the two above: does any entry point list an input variable decorated with
// this builtin?
static Bool ReflectedDeclaresInputBuiltin(const SpvReflectShaderModule& reflectModule, SpvBuiltIn builtin);
// True when an entry point writes the ViewportIndex builtin (gl_ViewportIndex), i.e. when
// the program can route primitives to a viewport other than 0 and its pipeline therefore
// has to declare more than one. Asks about OUTPUT variables because that is the direction
// a pre-rasterization stage declares it in.
static Bool ReflectedWritesViewportIndexBuiltin(const SpvReflectShaderModule& reflectModule);
static Bool ReflectedDeclaresOutputBuiltin(const SpvReflectShaderModule& reflectModule, SpvBuiltIn builtin);
// The pass-through tessellation control stage GL 4.6 core 11.2.2 describes for a
// program that has an evaluation stage and no control stage, for an input patch of
@@ -434,6 +446,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void ReflectVertexInputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
const Vector<Vector<Uint>>& spirv,
VkProgramObject& entry) const;
void ReflectViewportIndexUsage(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
const Vector<Vector<Uint>>& spirv,
VkProgramObject& entry) const;
void ReflectFragmentOutputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
const Vector<Vector<Uint>>& spirv,
VkProgramObject& entry) const;
@@ -437,12 +437,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
vkCmdSetScissor(commandBuffer, 0, 1, &scissor);
}
static void ApplyGLViewportState(VkCommandBuffer commandBuffer,
const IntVec2& framebufferExtent,
VkSurfaceTransformFlagBitsKHR preTransform,
Bool isDefaultFramebuffer) {
const IntVec4& viewportState = MG_State::pGLContext->GetViewport();
const FloatVec2& depthRange = MG_State::pGLContext->GetDepthRange();
// One viewport of the ARB_viewport_array state, mapped into Vulkan's frame. Split out of
// ApplyGLViewportState so the multi-viewport path derives index i through EXACTLY the same
// arithmetic as index 0 - the default-framebuffer Y-flip and pre-transform rotation
// especially, which is the classic way a multi-viewport port comes out upside down for every
// index but the one that was tested.
static VkViewport ComputeGLViewport(Uint32 index,
const IntVec2& framebufferExtent,
VkSurfaceTransformFlagBitsKHR preTransform,
Bool isDefaultFramebuffer) {
// Rounded to integers on purpose: MobileGL advertises GL_VIEWPORT_SUBPIXEL_BITS = 0, so
// the fractional rectangle glViewportIndexedf can store is exact as STATE and snapped
// when it rasterizes.
const FloatVec4& stored = MG_State::pGLContext->GetViewportIndexed(index);
const IntVec4 viewportState(static_cast<Int>(std::lround(stored.x())),
static_cast<Int>(std::lround(stored.y())),
static_cast<Int>(std::lround(stored.z())),
static_cast<Int>(std::lround(stored.w())));
const FloatVec2& depthRange = MG_State::pGLContext->GetDepthRangeIndexed(index);
const IntVec2 logicalExtent = isDefaultFramebuffer
? ResolveDefaultFramebufferLogicalExtent(preTransform, framebufferExtent)
: framebufferExtent;
@@ -477,6 +489,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
viewport.height = static_cast<float>(viewportHeight);
viewport.minDepth = depthRange.x();
viewport.maxDepth = depthRange.y();
return viewport;
}
static void ApplyGLViewportState(VkCommandBuffer commandBuffer,
const IntVec2& framebufferExtent,
VkSurfaceTransformFlagBitsKHR preTransform,
Bool isDefaultFramebuffer) {
const VkViewport viewport = ComputeGLViewport(0, framebufferExtent, preTransform, isDefaultFramebuffer);
auto& shadow = g_dynamicStateShadow;
if (shadow.viewportValid && shadow.viewport.x == viewport.x && shadow.viewport.y == viewport.y &&
shadow.viewport.width == viewport.width && shadow.viewport.height == viewport.height &&
@@ -4879,6 +4899,7 @@ void main() {
.topology = vkTopology,
.primitiveRestartEnable = primitiveRestartEnabled,
.patchControlPoints = static_cast<Uint32>(MG_State::pGLContext->GetPatchVertices()),
.viewportCount = ResolveDrawViewportCount(programObj.writesViewportIndexBuiltin),
.polygonMode = effectivePolygonMode,
.cullMode = cullFaceEnabled
? MG_Util::ConvertCullFaceModeToVkEnum(MG_State::pGLContext->GetCullFaceMode(), invertClockwise)
@@ -5321,9 +5342,71 @@ void main() {
}
void VulkanRenderer::ApplyDynamicDrawStateTail(FrameContext::FrameData& frame, const IntVec2& extent,
Bool isDefaultFbo) {
// The scissor rectangle Vulkan needs for ARB_viewport_array index `index`. Vulkan has no
// per-viewport scissor-test TOGGLE - a scissor rectangle always applies - so an index whose
// GL scissor test is disabled gets the whole framebuffer, which is exactly "the test always
// passes" (GL 4.6 core 17.3.2).
VkRect2D VulkanRenderer::ComputeGLScissorRect(Uint32 index, const IntVec2& extent,
VkSurfaceTransformFlagBitsKHR preTransform,
Bool isDefaultFbo) const {
const auto& parameters = MG_State::pGLContext->GetRenderStateParameters();
if ((parameters.ScissorTestEnabledMask & (1u << index)) == 0) {
VkRect2D full{};
full.offset = {0, 0};
full.extent = {static_cast<Uint32>(extent.x()), static_cast<Uint32>(extent.y())};
return full;
}
const IntVec4& scissorBox = parameters.ScissorBoxes[index];
return isDefaultFbo ? MakeDefaultFramebufferScissorRect(scissorBox, extent, preTransform)
: MakeClampedScissorRect(scissorBox, extent);
}
// The wide half of ApplyDynamicDrawStateTail: a pipeline built for a gl_ViewportIndex-writing
// program declares viewportCount > 1, and Vulkan then requires that many viewports AND that
// many scissors to have been set before the draw
// (VUID-vkCmdDraw-viewportCount-03417/-03418). Deliberately unmemoized: only conformance
// shaders reach it, the single-element dynamic-state shadow cannot describe an array, and
// leaving that shadow invalidated is what makes the next ordinary draw re-push its own
// single viewport instead of believing the array's element 0 is already bound.
void VulkanRenderer::ApplyMultiViewportDynamicState(VkCommandBuffer commandBuffer, Uint32 viewportCount,
const IntVec2& extent,
VkSurfaceTransformFlagBitsKHR preTransform,
Bool isDefaultFbo) {
MOBILEGL_ASSERT(viewportCount <= RenderStateParameters::MAX_VIEWPORTS,
"ApplyMultiViewportDynamicState: viewportCount=%u exceeds the indexed state width",
viewportCount);
const Uint32 count = std::min<Uint32>(viewportCount, RenderStateParameters::MAX_VIEWPORTS);
Array<VkViewport, RenderStateParameters::MAX_VIEWPORTS> viewports{};
Array<VkRect2D, RenderStateParameters::MAX_VIEWPORTS> scissors{};
for (Uint32 i = 0; i < count; ++i) {
viewports[i] = ComputeGLViewport(i, extent, preTransform, isDefaultFbo);
scissors[i] = ComputeGLScissorRect(i, extent, preTransform, isDefaultFbo);
}
vkCmdSetViewport(commandBuffer, 0, count, viewports.data());
vkCmdSetScissor(commandBuffer, 0, count, scissors.data());
auto& shadow = g_dynamicStateShadow;
shadow.viewportValid = false;
shadow.scissorValid = false;
shadow.dynamicTailValid = false;
}
void VulkanRenderer::ApplyDynamicDrawStateTail(FrameContext::FrameData& frame, const IntVec2& extent,
Bool isDefaultFbo, Uint32 viewportCount) {
auto& shadow = g_dynamicStateShadow;
if (viewportCount > 1) {
// The other five Apply* still run: blend constants, depth bias, line width and the
// stencil masks are not per-viewport and a multi-viewport draw needs them just as
// much. Only the viewport/scissor pair takes the array shape.
ApplyBlendConstants(frame.commandBuffer);
ApplyPolygonOffsetState(frame.commandBuffer);
ApplyLineWidthState(frame.commandBuffer);
ApplyStencilState(frame.commandBuffer);
ApplyMultiViewportDynamicState(frame.commandBuffer, viewportCount, extent,
m_swapchainObject.GetPreTransform(), isDefaultFbo);
return;
}
// One compare for the whole tail: see the gate's declaration in
// DynamicStateShadow for why (version, extent, default-FBO flag) pins every
// input the six Apply* below read.
@@ -5761,7 +5844,7 @@ void main() {
const Bool idxUploadOk = UploadAndBindIndexBuffer(frame, vao, pIndexBufferView);
MOBILEGL_ASSERT(idxUploadOk, "SetupDraw fast path: failed to upload index buffer");
}
ApplyDynamicDrawStateTail(frame, snap.renderPassExtent, snap.drawFboIsDefault);
ApplyDynamicDrawStateTail(frame, snap.renderPassExtent, snap.drawFboIsDefault, snap.viewportCount);
return true;
}
@@ -6201,7 +6284,8 @@ void main() {
MOBILEGL_ASSERT(idxUploadOk, "SetupDraw skipped: failed to upload index buffer");
}
ApplyDynamicDrawStateTail(frame, renderPassEntry->extent, drawFbo->IsDefaultFramebuffer());
ApplyDynamicDrawStateTail(frame, renderPassEntry->extent, drawFbo->IsDefaultFramebuffer(),
ResolveDrawViewportCount(programObj.writesViewportIndexBuiltin));
// Snapshot the fully resolved configuration for the consecutive-draw
// fast path (see TrySetupDrawFastPath).
@@ -6220,6 +6304,7 @@ void main() {
snap.drawFbo = drawFbo.get();
snap.fboVersion = drawFbo->GetObjectVersion();
snap.drawFboIsDefault = drawFboIsDefault;
snap.viewportCount = ResolveDrawViewportCount(programObj.writesViewportIndexBuiltin);
snap.renderStateVersion = MG_State::pGLContext->GetPipelineStateVersion();
snap.bindGeneration = MG_State::pGLContext->GetTextureBindGeneration();
snap.baseTransformFlags = GetBaseTransformFlagsRaw(drawFboIsDefault);
@@ -12216,6 +12301,28 @@ void main() {
m_fillModeNonSolidFeatureEnabled = deviceFeatures.fillModeNonSolid == VK_TRUE;
deviceFeatures.dualSrcBlend = supportedDeviceFeatures.dualSrcBlend;
m_dualSrcBlendFeatureEnabled = deviceFeatures.dualSrcBlend == VK_TRUE;
// ARB_viewport_array rasterization. Without multiViewport a pipeline may declare exactly
// one viewport (VUID-VkPipelineViewportStateCreateInfo-viewportCount-01216), so a shader's
// gl_ViewportIndex can only ever select viewport 0 and the other fifteen rectangles are
// state with nowhere to go. The GL state stays 16 wide either way - GL 4.3 core requires
// MAX_VIEWPORTS >= 16 and that is a frontend promise, not a device one; this gate decides
// only whether a DRAW can rasterize into more than one of them.
deviceFeatures.multiViewport = supportedDeviceFeatures.multiViewport;
m_multiViewportFeatureEnabled = deviceFeatures.multiViewport == VK_TRUE;
m_maxRasterizableViewports =
m_multiViewportFeatureEnabled
? std::min<Uint32>(RenderStateParameters::MAX_VIEWPORTS,
std::max<Uint32>(m_physicalDevice.properties.limits.maxViewports, 1u))
: 1u;
MGLOG_I("Vulkan: multiViewport %s; rasterizable viewports=%u (device limit %u, GL state width %u)",
m_multiViewportFeatureEnabled ? "enabled" : "UNAVAILABLE", m_maxRasterizableViewports,
m_physicalDevice.properties.limits.maxViewports,
static_cast<Uint32>(RenderStateParameters::MAX_VIEWPORTS));
if (!m_multiViewportFeatureEnabled) {
MGLOG_W("Vulkan: the device does not support the multiViewport feature; gl_ViewportIndex will always "
"select viewport 0 and per-viewport scissor/depth-range state past index 0 cannot be "
"rasterized (the state itself is still stored and queryable)");
}
deviceFeatures.logicOp = supportedDeviceFeatures.logicOp;
deviceFeatures.shaderClipDistance = supportedDeviceFeatures.shaderClipDistance;
deviceFeatures.shaderCullDistance = supportedDeviceFeatures.shaderCullDistance;
@@ -547,6 +547,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// needs no feature). Both cached at device creation and drive a hard-fail-at-draw when absent.
Bool m_dualSrcBlendFeatureEnabled = false;
Bool m_primitiveTopologyListRestartFeatureEnabled = false;
// multiViewport gates rasterizing into more than one of ARB_viewport_array's 16 viewports
// (gl_ViewportIndex). m_maxRasterizableViewports is min(MAX_VIEWPORTS, device limit), or 1
// when the feature is off, and is the viewportCount a gl_ViewportIndex-writing pipeline
// declares - it is NOT what GL_MAX_VIEWPORTS reports, which is the frontend state width.
Bool m_multiViewportFeatureEnabled = false;
Uint32 m_maxRasterizableViewports = 1;
// Union of shader stages sampled-read barriers may name; built at device creation
// because geometry/tessellation stage bits are invalid in a barrier when their
// feature is off (VUID-vkCmdPipelineBarrier-srcStageMask-04090/-04091), and
@@ -830,6 +836,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// re-resolve just the pipeline against the active pass; a change that
// flips it must fall back to the full path's pass selection.
Bool drawUsesDepthStencil = false;
// The snapshotting draw's pipeline viewportCount. A pure function of the PROGRAM
// (writesViewportIndexBuiltin) and of a device feature fixed at renderer init, both
// of which the programLifetimeId/programVersion guards above already pin - carried
// here so the fast path does not re-fetch the program object to re-derive it.
Uint32 viewportCount = 1;
IntVec2 renderPassExtent = {0, 0};
// colorAttachmentCount of the snapshotting draw's render pass: the
// pipeline-state hash input, so the fast path can refresh that hash and
@@ -1120,7 +1131,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// The per-draw dynamic-state tail (viewport, scissor, blend constants, depth
// bias, line width, stencil), gated behind one render-state-parameters-version
// compare per command buffer - see the gate fields in DynamicStateShadow.
void ApplyDynamicDrawStateTail(FrameContext::FrameData& frame, const IntVec2& extent, Bool isDefaultFbo);
// viewportCount is the bound pipeline's declared viewport count: 1 for every program that
// does not write gl_ViewportIndex (the memoized fast path), otherwise the renderer's
// rasterizable viewport count, which takes the unmemoized array path.
void ApplyDynamicDrawStateTail(FrameContext::FrameData& frame, const IntVec2& extent, Bool isDefaultFbo,
Uint32 viewportCount = 1);
void ApplyMultiViewportDynamicState(VkCommandBuffer commandBuffer, Uint32 viewportCount, const IntVec2& extent,
VkSurfaceTransformFlagBitsKHR preTransform, Bool isDefaultFbo);
VkRect2D ComputeGLScissorRect(Uint32 index, const IntVec2& extent,
VkSurfaceTransformFlagBitsKHR preTransform, Bool isDefaultFbo) const;
// How many viewports a draw with this program rasterizes into: 1 unless the program
// assigns gl_ViewportIndex AND the device enabled multiViewport. Both the pipeline's
// baked viewportCount and the dynamic arrays come from this one answer, so they cannot
// disagree.
Uint32 ResolveDrawViewportCount(Bool programWritesViewportIndex) const {
return programWritesViewportIndex && m_multiViewportFeatureEnabled ? m_maxRasterizableViewports : 1u;
}
Bool UploadAndBindVertexBuffers(VkCommandBuffer commandBuffer, const MG_State::GLState::VertexArrayObject& vao,
const ProgramFactory::VkProgramObject& programObj,