[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
@@ -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;