mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-11 13:48:30 +09:00
[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:
@@ -206,6 +206,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
XXHASH_VERIFY(
|
XXHASH_VERIFY(
|
||||||
XXH64_update(m_hashState, &payload.primitiveRestartEnable, sizeof(payload.primitiveRestartEnable)));
|
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.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.polygonMode, sizeof(payload.polygonMode)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.cullMode, sizeof(payload.cullMode)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.cullMode, sizeof(payload.cullMode)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.frontFace, sizeof(payload.frontFace)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.frontFace, sizeof(payload.frontFace)));
|
||||||
@@ -406,8 +407,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
tessellation.patchControlPoints = payload.patchControlPoints;
|
tessellation.patchControlPoints = payload.patchControlPoints;
|
||||||
|
|
||||||
VkPipelineViewportStateCreateInfo vpci{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO};
|
VkPipelineViewportStateCreateInfo vpci{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO};
|
||||||
vpci.viewportCount = 1;
|
// Both counts move together: GL has one scissor rectangle per viewport, and Vulkan
|
||||||
vpci.scissorCount = 1;
|
// 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};
|
VkPipelineRasterizationStateCreateInfo raster{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO};
|
||||||
raster.polygonMode = payload.polygonMode;
|
raster.polygonMode = payload.polygonMode;
|
||||||
|
|||||||
@@ -42,6 +42,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Bool primitiveRestartEnable = false;
|
Bool primitiveRestartEnable = false;
|
||||||
// GL_PATCH_VERTICES; only read for a PATCH_LIST topology.
|
// GL_PATCH_VERTICES; only read for a PATCH_LIST topology.
|
||||||
Uint32 patchControlPoints = 3;
|
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;
|
VkPolygonMode polygonMode = VK_POLYGON_MODE_FILL;
|
||||||
VkCullModeFlags cullMode = VK_CULL_MODE_BACK_BIT;
|
VkCullModeFlags cullMode = VK_CULL_MODE_BACK_BIT;
|
||||||
VkFrontFace frontFace = VK_FRONT_FACE_CLOCKWISE;
|
VkFrontFace frontFace = VK_FRONT_FACE_CLOCKWISE;
|
||||||
|
|||||||
@@ -1997,6 +1997,29 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return ReflectedDeclaresInputBuiltin(reflectModule, SpvBuiltInBaseVertex);
|
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,
|
Bool ProgramFactory::ReflectedDeclaresInputBuiltin(const SpvReflectShaderModule& reflectModule,
|
||||||
SpvBuiltIn builtin) {
|
SpvBuiltIn builtin) {
|
||||||
for (Uint32 entryIndex = 0; entryIndex < reflectModule.entry_point_count; ++entryIndex) {
|
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,
|
void ProgramFactory::ReflectFragmentOutputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||||
const Vector<Vector<Uint>>& spirv,
|
const Vector<Vector<Uint>>& spirv,
|
||||||
VkProgramObject& entry) const {
|
VkProgramObject& entry) const {
|
||||||
@@ -3189,6 +3252,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
ValidateRasterizationStageInterface(shaders, moduleSpirvs, entry, program.GetExternalIndex());
|
ValidateRasterizationStageInterface(shaders, moduleSpirvs, entry, program.GetExternalIndex());
|
||||||
#endif
|
#endif
|
||||||
ReflectVertexInputs(shaders, moduleSpirvs, entry);
|
ReflectVertexInputs(shaders, moduleSpirvs, entry);
|
||||||
|
ReflectViewportIndexUsage(shaders, moduleSpirvs, entry);
|
||||||
ReflectFragmentOutputs(shaders, moduleSpirvs, entry);
|
ReflectFragmentOutputs(shaders, moduleSpirvs, entry);
|
||||||
ReflectPassthroughTessControlNeed(shaders, moduleSpirvs, entry);
|
ReflectPassthroughTessControlNeed(shaders, moduleSpirvs, entry);
|
||||||
ReflectLayout(program, 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
|
// 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.
|
// declared, so both variants answer the same and the draw path can ask either.
|
||||||
Bool readsBaseVertexBuiltin = false;
|
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
|
// 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
|
// 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
|
// 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
|
// Shared by the two above: does any entry point list an input variable decorated with
|
||||||
// this builtin?
|
// this builtin?
|
||||||
static Bool ReflectedDeclaresInputBuiltin(const SpvReflectShaderModule& reflectModule, SpvBuiltIn 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
|
// 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
|
// 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,
|
void ReflectVertexInputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||||
const Vector<Vector<Uint>>& spirv,
|
const Vector<Vector<Uint>>& spirv,
|
||||||
VkProgramObject& entry) const;
|
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,
|
void ReflectFragmentOutputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||||
const Vector<Vector<Uint>>& spirv,
|
const Vector<Vector<Uint>>& spirv,
|
||||||
VkProgramObject& entry) const;
|
VkProgramObject& entry) const;
|
||||||
|
|||||||
@@ -437,12 +437,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
vkCmdSetScissor(commandBuffer, 0, 1, &scissor);
|
vkCmdSetScissor(commandBuffer, 0, 1, &scissor);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void ApplyGLViewportState(VkCommandBuffer commandBuffer,
|
// One viewport of the ARB_viewport_array state, mapped into Vulkan's frame. Split out of
|
||||||
const IntVec2& framebufferExtent,
|
// ApplyGLViewportState so the multi-viewport path derives index i through EXACTLY the same
|
||||||
VkSurfaceTransformFlagBitsKHR preTransform,
|
// arithmetic as index 0 - the default-framebuffer Y-flip and pre-transform rotation
|
||||||
Bool isDefaultFramebuffer) {
|
// especially, which is the classic way a multi-viewport port comes out upside down for every
|
||||||
const IntVec4& viewportState = MG_State::pGLContext->GetViewport();
|
// index but the one that was tested.
|
||||||
const FloatVec2& depthRange = MG_State::pGLContext->GetDepthRange();
|
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
|
const IntVec2 logicalExtent = isDefaultFramebuffer
|
||||||
? ResolveDefaultFramebufferLogicalExtent(preTransform, framebufferExtent)
|
? ResolveDefaultFramebufferLogicalExtent(preTransform, framebufferExtent)
|
||||||
: framebufferExtent;
|
: framebufferExtent;
|
||||||
@@ -477,6 +489,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
viewport.height = static_cast<float>(viewportHeight);
|
viewport.height = static_cast<float>(viewportHeight);
|
||||||
viewport.minDepth = depthRange.x();
|
viewport.minDepth = depthRange.x();
|
||||||
viewport.maxDepth = depthRange.y();
|
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;
|
auto& shadow = g_dynamicStateShadow;
|
||||||
if (shadow.viewportValid && shadow.viewport.x == viewport.x && shadow.viewport.y == viewport.y &&
|
if (shadow.viewportValid && shadow.viewport.x == viewport.x && shadow.viewport.y == viewport.y &&
|
||||||
shadow.viewport.width == viewport.width && shadow.viewport.height == viewport.height &&
|
shadow.viewport.width == viewport.width && shadow.viewport.height == viewport.height &&
|
||||||
@@ -4879,6 +4899,7 @@ void main() {
|
|||||||
.topology = vkTopology,
|
.topology = vkTopology,
|
||||||
.primitiveRestartEnable = primitiveRestartEnabled,
|
.primitiveRestartEnable = primitiveRestartEnabled,
|
||||||
.patchControlPoints = static_cast<Uint32>(MG_State::pGLContext->GetPatchVertices()),
|
.patchControlPoints = static_cast<Uint32>(MG_State::pGLContext->GetPatchVertices()),
|
||||||
|
.viewportCount = ResolveDrawViewportCount(programObj.writesViewportIndexBuiltin),
|
||||||
.polygonMode = effectivePolygonMode,
|
.polygonMode = effectivePolygonMode,
|
||||||
.cullMode = cullFaceEnabled
|
.cullMode = cullFaceEnabled
|
||||||
? MG_Util::ConvertCullFaceModeToVkEnum(MG_State::pGLContext->GetCullFaceMode(), invertClockwise)
|
? MG_Util::ConvertCullFaceModeToVkEnum(MG_State::pGLContext->GetCullFaceMode(), invertClockwise)
|
||||||
@@ -5321,9 +5342,71 @@ void main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void VulkanRenderer::ApplyDynamicDrawStateTail(FrameContext::FrameData& frame, const IntVec2& extent,
|
// The scissor rectangle Vulkan needs for ARB_viewport_array index `index`. Vulkan has no
|
||||||
Bool isDefaultFbo) {
|
// 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;
|
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
|
// One compare for the whole tail: see the gate's declaration in
|
||||||
// DynamicStateShadow for why (version, extent, default-FBO flag) pins every
|
// DynamicStateShadow for why (version, extent, default-FBO flag) pins every
|
||||||
// input the six Apply* below read.
|
// input the six Apply* below read.
|
||||||
@@ -5761,7 +5844,7 @@ void main() {
|
|||||||
const Bool idxUploadOk = UploadAndBindIndexBuffer(frame, vao, pIndexBufferView);
|
const Bool idxUploadOk = UploadAndBindIndexBuffer(frame, vao, pIndexBufferView);
|
||||||
MOBILEGL_ASSERT(idxUploadOk, "SetupDraw fast path: failed to upload index buffer");
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6201,7 +6284,8 @@ void main() {
|
|||||||
MOBILEGL_ASSERT(idxUploadOk, "SetupDraw skipped: failed to upload index buffer");
|
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
|
// Snapshot the fully resolved configuration for the consecutive-draw
|
||||||
// fast path (see TrySetupDrawFastPath).
|
// fast path (see TrySetupDrawFastPath).
|
||||||
@@ -6220,6 +6304,7 @@ void main() {
|
|||||||
snap.drawFbo = drawFbo.get();
|
snap.drawFbo = drawFbo.get();
|
||||||
snap.fboVersion = drawFbo->GetObjectVersion();
|
snap.fboVersion = drawFbo->GetObjectVersion();
|
||||||
snap.drawFboIsDefault = drawFboIsDefault;
|
snap.drawFboIsDefault = drawFboIsDefault;
|
||||||
|
snap.viewportCount = ResolveDrawViewportCount(programObj.writesViewportIndexBuiltin);
|
||||||
snap.renderStateVersion = MG_State::pGLContext->GetPipelineStateVersion();
|
snap.renderStateVersion = MG_State::pGLContext->GetPipelineStateVersion();
|
||||||
snap.bindGeneration = MG_State::pGLContext->GetTextureBindGeneration();
|
snap.bindGeneration = MG_State::pGLContext->GetTextureBindGeneration();
|
||||||
snap.baseTransformFlags = GetBaseTransformFlagsRaw(drawFboIsDefault);
|
snap.baseTransformFlags = GetBaseTransformFlagsRaw(drawFboIsDefault);
|
||||||
@@ -12216,6 +12301,28 @@ void main() {
|
|||||||
m_fillModeNonSolidFeatureEnabled = deviceFeatures.fillModeNonSolid == VK_TRUE;
|
m_fillModeNonSolidFeatureEnabled = deviceFeatures.fillModeNonSolid == VK_TRUE;
|
||||||
deviceFeatures.dualSrcBlend = supportedDeviceFeatures.dualSrcBlend;
|
deviceFeatures.dualSrcBlend = supportedDeviceFeatures.dualSrcBlend;
|
||||||
m_dualSrcBlendFeatureEnabled = deviceFeatures.dualSrcBlend == VK_TRUE;
|
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.logicOp = supportedDeviceFeatures.logicOp;
|
||||||
deviceFeatures.shaderClipDistance = supportedDeviceFeatures.shaderClipDistance;
|
deviceFeatures.shaderClipDistance = supportedDeviceFeatures.shaderClipDistance;
|
||||||
deviceFeatures.shaderCullDistance = supportedDeviceFeatures.shaderCullDistance;
|
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.
|
// needs no feature). Both cached at device creation and drive a hard-fail-at-draw when absent.
|
||||||
Bool m_dualSrcBlendFeatureEnabled = false;
|
Bool m_dualSrcBlendFeatureEnabled = false;
|
||||||
Bool m_primitiveTopologyListRestartFeatureEnabled = 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
|
// 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
|
// because geometry/tessellation stage bits are invalid in a barrier when their
|
||||||
// feature is off (VUID-vkCmdPipelineBarrier-srcStageMask-04090/-04091), and
|
// 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
|
// re-resolve just the pipeline against the active pass; a change that
|
||||||
// flips it must fall back to the full path's pass selection.
|
// flips it must fall back to the full path's pass selection.
|
||||||
Bool drawUsesDepthStencil = false;
|
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};
|
IntVec2 renderPassExtent = {0, 0};
|
||||||
// colorAttachmentCount of the snapshotting draw's render pass: the
|
// colorAttachmentCount of the snapshotting draw's render pass: the
|
||||||
// pipeline-state hash input, so the fast path can refresh that hash and
|
// 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
|
// The per-draw dynamic-state tail (viewport, scissor, blend constants, depth
|
||||||
// bias, line width, stencil), gated behind one render-state-parameters-version
|
// bias, line width, stencil), gated behind one render-state-parameters-version
|
||||||
// compare per command buffer - see the gate fields in DynamicStateShadow.
|
// 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,
|
Bool UploadAndBindVertexBuffers(VkCommandBuffer commandBuffer, const MG_State::GLState::VertexArrayObject& vao,
|
||||||
const ProgramFactory::VkProgramObject& programObj,
|
const ProgramFactory::VkProgramObject& programObj,
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ add_executable(MobileGLIntegrationTest
|
|||||||
Scenarios/DepthStencilReadbackMatrixScenario.cpp
|
Scenarios/DepthStencilReadbackMatrixScenario.cpp
|
||||||
Scenarios/DepthStencilReadbackAttachmentShapeScenario.cpp
|
Scenarios/DepthStencilReadbackAttachmentShapeScenario.cpp
|
||||||
Scenarios/ClipDistanceScenario.cpp
|
Scenarios/ClipDistanceScenario.cpp
|
||||||
|
Scenarios/ViewportArrayScenario.cpp
|
||||||
Scenarios/SsboArrayLengthScenario.cpp
|
Scenarios/SsboArrayLengthScenario.cpp
|
||||||
Scenarios/DoublePrecisionScenario.cpp
|
Scenarios/DoublePrecisionScenario.cpp
|
||||||
Scenarios/UniformInitializerScenario.cpp
|
Scenarios/UniformInitializerScenario.cpp
|
||||||
|
|||||||
@@ -0,0 +1,524 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ViewportArrayScenario.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
//
|
||||||
|
// Scenario - gl_ViewportIndex ACTUALLY ROUTES, AND THE PER-INDEX STATE IT SELECTS IS REAL.
|
||||||
|
//
|
||||||
|
// The state half of ARB_viewport_array is asserted in MG_Test/State/RenderStateTest.cpp, which
|
||||||
|
// is a pure set/get exercise and would pass just as green against a backend that stores all 16
|
||||||
|
// rectangles and rasterizes only the first. This file is the other half: every case here routes
|
||||||
|
// primitives to a viewport OTHER than 0 and then looks at where the pixels landed.
|
||||||
|
//
|
||||||
|
// Three claims, one per case:
|
||||||
|
// 1. gl_ViewportIndex selects the viewport RECTANGLE - a 4x4 grid of 32x32 viewports, one
|
||||||
|
// geometry-shader invocation per cell, and every cell must hold its own index.
|
||||||
|
// 2. gl_ViewportIndex selects the DEPTH RANGE - 16 one-pixel-wide viewports whose ranges are
|
||||||
|
// (i/16, 1 - i/16), a quad at each end of clip space, and gl_FragCoord.z read back.
|
||||||
|
// This is the claim that fails loudest against a single-viewport backend, because the
|
||||||
|
// geometry is still in the right place while every depth comes back as viewport 0's.
|
||||||
|
// 3. The per-index SCISSOR TEST ENABLE is honoured. Vulkan has no per-viewport scissor-test
|
||||||
|
// toggle, so a disabled index has to be given the whole framebuffer as its rectangle; the
|
||||||
|
// case draws the same primitive into the same index twice, once with the test off and once
|
||||||
|
// with it on, and requires the two results to differ in the documented direction.
|
||||||
|
//
|
||||||
|
// Case 1 runs a second time against the DEFAULT framebuffer. MobileGL Y-flips (and pre-transform
|
||||||
|
// rotates) the default framebuffer's rectangles and does not touch an FBO's, so a port that
|
||||||
|
// applies the flip to viewport 0 and forgets the other fifteen renders a correct-looking FBO and
|
||||||
|
// an upside-down window - the classic multi-viewport bug, and invisible to every FBO-only case.
|
||||||
|
//
|
||||||
|
// HONEST LIMIT OF THIS FILE. DirectGLES SKIPS every case: GLES has one viewport, one scissor
|
||||||
|
// rectangle and no gl_ViewportIndex, so routing to index > 0 is an emulation feature that has
|
||||||
|
// not been built (the Espryt half of KHR-GL43.viewport_array's rendering group is deliberately
|
||||||
|
// still red). The skip is explicit rather than silent so a future emulation lands here as a
|
||||||
|
// failing test and not as a test that was quietly never running. DirectVulkan additionally
|
||||||
|
// skips when the device lacks the multiViewport feature - Vulkan then forbids a pipeline from
|
||||||
|
// declaring more than one viewport at all, which is a device limit and not a MobileGL bug;
|
||||||
|
// lavapipe (every CI lane) and both Mali/Adreno devices support it, so the cases do run where
|
||||||
|
// it matters.
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "../Harness/HeadlessGL.h"
|
||||||
|
#include "../Harness/ScenarioFixture.h"
|
||||||
|
|
||||||
|
#ifdef GLAPI
|
||||||
|
#undef GLAPI
|
||||||
|
#endif
|
||||||
|
#define GL_GLEXT_PROTOTYPES
|
||||||
|
#include <GL/gl.h>
|
||||||
|
#include <GL/glcorearb.h>
|
||||||
|
#undef GL_GLEXT_PROTOTYPES
|
||||||
|
|
||||||
|
namespace MGITest {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr int kViewportCount = 16;
|
||||||
|
constexpr int kGridSide = 4; // 4x4 grid of viewports
|
||||||
|
constexpr int kCellSize = 32; // ... each 32x32
|
||||||
|
constexpr int kSurfaceSide = kGridSide * kCellSize;
|
||||||
|
constexpr GLint kUnwritten = -1;
|
||||||
|
|
||||||
|
// A geometry shader is the only stage GL 4.1 lets write gl_ViewportIndex, and
|
||||||
|
// `invocations` runs it once per viewport off a single input point - the same shape
|
||||||
|
// KHR-GL43.viewport_array.draw_to_single_layer_with_multiple_viewports uses.
|
||||||
|
const char* const kVertexSource = R"(#version 410 core
|
||||||
|
void main() { gl_Position = vec4(0.0, 0.0, 0.0, 1.0); }
|
||||||
|
)";
|
||||||
|
|
||||||
|
const char* const kGridGeometrySource = R"(#version 410 core
|
||||||
|
layout(points, invocations = 16) in;
|
||||||
|
layout(triangle_strip, max_vertices = 4) out;
|
||||||
|
flat out int gsIndex;
|
||||||
|
void main() {
|
||||||
|
gsIndex = gl_InvocationID;
|
||||||
|
gl_ViewportIndex = gl_InvocationID;
|
||||||
|
gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); EmitVertex();
|
||||||
|
gl_Position = vec4( 1.0, -1.0, 0.0, 1.0); EmitVertex();
|
||||||
|
gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); EmitVertex();
|
||||||
|
gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); EmitVertex();
|
||||||
|
EndPrimitive();
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
// One invocation, viewport chosen by a uniform: lets a case draw the SAME primitive into
|
||||||
|
// the SAME index twice under two different scissor-enable states.
|
||||||
|
const char* const kSingleGeometrySource = R"(#version 410 core
|
||||||
|
layout(points, invocations = 1) in;
|
||||||
|
layout(triangle_strip, max_vertices = 4) out;
|
||||||
|
uniform int uViewport;
|
||||||
|
flat out int gsIndex;
|
||||||
|
void main() {
|
||||||
|
gsIndex = uViewport;
|
||||||
|
gl_ViewportIndex = uViewport;
|
||||||
|
gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); EmitVertex();
|
||||||
|
gl_Position = vec4( 1.0, -1.0, 0.0, 1.0); EmitVertex();
|
||||||
|
gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); EmitVertex();
|
||||||
|
gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); EmitVertex();
|
||||||
|
EndPrimitive();
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
const char* const kIntFragmentSource = R"(#version 410 core
|
||||||
|
flat in int gsIndex;
|
||||||
|
layout(location = 0) out int fragColor;
|
||||||
|
void main() { fragColor = gsIndex; }
|
||||||
|
)";
|
||||||
|
|
||||||
|
// Two quads, one at each end of clip space, so the fragment stage can report the depth
|
||||||
|
// the viewport's range mapped them to. gl_FragCoord.z IS the post-range window depth, so
|
||||||
|
// it reads back the per-viewport minDepth/maxDepth directly.
|
||||||
|
const char* const kDepthGeometrySource = R"(#version 410 core
|
||||||
|
layout(points, invocations = 16) in;
|
||||||
|
layout(triangle_strip, max_vertices = 8) out;
|
||||||
|
void main() {
|
||||||
|
gl_ViewportIndex = gl_InvocationID;
|
||||||
|
gl_Position = vec4(-1.0, -1.0, -1.0, 1.0); EmitVertex();
|
||||||
|
gl_Position = vec4( 1.0, -1.0, -1.0, 1.0); EmitVertex();
|
||||||
|
gl_Position = vec4(-1.0, 0.0, -1.0, 1.0); EmitVertex();
|
||||||
|
gl_Position = vec4( 1.0, 0.0, -1.0, 1.0); EmitVertex();
|
||||||
|
EndPrimitive();
|
||||||
|
gl_Position = vec4(-1.0, 0.0, 1.0, 1.0); EmitVertex();
|
||||||
|
gl_Position = vec4( 1.0, 0.0, 1.0, 1.0); EmitVertex();
|
||||||
|
gl_Position = vec4(-1.0, 1.0, 1.0, 1.0); EmitVertex();
|
||||||
|
gl_Position = vec4( 1.0, 1.0, 1.0, 1.0); EmitVertex();
|
||||||
|
EndPrimitive();
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
const char* const kDepthFragmentSource = R"(#version 410 core
|
||||||
|
layout(location = 0) out float fragColor;
|
||||||
|
void main() { fragColor = gl_FragCoord.z; }
|
||||||
|
)";
|
||||||
|
|
||||||
|
class ViewportArrayScenario : public ScenarioTest {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
ScenarioTest::SetUp();
|
||||||
|
if (!Ready()) return;
|
||||||
|
|
||||||
|
if (Gl().BackendName() == "DirectGLES") {
|
||||||
|
GTEST_SKIP() << "gl_ViewportIndex routing is not emulated on DirectGLES: GLES has one viewport "
|
||||||
|
"and one scissor rectangle, so every index rasterizes as index 0. The indexed "
|
||||||
|
"STATE is still asserted (MG_Test RenderStateTest); this is the deferred "
|
||||||
|
"rendering half of KHR-GL43.viewport_array.";
|
||||||
|
}
|
||||||
|
|
||||||
|
GLint maxViewports = 0;
|
||||||
|
glGetIntegerv(GL_MAX_VIEWPORTS, &maxViewports);
|
||||||
|
ASSERT_GE(maxViewports, kViewportCount) << "GL 4.3 core requires GL_MAX_VIEWPORTS >= 16";
|
||||||
|
|
||||||
|
m_program = BuildProgram(kGridGeometrySource, kIntFragmentSource);
|
||||||
|
ASSERT_NE(m_program, 0u) << "grid program failed to build: " << m_buildLog;
|
||||||
|
glGenVertexArrays(1, &m_vao);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
ResetViewportArrayState();
|
||||||
|
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "setup left a GL error behind";
|
||||||
|
}
|
||||||
|
|
||||||
|
void TearDown() override {
|
||||||
|
if (!Ready() || IsSkipped()) return;
|
||||||
|
ResetViewportArrayState();
|
||||||
|
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||||
|
if (m_program != 0) glDeleteProgram(m_program);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
while (glGetError() != GL_NO_ERROR) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every case starts from the same slate: this fixture shares its context with every
|
||||||
|
// other scenario in the process, and a leftover per-index scissor enable is exactly
|
||||||
|
// the kind of state that would make a later case pass or fail for the wrong reason.
|
||||||
|
static void ResetViewportArrayState() {
|
||||||
|
for (int i = 0; i < kViewportCount; ++i) {
|
||||||
|
glDisablei(GL_SCISSOR_TEST, static_cast<GLuint>(i));
|
||||||
|
}
|
||||||
|
glDisable(GL_SCISSOR_TEST);
|
||||||
|
glViewport(0, 0, kSurfaceSide, kSurfaceSide);
|
||||||
|
glScissor(0, 0, kSurfaceSide, kSurfaceSide);
|
||||||
|
glDepthRange(0.0, 1.0);
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The 4x4 grid: viewport y*4+x covers the cell whose lower-left corner is
|
||||||
|
// (x*cellW, y*cellH), in GL's bottom-left-origin window coordinates. Parameterized on
|
||||||
|
// the cell size because the default framebuffer this scenario also renders into is
|
||||||
|
// deliberately non-square (HeadlessGL is 128x96, so a transposing bug cannot hide).
|
||||||
|
static void SetupGridViewports(int cellW, int cellH) {
|
||||||
|
std::vector<GLfloat> data(static_cast<size_t>(kViewportCount) * 4);
|
||||||
|
for (int y = 0; y < kGridSide; ++y) {
|
||||||
|
for (int x = 0; x < kGridSide; ++x) {
|
||||||
|
const size_t base = static_cast<size_t>(y * kGridSide + x) * 4;
|
||||||
|
data[base + 0] = static_cast<GLfloat>(x * cellW);
|
||||||
|
data[base + 1] = static_cast<GLfloat>(y * cellH);
|
||||||
|
data[base + 2] = static_cast<GLfloat>(cellW);
|
||||||
|
data[base + 3] = static_cast<GLfloat>(cellH);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
glViewportArrayv(0, kViewportCount, data.data());
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint BuildProgram(const char* geometrySource, const char* fragmentSource) {
|
||||||
|
const GLuint vs = CompileStage(GL_VERTEX_SHADER, kVertexSource);
|
||||||
|
if (vs == 0) return 0;
|
||||||
|
const GLuint gs = CompileStage(GL_GEOMETRY_SHADER, geometrySource);
|
||||||
|
if (gs == 0) {
|
||||||
|
glDeleteShader(vs);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const GLuint fs = CompileStage(GL_FRAGMENT_SHADER, fragmentSource);
|
||||||
|
if (fs == 0) {
|
||||||
|
glDeleteShader(vs);
|
||||||
|
glDeleteShader(gs);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const GLuint program = glCreateProgram();
|
||||||
|
glAttachShader(program, vs);
|
||||||
|
glAttachShader(program, gs);
|
||||||
|
glAttachShader(program, fs);
|
||||||
|
glLinkProgram(program);
|
||||||
|
GLint linked = 0;
|
||||||
|
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||||
|
glDeleteShader(vs);
|
||||||
|
glDeleteShader(gs);
|
||||||
|
glDeleteShader(fs);
|
||||||
|
if (!linked) {
|
||||||
|
GLint length = 0;
|
||||||
|
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
|
||||||
|
std::vector<char> log(static_cast<size_t>(length > 1 ? length : 1), '\0');
|
||||||
|
glGetProgramInfoLog(program, static_cast<GLsizei>(log.size()), nullptr, log.data());
|
||||||
|
m_buildLog = log.data();
|
||||||
|
glDeleteProgram(program);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint CompileStage(GLenum stage, const char* source) {
|
||||||
|
const GLuint shader = glCreateShader(stage);
|
||||||
|
glShaderSource(shader, 1, &source, nullptr);
|
||||||
|
glCompileShader(shader);
|
||||||
|
GLint compiled = 0;
|
||||||
|
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||||
|
if (compiled) return shader;
|
||||||
|
GLint length = 0;
|
||||||
|
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
|
||||||
|
std::vector<char> log(static_cast<size_t>(length > 1 ? length : 1), '\0');
|
||||||
|
glGetShaderInfoLog(shader, static_cast<GLsizei>(log.size()), nullptr, log.data());
|
||||||
|
m_buildLog = log.data();
|
||||||
|
glDeleteShader(shader);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// An R32I colour target, pre-filled with kUnwritten so "nothing was drawn here" is
|
||||||
|
// distinguishable from "index 0 was drawn here".
|
||||||
|
struct IntTarget {
|
||||||
|
GLuint fbo = 0;
|
||||||
|
GLuint texture = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The "nothing drawn here" value is UPLOADED, not cleared: the CTS fills its R32I
|
||||||
|
// targets the same way (fillTexture), and an upload cannot be confused with a clear
|
||||||
|
// that a backend defers, reorders or drops - which is exactly the ambiguity a case
|
||||||
|
// asserting "this cell must be untouched" cannot afford.
|
||||||
|
static void FillIntTarget(const IntTarget& target, int width, int height) {
|
||||||
|
const std::vector<GLint> unwritten(static_cast<size_t>(width) * height, kUnwritten);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, target.texture);
|
||||||
|
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RED_INTEGER, GL_INT, unwritten.data());
|
||||||
|
}
|
||||||
|
|
||||||
|
static IntTarget MakeIntTarget(int width, int height) {
|
||||||
|
IntTarget target;
|
||||||
|
glGenTextures(1, &target.texture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, target.texture);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_R32I, width, height, 0, GL_RED_INTEGER, GL_INT, nullptr);
|
||||||
|
glGenFramebuffers(1, &target.fbo);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, target.fbo);
|
||||||
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, target.texture, 0);
|
||||||
|
FillIntTarget(target, width, height);
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void DestroyIntTarget(IntTarget& target) {
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
if (target.fbo != 0) glDeleteFramebuffers(1, &target.fbo);
|
||||||
|
if (target.texture != 0) glDeleteTextures(1, &target.texture);
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::vector<GLint> ReadInts(int width, int height) {
|
||||||
|
std::vector<GLint> pixels(static_cast<size_t>(width) * height, 0);
|
||||||
|
glReadPixels(0, 0, width, height, GL_RED_INTEGER, GL_INT, pixels.data());
|
||||||
|
return pixels;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The centre of grid cell (x, y), in the bottom-left-origin coordinates glReadPixels
|
||||||
|
// returns. Sampling the centre rather than a corner keeps the assertion about WHICH
|
||||||
|
// viewport was selected rather than about edge rounding.
|
||||||
|
static GLint CellCentre(const std::vector<GLint>& pixels, int stride, int x, int y) {
|
||||||
|
const int px = x * kCellSize + kCellSize / 2;
|
||||||
|
const int py = y * kCellSize + kCellSize / 2;
|
||||||
|
return pixels[static_cast<size_t>(py) * stride + px];
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string m_buildLog;
|
||||||
|
GLuint m_program = 0;
|
||||||
|
GLuint m_vao = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- 1. the viewport rectangle -------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_F(ViewportArrayScenario, EachViewportIndexRasterizesIntoItsOwnRectangle) {
|
||||||
|
IntTarget target = MakeIntTarget(kSurfaceSide, kSurfaceSide);
|
||||||
|
SetupGridViewports(kCellSize, kCellSize);
|
||||||
|
glUseProgram(m_program);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
glDrawArrays(GL_POINTS, 0, 1);
|
||||||
|
ASSERT_EQ(glGetError(), GL_NO_ERROR);
|
||||||
|
|
||||||
|
const std::vector<GLint> pixels = ReadInts(kSurfaceSide, kSurfaceSide);
|
||||||
|
for (int y = 0; y < kGridSide; ++y) {
|
||||||
|
for (int x = 0; x < kGridSide; ++x) {
|
||||||
|
const GLint expected = y * kGridSide + x;
|
||||||
|
EXPECT_EQ(CellCentre(pixels, kSurfaceSide, x, y), expected)
|
||||||
|
<< "cell (" << x << ", " << y << ") should hold viewport index " << expected
|
||||||
|
<< "; a single-viewport backend paints the whole image with 15 (the last invocation)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DestroyIntTarget(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The same claim against the DEFAULT framebuffer, where MobileGL applies its Y-flip and
|
||||||
|
// pre-transform rotation. Index 0 alone getting the mapping is the classic bug.
|
||||||
|
TEST_F(ViewportArrayScenario, TheDefaultFramebufferAppliesTheSameFlipToEveryViewport) {
|
||||||
|
const int surfaceW = Gl().Width();
|
||||||
|
const int surfaceH = Gl().Height();
|
||||||
|
ASSERT_GE(surfaceW, kGridSide);
|
||||||
|
ASSERT_GE(surfaceH, kGridSide);
|
||||||
|
const int cellW = surfaceW / kGridSide;
|
||||||
|
const int cellH = surfaceH / kGridSide;
|
||||||
|
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
// Paint a value no viewport index can produce, so an unwritten cell is obvious.
|
||||||
|
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
|
||||||
|
// The default framebuffer is 8-bit RGBA, so the index travels as a colour: cell i is
|
||||||
|
// painted with red = i * 16, which is exact in 8 bits for i in [0, 16).
|
||||||
|
const char* const kColorFragmentSource = R"(#version 410 core
|
||||||
|
flat in int gsIndex;
|
||||||
|
layout(location = 0) out vec4 fragColor;
|
||||||
|
void main() { fragColor = vec4(float(gsIndex) * 16.0 / 255.0, 0.0, 0.0, 1.0); }
|
||||||
|
)";
|
||||||
|
const GLuint colorProgram = BuildProgram(kGridGeometrySource, kColorFragmentSource);
|
||||||
|
ASSERT_NE(colorProgram, 0u) << "colour program failed to build: " << m_buildLog;
|
||||||
|
|
||||||
|
SetupGridViewports(cellW, cellH);
|
||||||
|
glUseProgram(colorProgram);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
glDrawArrays(GL_POINTS, 0, 1);
|
||||||
|
ASSERT_EQ(glGetError(), GL_NO_ERROR);
|
||||||
|
|
||||||
|
std::vector<unsigned char> pixels(static_cast<size_t>(surfaceW) * surfaceH * 4, 0);
|
||||||
|
glReadPixels(0, 0, surfaceW, surfaceH, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||||
|
for (int y = 0; y < kGridSide; ++y) {
|
||||||
|
for (int x = 0; x < kGridSide; ++x) {
|
||||||
|
const int px = x * cellW + cellW / 2;
|
||||||
|
const int py = y * cellH + cellH / 2;
|
||||||
|
const int red = pixels[(static_cast<size_t>(py) * surfaceW + px) * 4];
|
||||||
|
const int expected = (y * kGridSide + x) * 16;
|
||||||
|
// One LSB of slack for an 8-bit round trip; the values are 16 apart, so this
|
||||||
|
// cannot confuse two neighbouring indices.
|
||||||
|
EXPECT_LE(std::abs(red - expected), 1)
|
||||||
|
<< "default-framebuffer cell (" << x << ", " << y << ") holds red=" << red << ", expected "
|
||||||
|
<< expected << ". A vertically mirrored grid means the Y-flip was applied to viewport 0 "
|
||||||
|
<< "only";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
glDeleteProgram(colorProgram);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 2. the depth range --------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_F(ViewportArrayScenario, EachViewportIndexUsesItsOwnDepthRange) {
|
||||||
|
// 16 columns one pixel wide and two rows tall: row 0 gets the near-plane quad, row 1
|
||||||
|
// the far-plane one, so both ends of viewport i's range land in the same column.
|
||||||
|
constexpr int kWidth = kViewportCount;
|
||||||
|
constexpr int kHeight = 2;
|
||||||
|
|
||||||
|
GLuint texture = 0;
|
||||||
|
GLuint fbo = 0;
|
||||||
|
glGenTextures(1, &texture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, texture);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_R32F, kWidth, kHeight, 0, GL_RED, GL_FLOAT, nullptr);
|
||||||
|
glGenFramebuffers(1, &fbo);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||||
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
|
||||||
|
const GLfloat clearValue[4] = {-1.0f, 0.0f, 0.0f, 0.0f};
|
||||||
|
glClearBufferfv(GL_COLOR, 0, clearValue);
|
||||||
|
|
||||||
|
std::vector<GLfloat> viewports(static_cast<size_t>(kViewportCount) * 4);
|
||||||
|
std::vector<GLdouble> ranges(static_cast<size_t>(kViewportCount) * 2);
|
||||||
|
for (int i = 0; i < kViewportCount; ++i) {
|
||||||
|
viewports[static_cast<size_t>(i) * 4 + 0] = static_cast<GLfloat>(i);
|
||||||
|
viewports[static_cast<size_t>(i) * 4 + 1] = 0.0f;
|
||||||
|
viewports[static_cast<size_t>(i) * 4 + 2] = 1.0f;
|
||||||
|
viewports[static_cast<size_t>(i) * 4 + 3] = 2.0f;
|
||||||
|
ranges[static_cast<size_t>(i) * 2 + 0] = static_cast<GLdouble>(i) / 16.0;
|
||||||
|
ranges[static_cast<size_t>(i) * 2 + 1] = 1.0 - static_cast<GLdouble>(i) / 16.0;
|
||||||
|
}
|
||||||
|
glViewportArrayv(0, kViewportCount, viewports.data());
|
||||||
|
glDepthRangeArrayv(0, kViewportCount, ranges.data());
|
||||||
|
|
||||||
|
const GLuint depthProgram = BuildProgram(kDepthGeometrySource, kDepthFragmentSource);
|
||||||
|
ASSERT_NE(depthProgram, 0u) << "depth program failed to build: " << m_buildLog;
|
||||||
|
glUseProgram(depthProgram);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
glDrawArrays(GL_POINTS, 0, 1);
|
||||||
|
ASSERT_EQ(glGetError(), GL_NO_ERROR);
|
||||||
|
|
||||||
|
std::vector<GLfloat> pixels(static_cast<size_t>(kWidth) * kHeight, 0.0f);
|
||||||
|
glReadPixels(0, 0, kWidth, kHeight, GL_RED, GL_FLOAT, pixels.data());
|
||||||
|
for (int i = 0; i < kViewportCount; ++i) {
|
||||||
|
const float near = static_cast<float>(i) / 16.0f;
|
||||||
|
const float far = 1.0f - static_cast<float>(i) / 16.0f;
|
||||||
|
// The tolerance covers depth-buffer-free rasterization of gl_FragCoord.z on a
|
||||||
|
// software rasterizer; the per-index values are 1/16 apart, so it cannot let a
|
||||||
|
// neighbouring viewport's range through, and viewport 0's range (0, 1) differs
|
||||||
|
// from every other index by at least 1/16.
|
||||||
|
EXPECT_NEAR(pixels[i], near, 1.0e-3f)
|
||||||
|
<< "viewport " << i << " near-plane depth; got viewport 0's range if this is 0";
|
||||||
|
EXPECT_NEAR(pixels[static_cast<size_t>(kWidth) + i], far, 1.0e-3f)
|
||||||
|
<< "viewport " << i << " far-plane depth; got viewport 0's range if this is 1";
|
||||||
|
}
|
||||||
|
|
||||||
|
glDeleteProgram(depthProgram);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
glDeleteFramebuffers(1, &fbo);
|
||||||
|
glDeleteTextures(1, &texture);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 3. the per-index scissor-test enable --------------------------------------------
|
||||||
|
|
||||||
|
TEST_F(ViewportArrayScenario, AnIndexedScissorEnableClipsOnlyThatIndex) {
|
||||||
|
IntTarget target = MakeIntTarget(kSurfaceSide, kSurfaceSide);
|
||||||
|
|
||||||
|
// One full-size viewport per index so the scissor rectangle is the ONLY thing that
|
||||||
|
// can shrink the quad - the same separation KHR-GL43.viewport_array.scissor uses.
|
||||||
|
glViewport(0, 0, kSurfaceSide, kSurfaceSide);
|
||||||
|
std::vector<GLint> boxes(static_cast<size_t>(kViewportCount) * 4);
|
||||||
|
for (int y = 0; y < kGridSide; ++y) {
|
||||||
|
for (int x = 0; x < kGridSide; ++x) {
|
||||||
|
const size_t base = static_cast<size_t>(y * kGridSide + x) * 4;
|
||||||
|
boxes[base + 0] = x * kCellSize;
|
||||||
|
boxes[base + 1] = y * kCellSize;
|
||||||
|
boxes[base + 2] = kCellSize;
|
||||||
|
boxes[base + 3] = kCellSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
glScissorArrayv(0, kViewportCount, boxes.data());
|
||||||
|
|
||||||
|
const GLuint singleProgram = BuildProgram(kSingleGeometrySource, kIntFragmentSource);
|
||||||
|
ASSERT_NE(singleProgram, 0u) << "single-viewport program failed to build: " << m_buildLog;
|
||||||
|
glUseProgram(singleProgram);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
const GLint uViewport = glGetUniformLocation(singleProgram, "uViewport");
|
||||||
|
ASSERT_NE(uViewport, -1);
|
||||||
|
|
||||||
|
constexpr GLint kProbeIndex = 6; // grid cell (2, 1)
|
||||||
|
constexpr int kProbeX = kProbeIndex % kGridSide;
|
||||||
|
constexpr int kProbeY = kProbeIndex / kGridSide;
|
||||||
|
|
||||||
|
// (a) scissor test ENABLED for this index: the quad is clipped to its 32x32 box.
|
||||||
|
glUniform1i(uViewport, kProbeIndex);
|
||||||
|
glEnablei(GL_SCISSOR_TEST, kProbeIndex);
|
||||||
|
glDrawArrays(GL_POINTS, 0, 1);
|
||||||
|
ASSERT_EQ(glGetError(), GL_NO_ERROR);
|
||||||
|
{
|
||||||
|
const std::vector<GLint> pixels = ReadInts(kSurfaceSide, kSurfaceSide);
|
||||||
|
EXPECT_EQ(CellCentre(pixels, kSurfaceSide, kProbeX, kProbeY), kProbeIndex)
|
||||||
|
<< "the scissored index must still paint inside its own box";
|
||||||
|
for (int y = 0; y < kGridSide; ++y) {
|
||||||
|
for (int x = 0; x < kGridSide; ++x) {
|
||||||
|
if (x == kProbeX && y == kProbeY) continue;
|
||||||
|
EXPECT_EQ(CellCentre(pixels, kSurfaceSide, x, y), kUnwritten)
|
||||||
|
<< "cell (" << x << ", " << y << ") is outside scissor rectangle " << kProbeIndex
|
||||||
|
<< " and must be untouched";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// (b) scissor test DISABLED for the same index, everything else identical: with no
|
||||||
|
// per-viewport toggle in Vulkan this is the case that needs the disabled index to be
|
||||||
|
// given the full framebuffer rectangle, and it is exactly where "leave the last
|
||||||
|
// rectangle bound" would show up as a still-clipped quad.
|
||||||
|
FillIntTarget(target, kSurfaceSide, kSurfaceSide);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, target.fbo);
|
||||||
|
glDisablei(GL_SCISSOR_TEST, kProbeIndex);
|
||||||
|
glDrawArrays(GL_POINTS, 0, 1);
|
||||||
|
ASSERT_EQ(glGetError(), GL_NO_ERROR);
|
||||||
|
{
|
||||||
|
const std::vector<GLint> pixels = ReadInts(kSurfaceSide, kSurfaceSide);
|
||||||
|
for (int y = 0; y < kGridSide; ++y) {
|
||||||
|
for (int x = 0; x < kGridSide; ++x) {
|
||||||
|
EXPECT_EQ(CellCentre(pixels, kSurfaceSide, x, y), kProbeIndex)
|
||||||
|
<< "with the scissor test off for index " << kProbeIndex
|
||||||
|
<< ", its full-viewport quad must cover cell (" << x << ", " << y << ")";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
glDeleteProgram(singleProgram);
|
||||||
|
DestroyIntTarget(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace MGITest
|
||||||
@@ -4,6 +4,7 @@ add_executable(
|
|||||||
PipelineQuirkTest
|
PipelineQuirkTest
|
||||||
PipelineQuirkTest.cpp
|
PipelineQuirkTest.cpp
|
||||||
PassthroughTessControlTest.cpp
|
PassthroughTessControlTest.cpp
|
||||||
|
ViewportIndexReflectionTest.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(PipelineQuirkTest PRIVATE
|
target_include_directories(PipelineQuirkTest PRIVATE
|
||||||
|
|||||||
@@ -0,0 +1,183 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Test/Pipeline/ViewportIndexReflectionTest.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
//
|
||||||
|
// ProgramFactory::ReflectedWritesViewportIndexBuiltin is the switch that decides whether a
|
||||||
|
// DirectVulkan pipeline declares one viewport or all sixteen. Getting it wrong is silent in both
|
||||||
|
// directions and neither direction is caught by a state test:
|
||||||
|
//
|
||||||
|
// - a false NEGATIVE collapses every gl_ViewportIndex onto viewport 0, which is precisely the
|
||||||
|
// bug the multi-viewport work exists to fix and which a set/get round trip cannot see;
|
||||||
|
// - a false POSITIVE widens viewportCount for an ordinary Minecraft shader, costing a longer
|
||||||
|
// vkCmdSetViewport per state change and, on a tiler, possibly a hardware fast path.
|
||||||
|
//
|
||||||
|
// So this compiles REAL GLSL through the same glslang path the renderer uses and reflects the
|
||||||
|
// SPIR-V that comes out, rather than asserting against hand-assembled words: what has to hold is
|
||||||
|
// that the detector agrees with what glslang actually emits for a shader that writes the builtin,
|
||||||
|
// including the stage-by-stage question of WHERE it may be written (GL 4.1 allows the geometry
|
||||||
|
// stage; ARB_shader_viewport_layer_array adds vertex and tessellation evaluation).
|
||||||
|
//
|
||||||
|
// The end-to-end claim - that a detected writer really does route pixels to its own viewport -
|
||||||
|
// lives in MG_IntegrationTest/Scenarios/ViewportArrayScenario.cpp.
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "Includes.h"
|
||||||
|
#include "Init.h"
|
||||||
|
|
||||||
|
#include <MG_Backend/DirectVulkan/Renderer/ProgramFactory.h>
|
||||||
|
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||||
|
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||||
|
|
||||||
|
#include <spirv_reflect.h>
|
||||||
|
|
||||||
|
using namespace MobileGL;
|
||||||
|
using MobileGL::MG_Backend::DirectVulkan::ProgramFactory;
|
||||||
|
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
Vector<Uint32> CompileToSpirv(GLenum stage, const String& source) {
|
||||||
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
ShaderAttrib shaderAttrib{.shaderType = stage, .sourceStr = source};
|
||||||
|
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
|
||||||
|
EXPECT_TRUE(shaderResult) << (shaderResult ? String{} : shaderResult.error().log);
|
||||||
|
if (!shaderResult) return {};
|
||||||
|
|
||||||
|
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
|
||||||
|
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
|
||||||
|
EXPECT_TRUE(programResult) << (programResult ? String{} : programResult.error().log);
|
||||||
|
if (!programResult) return {};
|
||||||
|
|
||||||
|
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {stage}, .program = *programResult.value()};
|
||||||
|
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
||||||
|
EXPECT_TRUE(binaryResult) << (binaryResult ? String{} : binaryResult.error().log);
|
||||||
|
if (!binaryResult || binaryResult->empty()) return {};
|
||||||
|
return binaryResult->front();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Owns the reflection module so a failing EXPECT cannot leak it.
|
||||||
|
class ReflectModule {
|
||||||
|
public:
|
||||||
|
explicit ReflectModule(const Vector<Uint32>& spirv) {
|
||||||
|
if (spirv.empty()) return;
|
||||||
|
m_created = spvReflectCreateShaderModule(spirv.size() * sizeof(Uint32), spirv.data(), &m_module) ==
|
||||||
|
SPV_REFLECT_RESULT_SUCCESS;
|
||||||
|
}
|
||||||
|
~ReflectModule() {
|
||||||
|
if (m_created) spvReflectDestroyShaderModule(&m_module);
|
||||||
|
}
|
||||||
|
ReflectModule(const ReflectModule&) = delete;
|
||||||
|
ReflectModule& operator=(const ReflectModule&) = delete;
|
||||||
|
|
||||||
|
Bool Created() const { return m_created; }
|
||||||
|
const SpvReflectShaderModule& Get() const { return m_module; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
SpvReflectShaderModule m_module{};
|
||||||
|
Bool m_created = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ViewportIndexReflectionTest : public ::testing::Test {
|
||||||
|
protected:
|
||||||
|
void SetUp() override { MobileGL::Initialize(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
const char* const kGeometryWritesViewportIndex = R"(#version 410 core
|
||||||
|
layout(points, invocations = 16) in;
|
||||||
|
layout(triangle_strip, max_vertices = 4) out;
|
||||||
|
void main() {
|
||||||
|
gl_ViewportIndex = gl_InvocationID;
|
||||||
|
gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); EmitVertex();
|
||||||
|
gl_Position = vec4( 1.0, -1.0, 0.0, 1.0); EmitVertex();
|
||||||
|
gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); EmitVertex();
|
||||||
|
gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); EmitVertex();
|
||||||
|
EndPrimitive();
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
// Same stage, same shape, writing gl_Layer INSTEAD. Layered rendering and viewport routing
|
||||||
|
// are different features and the detector must not confuse them: a Minecraft-style cubemap
|
||||||
|
// pass writes gl_Layer and must keep the one-viewport pipeline.
|
||||||
|
const char* const kGeometryWritesLayerOnly = R"(#version 410 core
|
||||||
|
layout(points, invocations = 6) in;
|
||||||
|
layout(triangle_strip, max_vertices = 4) out;
|
||||||
|
void main() {
|
||||||
|
gl_Layer = gl_InvocationID;
|
||||||
|
gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); EmitVertex();
|
||||||
|
gl_Position = vec4( 1.0, -1.0, 0.0, 1.0); EmitVertex();
|
||||||
|
gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); EmitVertex();
|
||||||
|
gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); EmitVertex();
|
||||||
|
EndPrimitive();
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
const char* const kPlainGeometry = R"(#version 410 core
|
||||||
|
layout(points, invocations = 1) in;
|
||||||
|
layout(triangle_strip, max_vertices = 4) out;
|
||||||
|
void main() {
|
||||||
|
gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); EmitVertex();
|
||||||
|
gl_Position = vec4( 1.0, -1.0, 0.0, 1.0); EmitVertex();
|
||||||
|
gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); EmitVertex();
|
||||||
|
gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); EmitVertex();
|
||||||
|
EndPrimitive();
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
const char* const kPlainVertex = R"(#version 410 core
|
||||||
|
void main() { gl_Position = vec4(0.0, 0.0, 0.0, 1.0); }
|
||||||
|
)";
|
||||||
|
|
||||||
|
const char* const kPlainFragment = R"(#version 410 core
|
||||||
|
layout(location = 0) out vec4 fragColor;
|
||||||
|
void main() { fragColor = vec4(1.0); }
|
||||||
|
)";
|
||||||
|
|
||||||
|
TEST_F(ViewportIndexReflectionTest, TrueForAGeometryShaderThatAssignsViewportIndex) {
|
||||||
|
const ReflectModule module(CompileToSpirv(GL_GEOMETRY_SHADER, kGeometryWritesViewportIndex));
|
||||||
|
ASSERT_TRUE(module.Created());
|
||||||
|
EXPECT_TRUE(ProgramFactory::ReflectedWritesViewportIndexBuiltin(module.Get()))
|
||||||
|
<< "a shader that assigns gl_ViewportIndex must get a multi-viewport pipeline; missing it is what "
|
||||||
|
"collapses every index onto viewport 0";
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ViewportIndexReflectionTest, FalseForAGeometryShaderThatOnlyAssignsLayer) {
|
||||||
|
const ReflectModule module(CompileToSpirv(GL_GEOMETRY_SHADER, kGeometryWritesLayerOnly));
|
||||||
|
ASSERT_TRUE(module.Created());
|
||||||
|
EXPECT_FALSE(ProgramFactory::ReflectedWritesViewportIndexBuiltin(module.Get()))
|
||||||
|
<< "gl_Layer is layered rendering, not viewport routing; widening viewportCount for it costs the "
|
||||||
|
"single-viewport fast path for nothing";
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ViewportIndexReflectionTest, FalseForAPlainGeometryShader) {
|
||||||
|
const ReflectModule module(CompileToSpirv(GL_GEOMETRY_SHADER, kPlainGeometry));
|
||||||
|
ASSERT_TRUE(module.Created());
|
||||||
|
EXPECT_FALSE(ProgramFactory::ReflectedWritesViewportIndexBuiltin(module.Get()));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ViewportIndexReflectionTest, FalseForTheOrdinaryVertexAndFragmentStages) {
|
||||||
|
// The shape every real application ships: neither stage may widen the pipeline.
|
||||||
|
const ReflectModule vertexModule(CompileToSpirv(GL_VERTEX_SHADER, kPlainVertex));
|
||||||
|
ASSERT_TRUE(vertexModule.Created());
|
||||||
|
EXPECT_FALSE(ProgramFactory::ReflectedWritesViewportIndexBuiltin(vertexModule.Get()));
|
||||||
|
|
||||||
|
const ReflectModule fragmentModule(CompileToSpirv(GL_FRAGMENT_SHADER, kPlainFragment));
|
||||||
|
ASSERT_TRUE(fragmentModule.Created());
|
||||||
|
EXPECT_FALSE(ProgramFactory::ReflectedWritesViewportIndexBuiltin(fragmentModule.Get()));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ViewportIndexReflectionTest, FalseForAnEmptyModuleWithoutDereferencing) {
|
||||||
|
// A default-constructed module has no entry points. The scan runs on every link, so it
|
||||||
|
// must survive a reflection that never got built rather than walk a null array.
|
||||||
|
SpvReflectShaderModule emptyModule{};
|
||||||
|
EXPECT_FALSE(ProgramFactory::ReflectedWritesViewportIndexBuiltin(emptyModule));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
Reference in New Issue
Block a user