From 5fbb17f6b924bae145add2dfcddfdfa788abfde9 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 13 Aug 2026 03:54:20 -0400 Subject: [PATCH] [Feat, Fix, Test] (MG_State, MG_Impl, MG_Backend, MG_Test): give ARB_viewport_array real 16-element indexed state instead of eight stubs and a viewport-0 echo --- MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 28 +- .../DirectVulkan/Renderer/VulkanRenderer.cpp | 30 +- .../MG_Impl/GLImpl/Exporting/Definitions.cpp | 16 +- MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp | 131 ++++- .../GLImpl/RenderState/GL_RenderState.cpp | 232 ++++++++- .../GLImpl/RenderState/GL_RenderState.h | 10 + MobileGL/MG_State/GLState/Core.cpp | 26 +- MobileGL/MG_State/GLState/Core.h | 16 +- .../GLState/RenderState/RenderState.cpp | 180 ++++++- .../GLState/RenderState/RenderState.h | 49 +- MobileGL/MG_Test/State/RenderStateTest.cpp | 484 +++++++++++++++++- 11 files changed, 1091 insertions(+), 111 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index a489a7de..51686a12 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -1600,7 +1600,7 @@ namespace MobileGL::MG_Backend::DirectGLES { !g_hasSyncedRenderState || std::memcmp(currentBytes + kBlendSpanEnd, syncedBytes + kBlendSpanEnd, sizeof(RenderStateParameters) - kBlendSpanEnd) != 0; - IntVec4 backendViewport = parameters.Viewport; + IntVec4 backendViewport = MG_State::pGLContext->GetViewport(); if (backendViewport.z() <= 0 || backendViewport.w() <= 0) { Int surfaceWidth = 0; Int surfaceHeight = 0; @@ -1633,11 +1633,26 @@ namespace MobileGL::MG_Backend::DirectGLES { SYNC_CAPABILITY(SampleMask, GL_SAMPLE_MASK); SYNC_CAPABILITY(PolygonOffsetFill, GL_POLYGON_OFFSET_FILL); SYNC_CAPABILITY(RasterizerDiscard, GL_RASTERIZER_DISCARD); - SYNC_CAPABILITY(ScissorTest, GL_SCISSOR_TEST); SYNC_CAPABILITY(StencilTest, GL_STENCIL_TEST); SYNC_CAPABILITY(CullFace, GL_CULL_FACE); #undef SYNC_CAPABILITY + + // GL_SCISSOR_TEST is per-viewport enable state (ARB_viewport_array), so it is a + // 16-bit mask and not a "Enabled" bool the macro above could key off. ES + // has exactly one scissor rectangle and one scissor enable, so only bit 0 - the + // index every ES draw rasterizes against - can be forwarded; a program that + // enables the test for viewport 3 alone gets viewport 0's answer here. That is + // the same limitation as the unemulated gl_ViewportIndex on this backend and is + // why the multi-viewport half of KHR-GL43.viewport_array stays red on Espryt. + { + const Bool scissorTest = (parameters.ScissorTestEnabledMask & 1u) != 0; + const Bool syncedScissorTest = + (g_syncedRenderStateParameters.ScissorTestEnabledMask & 1u) != 0; + if (forceFullPush || scissorTest != syncedScissorTest) { + scissorTest ? g_GLESFuncs.glEnable(GL_SCISSOR_TEST) : g_GLESFuncs.glDisable(GL_SCISSOR_TEST); + } + } } if (tailSpanDirty && g_GLESCapabilities.SupportsClipDistance) { @@ -1864,8 +1879,8 @@ namespace MobileGL::MG_Backend::DirectGLES { if (forceFullPush || parameters.DepthMask != g_syncedRenderStateParameters.DepthMask) { g_GLESFuncs.glDepthMask(parameters.DepthMask ? GL_TRUE : GL_FALSE); } - if (forceFullPush || parameters.DepthRange != g_syncedRenderStateParameters.DepthRange) { - g_GLESFuncs.glDepthRangef(parameters.DepthRange.x(), parameters.DepthRange.y()); + if (forceFullPush || parameters.DepthRanges[0] != g_syncedRenderStateParameters.DepthRanges[0]) { + g_GLESFuncs.glDepthRangef(parameters.DepthRanges[0].x(), parameters.DepthRanges[0].y()); } } @@ -2003,7 +2018,7 @@ namespace MobileGL::MG_Backend::DirectGLES { // everything drawn with GL_SCISSOR_TEST enabled before the app's first glScissor // is clipped away - Minecraft 26.2 keeps only its unscissored sky and hand and // loses the terrain and the whole GUI. - IntVec4 backendScissorBox = parameters.ScissorBox; + IntVec4 backendScissorBox = parameters.ScissorBoxes[0]; if (backendScissorBox.z() <= 0 || backendScissorBox.w() <= 0) { Int surfaceWidth = 0; Int surfaceHeight = 0; @@ -4762,7 +4777,8 @@ namespace MobileGL::MG_Backend::DirectGLES { // restores the app state on exit, tracked via the render-state shadow. class ScopedScissorDisable { public: - ScopedScissorDisable() : m_wasEnabled(RenderStateImpl::g_syncedRenderStateParameters.ScissorTestEnabled) { + ScopedScissorDisable() + : m_wasEnabled((RenderStateImpl::g_syncedRenderStateParameters.ScissorTestEnabledMask & 1u) != 0) { if (m_wasEnabled) g_GLESFuncs.glDisable(GL_SCISSOR_TEST); } ~ScopedScissorDisable() { diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index c6f8caf3..2400a392 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -335,19 +335,19 @@ namespace MobileGL::MG_Backend::DirectVulkan { // Complete input inventory of ApplyDynamicDrawStateTail, one line per reader // (each accessor it replaces is a verified plain field read of the same // RenderStateParameters field - RenderState.cpp): - // ApplyGLViewportState : Viewport, DepthRange, + extent/isDefaultFbo/preTransform + // ApplyGLViewportState : Viewports[0], DepthRanges[0], + extent/isDefaultFbo/preTransform // ApplyBlendConstants : BlendColor // ApplyPolygonOffsetState : PolygonOffsetUnits, PolygonOffsetFactor // ApplyLineWidthState : LineWidth (see the caveat below) // ApplyStencilState : StencilStates[0..1].{ValueMask, WriteMask, Ref} - // scissor rect : ScissorTestEnabled, ScissorBox, + // scissor rect : ScissorTestEnabledMask bit 0, ScissorBoxes[0], // + extent/isDefaultFbo/preTransform // Caveat, unchanged from the version-only gate: ApplyLineWidthState also clamps // to the ACTIVE BACKEND OBJECT's aliased line-width range. Those are device // limits queried once at backend init and constant for the renderer's lifetime, // so they are not part of the key (the version gate never covered them either). struct DynamicTailKey { - Int viewport[4] = {0, 0, 0, 0}; + Float viewport[4] = {0.0f, 0.0f, 0.0f, 0.0f}; Float depthRange[2] = {0.0f, 0.0f}; Float blendColor[4] = {0.0f, 0.0f, 0.0f, 0.0f}; Float polygonOffsetFactor = 0.0f; @@ -5342,12 +5342,14 @@ void main() { DynamicStateShadow::DynamicTailKey key; { const RenderStateParameters& p = MG_State::pGLContext->GetRenderStateParameters(); - key.viewport[0] = p.Viewport.x(); - key.viewport[1] = p.Viewport.y(); - key.viewport[2] = p.Viewport.z(); - key.viewport[3] = p.Viewport.w(); - key.depthRange[0] = p.DepthRange.x(); - key.depthRange[1] = p.DepthRange.y(); + // Viewport 0 and its depth range: ApplyGLViewportState reads exactly those two + // (per-index state for indices > 0 is keyed separately, see multiViewportKey below). + key.viewport[0] = p.Viewports[0].x(); + key.viewport[1] = p.Viewports[0].y(); + key.viewport[2] = p.Viewports[0].z(); + key.viewport[3] = p.Viewports[0].w(); + key.depthRange[0] = p.DepthRanges[0].x(); + key.depthRange[1] = p.DepthRanges[0].y(); key.blendColor[0] = p.BlendColor.x(); key.blendColor[1] = p.BlendColor.y(); key.blendColor[2] = p.BlendColor.z(); @@ -5362,11 +5364,11 @@ void main() { key.stencilWriteMask[face] = p.StencilStates[face].WriteMask; key.stencilRef[face] = p.StencilStates[face].Ref; } - key.scissorEnabled = p.ScissorTestEnabled; - key.scissorBox[0] = p.ScissorBox.x(); - key.scissorBox[1] = p.ScissorBox.y(); - key.scissorBox[2] = p.ScissorBox.z(); - key.scissorBox[3] = p.ScissorBox.w(); + key.scissorEnabled = (p.ScissorTestEnabledMask & 1u) != 0; + key.scissorBox[0] = p.ScissorBoxes[0].x(); + key.scissorBox[1] = p.ScissorBoxes[0].y(); + key.scissorBox[2] = p.ScissorBoxes[0].z(); + key.scissorBox[3] = p.ScissorBoxes[0].w(); key.extentX = extent.x(); key.extentY = extent.y(); key.preTransform = static_cast(preTransform); diff --git a/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp b/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp index 06434bde..7a15d937 100644 --- a/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp +++ b/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp @@ -969,14 +969,14 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribL3dv, GLuint index, const GLdoub DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribL4dv, GLuint index, const GLdouble* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribL4dv, index, v) DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribLPointer, GLuint index, GLint size, GLenum type, GLsizei stride, const void* pointer) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribLPointer, index, size, type, stride, pointer) DECLARE_GL_FUNCTION_STUB_HEAD(void, GetVertexAttribLdv, GLuint index, GLenum pname, GLdouble* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetVertexAttribLdv, index, pname, params) -DECLARE_GL_FUNCTION_STUB_HEAD(void, ViewportArrayv, GLuint first, GLsizei count, const GLfloat* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ViewportArrayv, first, count, v) -DECLARE_GL_FUNCTION_STUB_HEAD(void, ViewportIndexedf, GLuint index, GLfloat x, GLfloat y, GLfloat w, GLfloat h) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ViewportIndexedf, index, x, y, w, h) -DECLARE_GL_FUNCTION_STUB_HEAD(void, ViewportIndexedfv, GLuint index, const GLfloat* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ViewportIndexedfv, index, v) -DECLARE_GL_FUNCTION_STUB_HEAD(void, ScissorArrayv, GLuint first, GLsizei count, const GLint* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ScissorArrayv, first, count, v) -DECLARE_GL_FUNCTION_STUB_HEAD(void, ScissorIndexed, GLuint index, GLint left, GLint bottom, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ScissorIndexed, index, left, bottom, width, height) -DECLARE_GL_FUNCTION_STUB_HEAD(void, ScissorIndexedv, GLuint index, const GLint* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ScissorIndexedv, index, v) -DECLARE_GL_FUNCTION_STUB_HEAD(void, DepthRangeArrayv, GLuint first, GLsizei count, const GLdouble* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DepthRangeArrayv, first, count, v) -DECLARE_GL_FUNCTION_STUB_HEAD(void, DepthRangeIndexed, GLuint index, GLdouble n, GLdouble f) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DepthRangeIndexed, index, n, f) +DECLARE_GL_FUNCTION_HEAD(void, ViewportArrayv, GLuint first, GLsizei count, const GLfloat* v) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ViewportArrayv, first, count, v) +DECLARE_GL_FUNCTION_HEAD(void, ViewportIndexedf, GLuint index, GLfloat x, GLfloat y, GLfloat w, GLfloat h) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ViewportIndexedf, index, x, y, w, h) +DECLARE_GL_FUNCTION_HEAD(void, ViewportIndexedfv, GLuint index, const GLfloat* v) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ViewportIndexedfv, index, v) +DECLARE_GL_FUNCTION_HEAD(void, ScissorArrayv, GLuint first, GLsizei count, const GLint* v) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ScissorArrayv, first, count, v) +DECLARE_GL_FUNCTION_HEAD(void, ScissorIndexed, GLuint index, GLint left, GLint bottom, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ScissorIndexed, index, left, bottom, width, height) +DECLARE_GL_FUNCTION_HEAD(void, ScissorIndexedv, GLuint index, const GLint* v) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ScissorIndexedv, index, v) +DECLARE_GL_FUNCTION_HEAD(void, DepthRangeArrayv, GLuint first, GLsizei count, const GLdouble* v) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DepthRangeArrayv, first, count, v) +DECLARE_GL_FUNCTION_HEAD(void, DepthRangeIndexed, GLuint index, GLdouble n, GLdouble f) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DepthRangeIndexed, index, n, f) DECLARE_GL_FUNCTION_HEAD(void, GetFloati_v, GLenum target, GLuint index, GLfloat* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetFloati_v, target, index, data) DECLARE_GL_FUNCTION_HEAD(void, GetDoublei_v, GLenum target, GLuint index, GLdouble* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetDoublei_v, target, index, data) DECLARE_GL_FUNCTION_HEAD(void, DrawArraysInstancedBaseInstance, GLenum mode, GLint first, GLsizei count, GLsizei instancecount, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawArraysInstancedBaseInstance, mode, first, count, instancecount, baseinstance) diff --git a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp index 9d6baf93..893f4909 100644 --- a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp +++ b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -27,6 +28,11 @@ #include namespace MobileGL::MG_Impl::GLImpl { + // Declared rather than #included from GL_RenderState.h on purpose: that header also declares + // a free function named BlendEquation, which would hide the ::MobileGL::BlendEquation enum + // this file's blend-state queries name unqualified. + GLboolean IsEnabledi(GLenum target, GLuint index); + namespace { enum class IndexedBufferQueryKind { Binding, @@ -339,26 +345,70 @@ namespace MobileGL::MG_Impl::GLImpl { return sampler ? static_cast(sampler->GetExternalIndex()) : 0; } - // The ARB_viewport_array indexed rectangles. MobileGL keeps exactly one viewport, one - // scissor box and one depth range, so every in-range index answers with that single - // value - but it has to come from the frontend state the non-indexed getters read. - // The generic path at the bottom of GetIntegeri_v is a raw backend passthrough that - // has no case for these, so routing them through it returned zeros. + // The ARB_viewport_array indexed rectangles. Each of these is genuinely per-viewport + // frontend state (RenderStateParameters::Viewports / ScissorBoxes / DepthRanges), so the + // indexed getters must read the indexed storage - the generic path at the bottom of + // GetIntegeri_v is a raw backend passthrough that has no case for them and returned + // zeros, and routing them to the NON-indexed getter (what this used to do) answered every + // index with viewport 0's value, which is what + // KHR-GL43.viewport_array.{viewport,scissor,depth_range}_api caught. Bool IsIndexedViewportQuery(GLenum target) { return target == GL_VIEWPORT || target == GL_SCISSOR_BOX || target == GL_DEPTH_RANGE; } - // ARB_viewport_array: `index` selects a viewport and MAX_VIEWPORTS bounds it. + // Component count of an indexed viewport-array query, so every width of getter writes the + // caller's whole buffer instead of just element 0 (GL 4.6 core 22.1). + GLsizei IndexedViewportQueryComponents(GLenum target) { + return target == GL_DEPTH_RANGE ? 2 : 4; + } + + // ARB_viewport_array: `index` selects a viewport and MAX_VIEWPORTS bounds it. The bound is + // the frontend's own state width, which is also exactly what GL_MAX_VIEWPORTS reports - + // taking it from the backend caps instead would let a device limit of 1 (a Vulkan device + // without the multiViewport feature) make index 1 illegal even though the state exists. Bool ValidateViewportQueryIndex(GLuint index, const char* caller) { - GLint maxViewports = 0; - GetIntegerv(GL_MAX_VIEWPORTS, &maxViewports); - if (index < static_cast(std::max(maxViewports, 1))) return true; + if (index < RenderStateParameters::MAX_VIEWPORTS) return true; MG_State::pGLContext->RecordError( ErrorCode::InvalidValue, MakeUnique("MG_Impl/GLImpl", caller, "Viewport index is out of range.")); return false; } + // The indexed viewport/scissor/depth-range state as floats, which is the widest lossless + // shape MobileGL stores (the viewport really is float state; the scissor box is integral + // and well inside float's exact range, and every depth range is in [0, 1]). Every indexed + // getter width funnels through this so they can never disagree with each other. + void ReadIndexedViewportStateFloat(GLenum target, GLuint index, GLfloat* out) { + switch (target) { + case GL_VIEWPORT: { + const FloatVec4& viewport = MG_State::pGLContext->GetViewportIndexed(index); + out[0] = viewport.x(); + out[1] = viewport.y(); + out[2] = viewport.z(); + out[3] = viewport.w(); + return; + } + case GL_SCISSOR_BOX: { + const IntVec4& box = MG_State::pGLContext->GetScissorBoxIndexed(index); + out[0] = static_cast(box.x()); + out[1] = static_cast(box.y()); + out[2] = static_cast(box.z()); + out[3] = static_cast(box.w()); + return; + } + case GL_DEPTH_RANGE: { + const FloatVec2& range = MG_State::pGLContext->GetDepthRangeIndexed(index); + out[0] = range.x(); + out[1] = range.y(); + return; + } + default: + MOBILEGL_ASSERT(false, "ReadIndexedViewportStateFloat: unexpected target 0x%x", + static_cast(target)); + return; + } + } + void CopyIntsToBooleans(const GLint* src, SizeT count, GLboolean* dst) { for (SizeT i = 0; i < count; ++i) { dst[i] = src[i] ? GL_TRUE : GL_FALSE; @@ -629,6 +679,17 @@ namespace MobileGL::MG_Impl::GLImpl { params[1] = dynamicParameters.ViewportBoundsRangeMax; return; } + // Viewport 0's rectangle, verbatim. Falling through to the integer width below would + // round the fractional rectangle a glViewportIndexedf(0, ...) is allowed to set, and + // glGetFloatv(GL_VIEWPORT) is a lossless query of float state. + case GL_VIEWPORT: { + const FloatVec4& viewport = MG_State::pGLContext->GetViewportIndexed(0); + params[0] = viewport.x(); + params[1] = viewport.y(); + params[2] = viewport.z(); + params[3] = viewport.w(); + return; + } case GL_MIN_FRAGMENT_INTERPOLATION_OFFSET: case GL_MAX_FRAGMENT_INTERPOLATION_OFFSET: case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS: { @@ -792,15 +853,32 @@ namespace MobileGL::MG_Impl::GLImpl { return; } + // GL 4.6 core 22.1: an indexed query answers EVERY indexed state, and GL_SCISSOR_TEST is + // indexed by viewport just like GL_BLEND is by draw buffer. Without this the integer + // width fell through to the backend passthrough and answered GL_INVALID_ENUM, which is + // the sticky error KHR-GL43.viewport_array.queries trips over at its next error check. + if (MG_Util::ConvertGLEnumToCapabilityInput(target) != CapabilityInput::Unknown) { + *data = IsEnabledi(target, index); + return; + } + switch (target) { // ARB_viewport_array queries the indexed rectangles through glGetIntegeri_v as well - // (gl4cMultiBindTests and the viewport_array group both do). The frontend keeps one - // viewport and one scissor box, so every in-range index reports that one. + // (gl4cMultiBindTests and the viewport_array group both do). case GL_VIEWPORT: case GL_SCISSOR_BOX: + case GL_DEPTH_RANGE: { if (!ValidateViewportQueryIndex(index, __func__)) return; - GetIntegerv(target, data); + GLfloat values[4] = {}; + ReadIndexedViewportStateFloat(target, index, values); + const GLsizei components = IndexedViewportQueryComponents(target); + for (GLsizei i = 0; i < components; ++i) { + // Round, not truncate: glGetIntegerv on floating-point state rounds to nearest + // (GL 4.6 core 22.2), so a 255.875-wide viewport reads back as 256 and not 255. + data[i] = static_cast(std::lround(values[i])); + } return; + } // The vertex buffer binding points of the vertex array object that is bound. Indexed by // binding point, not by attribute (GL 4.6 core 10.3.1). case GL_VERTEX_BINDING_BUFFER: @@ -927,7 +1005,10 @@ namespace MobileGL::MG_Impl::GLImpl { } if (IsIndexedViewportQuery(target)) { if (!ValidateViewportQueryIndex(index, __func__)) return; - GetFloatv(target, data); + // Verbatim, NOT via the integer width: the viewport is float state and + // KHR-GL43.viewport_array.viewport_api compares the read-back with ==, so a + // glViewportIndexedf(i, 0.125f, ...) has to come back as 0.125f exactly. + ReadIndexedViewportStateFloat(target, index, data); return; } GLint ints[4] = {}; @@ -944,7 +1025,12 @@ namespace MobileGL::MG_Impl::GLImpl { } if (IsIndexedViewportQuery(target)) { if (!ValidateViewportQueryIndex(index, __func__)) return; - GetDoublev(target, data); + GLfloat values[4] = {}; + ReadIndexedViewportStateFloat(target, index, values); + const GLsizei components = IndexedViewportQueryComponents(target); + for (GLsizei i = 0; i < components; ++i) { + data[i] = static_cast(values[i]); + } return; } GLint ints[4] = {}; @@ -1020,7 +1106,12 @@ namespace MobileGL::MG_Impl::GLImpl { // frontend-only value simply is not in the driver's table. GLint values[4] = {}; GetIntegeri_v(target, index, values); - *data = static_cast(values[0]); + // The viewport-array rectangles are the only multi-component indexed state here; every + // other pname is scalar, so widening element 0 alone would silently truncate them. + const GLsizei components = IsIndexedViewportQuery(target) ? IndexedViewportQueryComponents(target) : 1; + for (GLsizei i = 0; i < components; ++i) { + data[i] = static_cast(values[i]); + } } void GetInteger64v(GLenum pname, GLint64* params) { @@ -2192,7 +2283,15 @@ namespace MobileGL::MG_Impl::GLImpl { params[1] = dynamicParameters.MaxViewportHeight; break; case GL_MAX_VIEWPORTS: - *params = dynamicParameters.MaxViewports; + // The frontend's own state width, not the backend's device limit. GL 4.3 core + // requires MAX_VIEWPORTS >= 16 and every indexed viewport entry point validates + // against RenderStateParameters::MAX_VIEWPORTS, so reporting anything else would + // either advertise viewports the state cannot hold or reject indices it can. A + // Vulkan device without the multiViewport feature reports maxViewports == 1, which + // limits what can be RASTERIZED to more than one rectangle (see the multiViewport + // gate in VulkanRenderer), not what the GL state can hold; caps.MaxViewports keeps + // carrying that device number for exactly that decision. + *params = static_cast(RenderStateParameters::MAX_VIEWPORTS); break; case GL_MINOR_VERSION: *params = rendererInfo.RendererGLInfo.TargetGLVersion.Minor; diff --git a/MobileGL/MG_Impl/GLImpl/RenderState/GL_RenderState.cpp b/MobileGL/MG_Impl/GLImpl/RenderState/GL_RenderState.cpp index 14697130..6ccf39bf 100644 --- a/MobileGL/MG_Impl/GLImpl/RenderState/GL_RenderState.cpp +++ b/MobileGL/MG_Impl/GLImpl/RenderState/GL_RenderState.cpp @@ -20,28 +20,118 @@ namespace MobileGL::MG_Impl::GLImpl { return std::clamp(static_cast(value), 0.0f, 1.0f); } - static Bool ValidateIndexedBlendCapability(GLenum target, GLuint index, const char* functionName) { - if (target != GL_BLEND) { + // GL 4.6 core 17.3.2 and 22.1 give exactly two indexed capabilities: GL_BLEND, indexed by + // draw buffer, and GL_SCISSOR_TEST, indexed by viewport. They have DIFFERENT bounds + // (MAX_DRAW_BUFFERS vs MAX_VIEWPORTS), so the limit is picked per target rather than shared. + static Bool ValidateIndexedCapability(GLenum target, GLuint index, const char* functionName) { + GLuint limit = 0; + const char* indexName = nullptr; + switch (target) { + case GL_BLEND: + limit = MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS; + indexName = "Buffer"; + break; + case GL_SCISSOR_TEST: + limit = RenderStateParameters::MAX_VIEWPORTS; + indexName = "Viewport"; + break; + default: MG_State::pGLContext->RecordError( ErrorCode::InvalidEnum, MakeUnique("MG_Impl/GLImpl", functionName, - "Only GL_BLEND is supported for indexed capability state.")); + "Only GL_BLEND and GL_SCISSOR_TEST are supported for indexed " + "capability state.")); return false; } - if (index >= MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS) { + if (index >= limit) { MG_State::pGLContext->RecordError( ErrorCode::InvalidValue, - MakeUnique( - "MG_Impl/GLImpl", functionName, - "Buffer index " + std::to_string(index) + " is out of range. Max supported is " + - std::to_string(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS - 1) + ".")); + MakeUnique("MG_Impl/GLImpl", functionName, + String(indexName) + " index " + std::to_string(index) + + " is out of range. Max supported is " + std::to_string(limit - 1) + + ".")); return false; } return true; } + // ------------------ ARB_viewport_array parameter validation ------------------ + // All three families share the same two shapes, so they share the two checkers. GL 4.6 core + // 13.6.1/17.3.2: an out-of-range index is GL_INVALID_VALUE, and so is a negative width or + // height. `first + count == MAX_VIEWPORTS` is LEGAL - only strictly greater is an error, + // which KHR-GL43.viewport_array.api_errors checks explicitly in both directions. + static Bool ValidateViewportIndex(GLuint index, const char* functionName) { + if (index < RenderStateParameters::MAX_VIEWPORTS) return true; + + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique("MG_Impl/GLImpl", functionName, + "Viewport index " + std::to_string(index) + + " is out of range. Max supported is " + + std::to_string(RenderStateParameters::MAX_VIEWPORTS - 1) + ".")); + return false; + } + + static Bool ValidateViewportRange(GLuint first, GLsizei count, const char* functionName) { + if (count < 0) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique("MG_Impl/GLImpl", functionName, "count must not be negative.")); + return false; + } + // Widened before adding: first is a GLuint and count a GLsizei, so `first + count` in + // 32 bits can wrap past MAX_VIEWPORTS and let an out-of-range range through. + const Uint64 last = static_cast(first) + static_cast(count); + if (last > RenderStateParameters::MAX_VIEWPORTS) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique("MG_Impl/GLImpl", functionName, + "first (" + std::to_string(first) + ") + count (" + + std::to_string(count) + ") exceeds GL_MAX_VIEWPORTS (" + + std::to_string(RenderStateParameters::MAX_VIEWPORTS) + ").")); + return false; + } + return true; + } + + template + static Bool ValidateNonNegativeExtent(T width, T height, const char* functionName) { + if (width >= T(0) && height >= T(0)) return true; + + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique("MG_Impl/GLImpl", functionName, "Width and height must be non-negative.")); + return false; + } + + // The array forms are all-or-nothing: one bad element rejects the whole call with a SINGLE + // GL_INVALID_VALUE and leaves every rectangle untouched. api_errors relies on both halves - + // it passes a full 16-element array with exactly one negative extent and then asserts the + // error queue holds exactly one entry. + template + static Bool ValidateArrayExtents(GLsizei count, const T* v, const char* functionName) { + for (GLsizei i = 0; i < count; ++i) { + if (v[i * 4 + 2] >= T(0) && v[i * 4 + 3] >= T(0)) continue; + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique("MG_Impl/GLImpl", functionName, + "Width and height must be non-negative (element " + std::to_string(i) + + ").")); + return false; + } + return true; + } + + static Bool ValidateNonNullArray(const void* v, const char* functionName) { + if (v != nullptr) return true; + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique("MG_Impl/GLImpl", functionName, "value pointer cannot be null.")); + return false; + } + static Bool TryConvertBlendEquation(GLenum mode, const char* functionName, ::MobileGL::BlendEquation& outEquation) { outEquation = MG_Util::ConvertGLEnumToBlendEquation(mode); @@ -93,16 +183,70 @@ namespace MobileGL::MG_Impl::GLImpl { } void Viewport_State(GLint x, GLint y, GLsizei width, GLsizei height) { - if (width < 0 || height < 0) { - MG_State::pGLContext->RecordError(ErrorCode::InvalidValue, - MakeUnique("MG_Impl/GLImpl", "Viewport_State", - "Width abd height must be non-negative.")); - return; - } + if (!ValidateNonNegativeExtent(width, height, "Viewport_State")) return; MG_State::pGLContext->SetViewport(IntVec4(x, y, width, height)); } + // ------------------ ARB_viewport_array setters ------------------ + void ViewportArrayv_State(GLuint first, GLsizei count, const GLfloat* v) { + if (!ValidateViewportRange(first, count, "ViewportArrayv_State")) return; + if (count == 0) return; + if (!ValidateNonNullArray(v, "ViewportArrayv_State")) return; + if (!ValidateArrayExtents(count, v, "ViewportArrayv_State")) return; + + for (GLsizei i = 0; i < count; ++i) { + MG_State::pGLContext->SetViewportIndexed(first + static_cast(i), + FloatVec4(v[i * 4 + 0], v[i * 4 + 1], v[i * 4 + 2], v[i * 4 + 3])); + } + } + + void ViewportIndexedf_State(GLuint index, GLfloat x, GLfloat y, GLfloat w, GLfloat h) { + if (!ValidateViewportIndex(index, "ViewportIndexedf_State")) return; + if (!ValidateNonNegativeExtent(w, h, "ViewportIndexedf_State")) return; + + MG_State::pGLContext->SetViewportIndexed(index, FloatVec4(x, y, w, h)); + } + + void ScissorArrayv_State(GLuint first, GLsizei count, const GLint* v) { + if (!ValidateViewportRange(first, count, "ScissorArrayv_State")) return; + if (count == 0) return; + if (!ValidateNonNullArray(v, "ScissorArrayv_State")) return; + if (!ValidateArrayExtents(count, v, "ScissorArrayv_State")) return; + + for (GLsizei i = 0; i < count; ++i) { + MG_State::pGLContext->SetScissorBoxIndexed(first + static_cast(i), + IntVec4(v[i * 4 + 0], v[i * 4 + 1], v[i * 4 + 2], v[i * 4 + 3])); + } + } + + void ScissorIndexed_State(GLuint index, GLint left, GLint bottom, GLsizei width, GLsizei height) { + if (!ValidateViewportIndex(index, "ScissorIndexed_State")) return; + if (!ValidateNonNegativeExtent(width, height, "ScissorIndexed_State")) return; + + MG_State::pGLContext->SetScissorBoxIndexed(index, IntVec4(left, bottom, width, height)); + } + + void DepthRangeArrayv_State(GLuint first, GLsizei count, const GLdouble* v) { + if (!ValidateViewportRange(first, count, "DepthRangeArrayv_State")) return; + if (count == 0) return; + if (!ValidateNonNullArray(v, "DepthRangeArrayv_State")) return; + + for (GLsizei i = 0; i < count; ++i) { + MG_State::pGLContext->SetDepthRangeIndexed( + first + static_cast(i), + FloatVec2(ClampUnitFloat(static_cast(v[i * 2 + 0])), + ClampUnitFloat(static_cast(v[i * 2 + 1])))); + } + } + + void DepthRangeIndexed_State(GLuint index, GLdouble n, GLdouble f) { + if (!ValidateViewportIndex(index, "DepthRangeIndexed_State")) return; + + MG_State::pGLContext->SetDepthRangeIndexed( + index, FloatVec2(ClampUnitFloat(static_cast(n)), ClampUnitFloat(static_cast(f)))); + } + void StencilOpSeparate_State(GLenum face, GLenum sfail, GLenum dpfail, GLenum dppass) { Bool applyFront = false; Bool applyBack = false; @@ -175,12 +319,7 @@ namespace MobileGL::MG_Impl::GLImpl { } void Scissor_State(GLint x, GLint y, GLsizei width, GLsizei height) { - if (width < 0 || height < 0) { - MG_State::pGLContext->RecordError(ErrorCode::InvalidValue, - MakeUnique("MG_Impl/GLImpl", "Scissor_State", - "Width abd height must be non-negative.")); - return; - } + if (!ValidateNonNegativeExtent(width, height, "Scissor_State")) return; MG_State::pGLContext->SetScissorBox(IntVec4(x, y, width, height)); } @@ -336,7 +475,7 @@ namespace MobileGL::MG_Impl::GLImpl { } GLboolean IsEnabledi_State(GLenum target, GLuint index) { - if (!ValidateIndexedBlendCapability(target, index, "IsEnabledi_State")) { + if (!ValidateIndexedCapability(target, index, "IsEnabledi_State")) { return GL_FALSE; } @@ -392,7 +531,14 @@ namespace MobileGL::MG_Impl::GLImpl { } GLint values[4] = {}; GetIntegeri_v(target, index, values); - *data = values[0] != 0 ? GL_TRUE : GL_FALSE; + // The ARB_viewport_array rectangles are the only multi-component indexed state that + // reaches here; writing element 0 alone would leave the caller's other three untouched. + const GLsizei components = target == GL_VIEWPORT || target == GL_SCISSOR_BOX + ? 4 + : (target == GL_DEPTH_RANGE ? 2 : 1); + for (GLsizei i = 0; i < components; ++i) { + data[i] = values[i] != 0 ? GL_TRUE : GL_FALSE; + } } GLboolean IsEnabled_State(GLenum cap) { @@ -725,7 +871,7 @@ namespace MobileGL::MG_Impl::GLImpl { } void Disablei_State(GLenum target, GLuint index) { - if (!ValidateIndexedBlendCapability(target, index, "Disablei_State")) { + if (!ValidateIndexedCapability(target, index, "Disablei_State")) { return; } @@ -743,7 +889,7 @@ namespace MobileGL::MG_Impl::GLImpl { } void Enablei_State(GLenum target, GLuint index) { - if (!ValidateIndexedBlendCapability(target, index, "Enablei_State")) { + if (!ValidateIndexedCapability(target, index, "Enablei_State")) { return; } @@ -797,6 +943,44 @@ namespace MobileGL::MG_Impl::GLImpl { Viewport_State(x, y, width, height); } + void ViewportArrayv(GLuint first, GLsizei count, const GLfloat* v) { + ViewportArrayv_State(first, count, v); + } + + void ViewportIndexedf(GLuint index, GLfloat x, GLfloat y, GLfloat w, GLfloat h) { + ViewportIndexedf_State(index, x, y, w, h); + } + + void ViewportIndexedfv(GLuint index, const GLfloat* v) { + // The index is validated before the pointer is touched: glViewportIndexedfv(MAX, nullptr) + // must be one GL_INVALID_VALUE, not a null dereference. + if (!ValidateViewportIndex(index, "ViewportIndexedfv")) return; + if (!ValidateNonNullArray(v, "ViewportIndexedfv")) return; + ViewportIndexedf_State(index, v[0], v[1], v[2], v[3]); + } + + void ScissorArrayv(GLuint first, GLsizei count, const GLint* v) { + ScissorArrayv_State(first, count, v); + } + + void ScissorIndexed(GLuint index, GLint left, GLint bottom, GLsizei width, GLsizei height) { + ScissorIndexed_State(index, left, bottom, width, height); + } + + void ScissorIndexedv(GLuint index, const GLint* v) { + if (!ValidateViewportIndex(index, "ScissorIndexedv")) return; + if (!ValidateNonNullArray(v, "ScissorIndexedv")) return; + ScissorIndexed_State(index, v[0], v[1], v[2], v[3]); + } + + void DepthRangeArrayv(GLuint first, GLsizei count, const GLdouble* v) { + DepthRangeArrayv_State(first, count, v); + } + + void DepthRangeIndexed(GLuint index, GLdouble n, GLdouble f) { + DepthRangeIndexed_State(index, n, f); + } + void StencilOpSeparate(GLenum face, GLenum sfail, GLenum dpfail, GLenum dppass) { StencilOpSeparate_State(face, sfail, dpfail, dppass); } diff --git a/MobileGL/MG_Impl/GLImpl/RenderState/GL_RenderState.h b/MobileGL/MG_Impl/GLImpl/RenderState/GL_RenderState.h index 1fe19f4a..30b668eb 100644 --- a/MobileGL/MG_Impl/GLImpl/RenderState/GL_RenderState.h +++ b/MobileGL/MG_Impl/GLImpl/RenderState/GL_RenderState.h @@ -20,6 +20,16 @@ namespace MobileGL::MG_Impl::GLImpl { void Enablei(GLenum target, GLuint index); void BlendFunc(GLenum sfactor, GLenum dfactor); void Viewport(GLint x, GLint y, GLsizei width, GLsizei height); + // ARB_viewport_array (core since GL 4.1). Every one of these addresses the same 16-element + // indexed state the classic glViewport/glScissor/glDepthRange trio broadcasts to. + void ViewportArrayv(GLuint first, GLsizei count, const GLfloat* v); + void ViewportIndexedf(GLuint index, GLfloat x, GLfloat y, GLfloat w, GLfloat h); + void ViewportIndexedfv(GLuint index, const GLfloat* v); + void ScissorArrayv(GLuint first, GLsizei count, const GLint* v); + void ScissorIndexed(GLuint index, GLint left, GLint bottom, GLsizei width, GLsizei height); + void ScissorIndexedv(GLuint index, const GLint* v); + void DepthRangeArrayv(GLuint first, GLsizei count, const GLdouble* v); + void DepthRangeIndexed(GLuint index, GLdouble n, GLdouble f); void StencilOpSeparate(GLenum face, GLenum sfail, GLenum dpfail, GLenum dppass); void StencilOp(GLenum fail, GLenum zfail, GLenum zpass); void StencilMaskSeparate(GLenum face, GLuint mask); diff --git a/MobileGL/MG_State/GLState/Core.cpp b/MobileGL/MG_State/GLState/Core.cpp index 3d8cd5e9..03c5c165 100644 --- a/MobileGL/MG_State/GLState/Core.cpp +++ b/MobileGL/MG_State/GLState/Core.cpp @@ -712,10 +712,18 @@ namespace MobileGL::MG_State { m_renderState.SetViewport(viewport); } - const IntVec4& GLContext::GetViewport() const { + IntVec4 GLContext::GetViewport() const { return m_renderState.GetViewport(); } + void GLContext::SetViewportIndexed(Uint index, FloatVec4 viewport) { + m_renderState.SetViewportIndexed(index, viewport); + } + + const FloatVec4& GLContext::GetViewportIndexed(Uint index) const { + return m_renderState.GetViewportIndexed(index); + } + void GLContext::SetLineWidth(Float width) { m_renderState.SetLineWidth(width); } @@ -953,6 +961,14 @@ namespace MobileGL::MG_State { return m_renderState.GetDepthRange(); } + void GLContext::SetDepthRangeIndexed(Uint index, FloatVec2 range) { + m_renderState.SetDepthRangeIndexed(index, range); + } + + const FloatVec2& GLContext::GetDepthRangeIndexed(Uint index) const { + return m_renderState.GetDepthRangeIndexed(index); + } + void GLContext::SetSampleCoverage(Float value, Bool invert) { m_renderState.SetSampleCoverage(value, invert); } @@ -1017,6 +1033,14 @@ namespace MobileGL::MG_State { return m_renderState.GetScissorBox(); } + void GLContext::SetScissorBoxIndexed(Uint index, IntVec4 box) { + m_renderState.SetScissorBoxIndexed(index, box); + } + + const IntVec4& GLContext::GetScissorBoxIndexed(Uint index) const { + return m_renderState.GetScissorBoxIndexed(index); + } + // Framebuffer void GLContext::GenFramebufferNames(Uint number, Vector& framebuffers) { m_framebufferState.GenerateNames(number, framebuffers); diff --git a/MobileGL/MG_State/GLState/Core.h b/MobileGL/MG_State/GLState/Core.h index 1d26519a..73b28fcf 100644 --- a/MobileGL/MG_State/GLState/Core.h +++ b/MobileGL/MG_State/GLState/Core.h @@ -198,8 +198,10 @@ namespace MobileGL { // Only the pipeline-relevant subset - see RenderState::m_pipelineStateVersion. Uint GetPipelineStateVersion() const; const RenderStateParameters& GetRenderStateParameters() const; - void SetViewport(IntVec4 viewport); // x, y, width, height - const IntVec4& GetViewport() const; // x, y, width, height + void SetViewport(IntVec4 viewport); // x, y, width, height; writes ALL viewports + IntVec4 GetViewport() const; // x, y, width, height; viewport 0, rounded + void SetViewportIndexed(Uint index, FloatVec4 viewport); + const FloatVec4& GetViewportIndexed(Uint index) const; void SetLineWidth(Float width); Float GetLineWidth() const; void SetPointSize(Float size); @@ -260,8 +262,10 @@ namespace MobileGL { Uint32 GetClearStencil() const; void SetBlendColor(FloatVec4 color); const FloatVec4& GetBlendColor() const; - void SetDepthRange(FloatVec2 range); + void SetDepthRange(FloatVec2 range); // writes ALL viewports' depth ranges const FloatVec2& GetDepthRange() const; + void SetDepthRangeIndexed(Uint index, FloatVec2 range); + const FloatVec2& GetDepthRangeIndexed(Uint index) const; void SetSampleCoverage(Float value, Bool invert); Float GetSampleCoverageValue() const; Bool GetSampleCoverageInvert() const; @@ -276,8 +280,10 @@ namespace MobileGL { FrontFaceMode GetFrontFaceMode() const; void SetProvokingVertexMode(ProvokingVertexMode mode); ProvokingVertexMode GetProvokingVertexMode() const; - void SetScissorBox(IntVec4 box); // x, y, width, height - const IntVec4& GetScissorBox() const; // x, y, width, height + void SetScissorBox(IntVec4 box); // x, y, width, height; writes ALL rectangles + const IntVec4& GetScissorBox() const; // x, y, width, height; rectangle 0 + void SetScissorBoxIndexed(Uint index, IntVec4 box); + const IntVec4& GetScissorBoxIndexed(Uint index) const; // Transform feedback. The fields below are the state of the transform // feedback object currently bound to GL_TRANSFORM_FEEDBACK; see the object diff --git a/MobileGL/MG_State/GLState/RenderState/RenderState.cpp b/MobileGL/MG_State/GLState/RenderState/RenderState.cpp index 22c8292b..f2eb4503 100644 --- a/MobileGL/MG_State/GLState/RenderState/RenderState.cpp +++ b/MobileGL/MG_State/GLState/RenderState/RenderState.cpp @@ -25,6 +25,12 @@ namespace MobileGL { return 0; } } + + // Every viewport's scissor-test bit set, i.e. what glEnable(GL_SCISSOR_TEST) writes. + constexpr Uint32 kAllViewportsMask = + RenderStateParameters::MAX_VIEWPORTS >= 32 + ? ~0u + : (1u << RenderStateParameters::MAX_VIEWPORTS) - 1u; } // namespace RenderState::RenderState() { @@ -32,6 +38,15 @@ namespace MobileGL { for (auto& mask : m_parameters.ColorMasks) { mask = BoolVec4(true, true, true, true); } + // Every viewport's depth range starts at (0, 1) - GL 4.6 core table 23.4. The + // viewport and scissor rectangles legitimately start all-zero here: their spec + // initial value is the size of the window the context is first made current to, + // which the frontend does not know yet, so an all-zero rectangle means "never + // written" and the backends resolve it against the live surface (see + // DirectGLES' SyncRenderState and VulkanRenderer's ApplyGLViewportState). + for (auto& range : m_parameters.DepthRanges) { + range = FloatVec2(0.0f, 1.0f); + } } Uint RenderState::GetVersion() const { @@ -47,15 +62,47 @@ namespace MobileGL { } // -------------------- Rasterization -------------------- + // ARB_viewport_array, "Additions to Chapter 2": Viewport(x, y, w, h) is equivalent to + // ViewportIndexedf(i, x, y, w, h) for every i in [0, MAX_VIEWPORTS) - it is not a + // synonym for "viewport 0". void RenderState::SetViewport(IntVec4 viewport) { - if (m_parameters.Viewport == viewport) return; + const FloatVec4 asFloat(static_cast(viewport.x()), static_cast(viewport.y()), + static_cast(viewport.z()), static_cast(viewport.w())); + Bool stateChanged = false; + for (auto& stored : m_parameters.Viewports) { + if (stored == asFloat) continue; + stored = asFloat; + stateChanged = true; + } + if (stateChanged) ++m_version; + } - m_parameters.Viewport = viewport; + IntVec4 RenderState::GetViewport() const { + const FloatVec4& viewport = m_parameters.Viewports[0]; + // Round rather than truncate: glGetIntegerv on floating-point state rounds to + // nearest (GL 4.6 core 22.2), and truncating a 63.5-wide viewport to 63 would + // also hand the backends a rectangle one pixel short of what was asked for. + return IntVec4(static_cast(std::lround(viewport.x())), static_cast(std::lround(viewport.y())), + static_cast(std::lround(viewport.z())), static_cast(std::lround(viewport.w()))); + } + + void RenderState::SetViewportIndexed(Uint index, FloatVec4 viewport) { + if (index >= RenderStateParameters::MAX_VIEWPORTS) { + MOBILEGL_ASSERT(false, "Viewport index out of range: %u", index); + return; + } + if (m_parameters.Viewports[index] == viewport) return; + + m_parameters.Viewports[index] = viewport; ++m_version; } - const IntVec4& RenderState::GetViewport() const { - return m_parameters.Viewport; + const FloatVec4& RenderState::GetViewportIndexed(Uint index) const { + if (index >= RenderStateParameters::MAX_VIEWPORTS) { + MOBILEGL_ASSERT(false, "Viewport index out of range: %u", index); + return m_parameters.Viewports[0]; + } + return m_parameters.Viewports[index]; } void RenderState::SetLineWidth(Float width) { @@ -223,7 +270,6 @@ namespace MobileGL { SET_CAPABILITY(SampleAlphaToOne, enabled); SET_CAPABILITY(SampleCoverage, enabled); SET_CAPABILITY(SampleMask, enabled); - SET_CAPABILITY(ScissorTest, enabled); SET_CAPABILITY(StencilTest, enabled); SET_CAPABILITY(ProgramPointSize, enabled); case CapabilityInput::Blend: { @@ -236,6 +282,17 @@ namespace MobileGL { if (stateChanged) BumpVersions(); break; } + // GL 4.6 core 17.3.2: the non-indexed Enable/Disable(SCISSOR_TEST) enables or + // disables the test for ALL viewports, exactly like glViewport writes all + // viewports. Anything narrower fails KHR-GL43.viewport_array.scissor_test_state_api, + // whose "enable all" phase reads every index back through glIsEnabledi. + case CapabilityInput::ScissorTest: { + const Uint32 updated = enabled ? kAllViewportsMask : 0u; + if (m_parameters.ScissorTestEnabledMask == updated) break; + m_parameters.ScissorTestEnabledMask = updated; + BumpVersions(); + break; + } case CapabilityInput::ClipDistance0: case CapabilityInput::ClipDistance1: case CapabilityInput::ClipDistance2: @@ -287,11 +344,14 @@ namespace MobileGL { RETURN_CAPABILITY(SampleAlphaToOne); RETURN_CAPABILITY(SampleCoverage); RETURN_CAPABILITY(SampleMask); - RETURN_CAPABILITY(ScissorTest); RETURN_CAPABILITY(StencilTest); RETURN_CAPABILITY(ProgramPointSize); case CapabilityInput::Blend: return m_parameters.BlendStates[0].Enabled; + // The non-indexed query of an indexed capability answers for index 0 + // (GL 4.6 core 22.1), which is also the only bit either backend consumes today. + case CapabilityInput::ScissorTest: + return (m_parameters.ScissorTestEnabledMask & 1u) != 0; case CapabilityInput::ClipDistance0: case CapabilityInput::ClipDistance1: case CapabilityInput::ClipDistance2: @@ -307,13 +367,29 @@ namespace MobileGL { } void RenderState::SetCapabilityIndexed(CapabilityInput cap, Uint index, Bool enabled) { - // Only for BlendState currently. The GL entry points (glEnablei/glDisablei) already - // reject every non-GL_BLEND target with GL_INVALID_ENUM before reaching here, so this - // is a backstop - but it must stay a backstop: THROW_UNIMPL_EXCEPTION unwinds a C++ - // exception through the C GL ABI and terminates the process. + // GL_BLEND (indexed by draw buffer) and GL_SCISSOR_TEST (indexed by viewport) are + // the only indexed capabilities in GL 4.6 core. The GL entry points + // (glEnablei/glDisablei) already reject every other target with GL_INVALID_ENUM + // and every out-of-range index with GL_INVALID_VALUE before reaching here, so the + // guards below are backstops - but they must stay backstops: + // THROW_UNIMPL_EXCEPTION unwinds a C++ exception through the C GL ABI and + // terminates the process. + if (cap == CapabilityInput::ScissorTest) { + if (index >= RenderStateParameters::MAX_VIEWPORTS) { + MOBILEGL_ASSERT(false, "Scissor test capability index out of range: %u", index); + return; + } + const Uint32 bit = 1u << index; + const Uint32 updated = enabled ? (m_parameters.ScissorTestEnabledMask | bit) + : (m_parameters.ScissorTestEnabledMask & ~bit); + if (updated == m_parameters.ScissorTestEnabledMask) return; + m_parameters.ScissorTestEnabledMask = updated; + BumpVersions(); + return; + } if (cap != CapabilityInput::Blend) { MGLOG_I("RenderState::SetCapabilityIndexed: indexed capability state exists only for " - "GL_BLEND (cap=%d, index=%u); ignoring", + "GL_BLEND and GL_SCISSOR_TEST (cap=%d, index=%u); ignoring", static_cast(cap), index); return; } @@ -328,9 +404,17 @@ namespace MobileGL { } Bool RenderState::IsCapabilityEnabledIndexed(CapabilityInput cap, Uint index) const { - // Only for BlendState currently - same backstop reasoning as SetCapabilityIndexed: - // glIsEnabledi has already answered GL_INVALID_ENUM/GL_FALSE for anything else, and a - // query must never be able to terminate the process. + // GL_BLEND and GL_SCISSOR_TEST only - same backstop reasoning as + // SetCapabilityIndexed: glIsEnabledi has already answered + // GL_INVALID_ENUM/GL_INVALID_VALUE for anything else, and a query must never be + // able to terminate the process. + if (cap == CapabilityInput::ScissorTest) { + if (index >= RenderStateParameters::MAX_VIEWPORTS) { + MOBILEGL_ASSERT(false, "Scissor test capability index out of range: %u", index); + return false; + } + return (m_parameters.ScissorTestEnabledMask & (1u << index)) != 0; + } if (cap != CapabilityInput::Blend) { MGLOG_I("RenderState::IsCapabilityEnabledIndexed: indexed capability state exists only " "for GL_BLEND (cap=%d, index=%u); reporting disabled", @@ -591,15 +675,39 @@ namespace MobileGL { return m_parameters.BlendColor; } + // Like Viewport: ARB_viewport_array makes DepthRange(n, f) the same as + // DepthRangeIndexed(i, n, f) for every i. void RenderState::SetDepthRange(FloatVec2 range) { - if (m_parameters.DepthRange == range) return; - - m_parameters.DepthRange = range; - ++m_version; + Bool stateChanged = false; + for (auto& stored : m_parameters.DepthRanges) { + if (stored == range) continue; + stored = range; + stateChanged = true; + } + if (stateChanged) ++m_version; } const FloatVec2& RenderState::GetDepthRange() const { - return m_parameters.DepthRange; + return m_parameters.DepthRanges[0]; + } + + void RenderState::SetDepthRangeIndexed(Uint index, FloatVec2 range) { + if (index >= RenderStateParameters::MAX_VIEWPORTS) { + MOBILEGL_ASSERT(false, "Depth range index out of range: %u", index); + return; + } + if (m_parameters.DepthRanges[index] == range) return; + + m_parameters.DepthRanges[index] = range; + ++m_version; + } + + const FloatVec2& RenderState::GetDepthRangeIndexed(Uint index) const { + if (index >= RenderStateParameters::MAX_VIEWPORTS) { + MOBILEGL_ASSERT(false, "Depth range index out of range: %u", index); + return m_parameters.DepthRanges[0]; + } + return m_parameters.DepthRanges[index]; } void RenderState::SetSampleCoverage(Float value, Bool invert) { @@ -726,15 +834,39 @@ namespace MobileGL { } // --------------------- Scissor --------------------- + // Like Viewport: ARB_viewport_array makes Scissor(x, y, w, h) the same as + // ScissorIndexed(i, x, y, w, h) for every i. void RenderState::SetScissorBox(IntVec4 box) { - if (m_parameters.ScissorBox == box) return; - - m_parameters.ScissorBox = box; - ++m_version; + Bool stateChanged = false; + for (auto& stored : m_parameters.ScissorBoxes) { + if (stored == box) continue; + stored = box; + stateChanged = true; + } + if (stateChanged) ++m_version; } const IntVec4& RenderState::GetScissorBox() const { - return m_parameters.ScissorBox; + return m_parameters.ScissorBoxes[0]; + } + + void RenderState::SetScissorBoxIndexed(Uint index, IntVec4 box) { + if (index >= RenderStateParameters::MAX_VIEWPORTS) { + MOBILEGL_ASSERT(false, "Scissor box index out of range: %u", index); + return; + } + if (m_parameters.ScissorBoxes[index] == box) return; + + m_parameters.ScissorBoxes[index] = box; + ++m_version; + } + + const IntVec4& RenderState::GetScissorBoxIndexed(Uint index) const { + if (index >= RenderStateParameters::MAX_VIEWPORTS) { + MOBILEGL_ASSERT(false, "Scissor box index out of range: %u", index); + return m_parameters.ScissorBoxes[0]; + } + return m_parameters.ScissorBoxes[index]; } } // namespace GLState } // namespace MG_State diff --git a/MobileGL/MG_State/GLState/RenderState/RenderState.h b/MobileGL/MG_State/GLState/RenderState/RenderState.h index 52009e79..a79cd148 100644 --- a/MobileGL/MG_State/GLState/RenderState/RenderState.h +++ b/MobileGL/MG_State/GLState/RenderState/RenderState.h @@ -220,8 +220,21 @@ namespace MobileGL { }; struct RenderStateParameters { + // ARB_viewport_array / GL 4.6 core 13.6.1: the viewport, the scissor rectangle, the depth + // range and the scissor-test enable are all arrays indexed by gl_ViewportIndex, and the + // spec floor for MAX_VIEWPORTS is 16. MobileGL advertises exactly 16 on both backends, so + // this is also what GL_MAX_VIEWPORTS reports (see the backend loaders' caps.MaxViewports). + static constexpr Uint MAX_VIEWPORTS = 16; + // Rasterization - IntVec4 Viewport = IntVec4(0, 0, 0, 0); // x, y, width, height + // The viewport rectangle is FLOAT state as of GL 4.1 - ViewportIndexedf writes fractional + // values and GetFloati_v(GL_VIEWPORT) must hand them back bit-exact + // (KHR-GL43.viewport_array.viewport_api compares with ==, no tolerance). glViewport's + // integers are simply one way to write it. Index 0 is what a program that never assigns + // gl_ViewportIndex rasterizes against, and what the classic glViewport / + // glGetIntegerv(GL_VIEWPORT) pair addresses; both backends consume index 0 only, rounded + // back to integers (GL_VIEWPORT_SUBPIXEL_BITS is 0, so rounding is the honest answer). + Array Viewports{}; // x, y, width, height Float LineWidth = 1.0f; Float PointSize = 1.0f; // GL_PATCH_VERTICES: how many vertices one tessellation patch consumes. @@ -247,7 +260,13 @@ namespace MobileGL { Float ClearDepth = 1.0f; Uint32 ClearStencil = 0; FloatVec4 BlendColor = FloatVec4(0.0f, 0.0f, 0.0f, 0.0f); - FloatVec2 DepthRange = FloatVec2(0.0f, 1.0f); + // Per-viewport depth range (glDepthRangeIndexed / glDepthRangeArrayv). Every entry is + // initialized to (0, 1) in RenderState's constructor - a default member initializer would + // not survive the Array<> aggregate. Kept float rather than double: DepthRangeArrayv takes + // GLdouble, but the value reaches the hardware as VkViewport::minDepth/maxDepth (float) on + // Magma and glDepthRangef on Espryt, so a double store would only widen the readback and + // then lose it again at the same place. + Array DepthRanges{}; Float SampleCoverageValue = 1.0f; Bool SampleCoverageInvert = false; Uint32 SampleMaskValue = 0xffffffffu; @@ -299,10 +318,15 @@ namespace MobileGL { Bool SampleAlphaToOneEnabled = false; Bool SampleCoverageEnabled = false; Bool SampleMaskEnabled = false; - Bool ScissorTestEnabled = false; Bool StencilTestEnabled = false; Bool ProgramPointSizeEnabled = false; - IntVec4 ScissorBox = IntVec4(0, 0, 0, 0); // x, y, width, height + // glEnable(GL_SCISSOR_TEST) enables the test for EVERY viewport, glEnablei for one + // (GL 4.6 core 17.3.2), so this is 16 bits and not a bool. Bit 0 is what the classic + // glIsEnabled(GL_SCISSOR_TEST) reports and what both backends currently consume. Unlike + // ClipDistanceEnabledMask below it DOES bump the pipeline version, because DirectGLES + // turns it into a real glEnable/glDisable. + Uint32 ScissorTestEnabledMask = 0; + Array ScissorBoxes{}; // x, y, width, height // glEnable(GL_CLIP_DISTANCE0 + i) for i in [0, 8), one bit each. A bitmask rather than // eight bools because every consumer wants the set, not an individual flag, and because // the SYNC_CAPABILITY/SET_CAPABILITY macros key off a "Enabled" field name that @@ -323,8 +347,14 @@ namespace MobileGL { const RenderStateParameters& GetAllParameters() const; // Rasterization + // ARB_viewport_array defines glViewport as ViewportIndexedf on EVERY index, so the + // classic setter broadcasts; GetViewport answers for index 0 (rounded to the + // integers glGetIntegerv(GL_VIEWPORT) and both backends want) and is BY VALUE for + // that reason. The indexed pair is the verbatim float state. void SetViewport(IntVec4 viewport); // x, y, width, height - const IntVec4& GetViewport() const; // x, y, width, height + IntVec4 GetViewport() const; // x, y, width, height, viewport 0, rounded + void SetViewportIndexed(Uint index, FloatVec4 viewport); + const FloatVec4& GetViewportIndexed(Uint index) const; void SetLineWidth(Float width); Float GetLineWidth() const; void SetPointSize(Float size); @@ -400,8 +430,12 @@ namespace MobileGL { Uint32 GetClearStencil() const; void SetBlendColor(FloatVec4 color); const FloatVec4& GetBlendColor() const; + // glDepthRange(f) writes every viewport's range (ARB_viewport_array); the indexed + // pair is glDepthRangeIndexed / glDepthRangeArrayv. GetDepthRange answers index 0. void SetDepthRange(FloatVec2 range); const FloatVec2& GetDepthRange() const; + void SetDepthRangeIndexed(Uint index, FloatVec2 range); + const FloatVec2& GetDepthRangeIndexed(Uint index) const; void SetSampleCoverage(Float value, Bool invert); Float GetSampleCoverageValue() const; Bool GetSampleCoverageInvert() const; @@ -421,9 +455,12 @@ namespace MobileGL { void SetProvokingVertexMode(ProvokingVertexMode mode); ProvokingVertexMode GetProvokingVertexMode() const; - // Scissor + // Scissor. glScissor writes every rectangle (ARB_viewport_array); GetScissorBox + // answers for index 0. void SetScissorBox(IntVec4 box); // x, y, width, height const IntVec4& GetScissorBox() const; // x, y, width, height + void SetScissorBoxIndexed(Uint index, IntVec4 box); + const IntVec4& GetScissorBoxIndexed(Uint index) const; private: // Bump both: any state change invalidates the draw snapshot, and this one also diff --git a/MobileGL/MG_Test/State/RenderStateTest.cpp b/MobileGL/MG_Test/State/RenderStateTest.cpp index 8a5ee533..66ed6033 100644 --- a/MobileGL/MG_Test/State/RenderStateTest.cpp +++ b/MobileGL/MG_Test/State/RenderStateTest.cpp @@ -6,11 +6,20 @@ // SPDX-License-Identifier: LGPL-3.0-only // End of Source File Header // -// Indexed capability state (glEnablei/glDisablei/glIsEnabledi) exists only for GL_BLEND in this -// stack. Every other capability must come back as GL_INVALID_ENUM per GL 4.6 sec. 17.3.3 - and, -// far more importantly, must come back at all: RenderState::SetCapabilityIndexed and -// IsCapabilityEnabledIndexed used to answer a non-blend capability with THROW_UNIMPL_EXCEPTION, +// Indexed capability state (glEnablei/glDisablei/glIsEnabledi) exists for exactly two +// capabilities: GL_BLEND, indexed by draw buffer, and GL_SCISSOR_TEST, indexed by viewport +// (ARB_viewport_array). Every other capability must come back as GL_INVALID_ENUM per GL 4.6 +// sec. 17.3.3 - and, far more importantly, must come back at all: RenderState::SetCapabilityIndexed +// and IsCapabilityEnabledIndexed used to answer a non-blend capability with THROW_UNIMPL_EXCEPTION, // which unwinds a C++ exception through the C GL ABI and terminates the process. +// +// The second half of this file is the ARB_viewport_array indexed rectangle state. Every one of +// glViewportArrayv/glViewportIndexedf(v)/glScissorArrayv/glScissorIndexed(v)/glDepthRangeArrayv/ +// glDepthRangeIndexed was a MGLOG_W_ONCE stub that raised no error and stored nothing, and the +// indexed getters answered EVERY index with viewport 0's value, so a set/get round trip silently +// reported the initial state. The assertions below are deliberately state-shaped rather than +// render-shaped: this IS the state machine, and the rendering half (gl_ViewportIndex routing) is +// asserted separately in MG_IntegrationTest/Scenarios/ViewportArrayScenario.cpp. #include @@ -21,6 +30,7 @@ #include #include #include +#include using namespace MobileGL; @@ -50,10 +60,11 @@ namespace { }; } // namespace -TEST_F(RenderStateTest, IndexedCapabilityTogglesRejectNonBlendCapabilities) { +TEST_F(RenderStateTest, IndexedCapabilityTogglesRejectNonIndexedCapabilities) { // GL_CLIP_DISTANCE0 is a real capability, just not an indexed one - the shape an application or - // a CTS negative test would hit. - for (const GLenum cap : {GL_CLIP_DISTANCE0, GL_DEPTH_TEST, GL_SCISSOR_TEST}) { + // a CTS negative test would hit. GL_SCISSOR_TEST used to be in this list and is not any more: + // ARB_viewport_array makes it the second indexed capability (see the tests below). + for (const GLenum cap : {GL_CLIP_DISTANCE0, GL_DEPTH_TEST, GL_STENCIL_TEST}) { MG_Impl::GLImpl::Enablei(cap, 0); ExpectSingleGlError(GL_INVALID_ENUM); @@ -89,3 +100,462 @@ TEST_F(RenderStateTest, IndexedBlendTogglesStillWork) { EXPECT_EQ(MG_Impl::GLImpl::IsEnabledi(GL_BLEND, 1), GL_FALSE); EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); } + +// --------------------------------------------------------------------------------------------- +// ARB_viewport_array: indexed viewport / scissor / depth-range state +// --------------------------------------------------------------------------------------------- + +namespace { + constexpr GLuint kMaxViewports = RenderStateParameters::MAX_VIEWPORTS; + + Array, kMaxViewports> ReadAllViewports() { + Array, kMaxViewports> out{}; + for (GLuint i = 0; i < kMaxViewports; ++i) { + MG_Impl::GLImpl::GetFloati_v(GL_VIEWPORT, i, out[i].data()); + } + return out; + } + + Array, kMaxViewports> ReadAllDepthRanges() { + Array, kMaxViewports> out{}; + for (GLuint i = 0; i < kMaxViewports; ++i) { + MG_Impl::GLImpl::GetDoublei_v(GL_DEPTH_RANGE, i, out[i].data()); + } + return out; + } +} // namespace + +TEST_F(RenderStateTest, ScissorTestIsIndexedByViewport) { + // The exact shape of KHR-GL43.viewport_array.scissor_test_state_api's toggle loop: one index + // is flipped and EVERY index is read back, so a broadcast masquerading as an indexed write + // cannot pass. + MG_Impl::GLImpl::Disable(GL_SCISSOR_TEST); + ExpectSingleGlError(GL_NO_ERROR); + + for (GLuint toggled = 0; toggled < kMaxViewports; ++toggled) { + MG_Impl::GLImpl::Enablei(GL_SCISSOR_TEST, toggled); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "index " << toggled; + for (GLuint i = 0; i < kMaxViewports; ++i) { + EXPECT_EQ(MG_Impl::GLImpl::IsEnabledi(GL_SCISSOR_TEST, i), i == toggled ? GL_TRUE : GL_FALSE) + << "enabled index " << toggled << ", read index " << i; + } + MG_Impl::GLImpl::Disablei(GL_SCISSOR_TEST, toggled); + EXPECT_EQ(MG_Impl::GLImpl::IsEnabledi(GL_SCISSOR_TEST, toggled), GL_FALSE); + } + ExpectSingleGlError(GL_NO_ERROR); +} + +TEST_F(RenderStateTest, NonIndexedScissorTestEnableWritesEveryViewport) { + // GL 4.6 core 17.3.2: Enable/Disable(SCISSOR_TEST) is "for all viewports". Reading only + // index 0 back would let a broadcast-less implementation through, so every index is checked. + MG_Impl::GLImpl::Enable(GL_SCISSOR_TEST); + for (GLuint i = 0; i < kMaxViewports; ++i) { + EXPECT_EQ(MG_Impl::GLImpl::IsEnabledi(GL_SCISSOR_TEST, i), GL_TRUE) << "index " << i; + } + // ... and the non-indexed query answers for viewport 0 (GL 4.6 core 22.1). + EXPECT_EQ(MG_Impl::GLImpl::IsEnabled(GL_SCISSOR_TEST), GL_TRUE); + + MG_Impl::GLImpl::Disable(GL_SCISSOR_TEST); + for (GLuint i = 0; i < kMaxViewports; ++i) { + EXPECT_EQ(MG_Impl::GLImpl::IsEnabledi(GL_SCISSOR_TEST, i), GL_FALSE) << "index " << i; + } + EXPECT_EQ(MG_Impl::GLImpl::IsEnabled(GL_SCISSOR_TEST), GL_FALSE); + + // An indexed enable on a NON-zero index must not move the non-indexed answer. + MG_Impl::GLImpl::Enablei(GL_SCISSOR_TEST, 3); + EXPECT_EQ(MG_Impl::GLImpl::IsEnabled(GL_SCISSOR_TEST), GL_FALSE); + MG_Impl::GLImpl::Enablei(GL_SCISSOR_TEST, 0); + EXPECT_EQ(MG_Impl::GLImpl::IsEnabled(GL_SCISSOR_TEST), GL_TRUE); + + MG_Impl::GLImpl::Disable(GL_SCISSOR_TEST); + ExpectSingleGlError(GL_NO_ERROR); +} + +TEST_F(RenderStateTest, ScissorTestEnableRejectsAnOutOfRangeViewportIndex) { + MG_Impl::GLImpl::Enablei(GL_SCISSOR_TEST, kMaxViewports); + ExpectSingleGlError(GL_INVALID_VALUE); + + MG_Impl::GLImpl::Disablei(GL_SCISSOR_TEST, kMaxViewports); + ExpectSingleGlError(GL_INVALID_VALUE); + + EXPECT_EQ(MG_Impl::GLImpl::IsEnabledi(GL_SCISSOR_TEST, kMaxViewports), GL_FALSE); + ExpectSingleGlError(GL_INVALID_VALUE); + + // MAX_VIEWPORTS - 1 is the last LEGAL index and must stay silent. + MG_Impl::GLImpl::Enablei(GL_SCISSOR_TEST, kMaxViewports - 1); + ExpectSingleGlError(GL_NO_ERROR); + MG_Impl::GLImpl::Disablei(GL_SCISSOR_TEST, kMaxViewports - 1); + ExpectSingleGlError(GL_NO_ERROR); +} + +TEST_F(RenderStateTest, MaxViewportsMatchesTheIndexedStateWidth) { + // The advertised limit and the width of the state arrays are the same number by + // construction; a divergence would make some index simultaneously legal to the CTS and + // out of range to the setters. + GLint maxViewports = 0; + MG_Impl::GLImpl::GetIntegerv(GL_MAX_VIEWPORTS, &maxViewports); + ExpectSingleGlError(GL_NO_ERROR); + EXPECT_EQ(maxViewports, static_cast(kMaxViewports)); + EXPECT_GE(maxViewports, 16) << "GL 4.3 core requires MAX_VIEWPORTS >= 16"; +} + +TEST_F(RenderStateTest, ViewportArrayvRoundTripsThroughEveryGetterWidth) { + Array written{}; + for (GLuint i = 0; i < kMaxViewports; ++i) { + written[i * 4 + 0] = static_cast(i) + 0.125f; + written[i * 4 + 1] = static_cast(i) + 0.25f; + written[i * 4 + 2] = static_cast(64 + i); + written[i * 4 + 3] = static_cast(32 + i); + } + MG_Impl::GLImpl::ViewportArrayv(0, kMaxViewports, written.data()); + ExpectSingleGlError(GL_NO_ERROR); + + for (GLuint i = 0; i < kMaxViewports; ++i) { + GLfloat asFloat[4] = {}; + MG_Impl::GLImpl::GetFloati_v(GL_VIEWPORT, i, asFloat); + // Bit-exact: the fractional origin is the whole point of float viewport state, and the + // CTS compares with == (0.125 and 0.25 are exact binary fractions, so this is fair). + EXPECT_EQ(asFloat[0], written[i * 4 + 0]) << "index " << i << " must round-trip verbatim"; + EXPECT_EQ(asFloat[1], written[i * 4 + 1]) << "index " << i; + EXPECT_EQ(asFloat[2], written[i * 4 + 2]) << "index " << i; + EXPECT_EQ(asFloat[3], written[i * 4 + 3]) << "index " << i; + + GLdouble asDouble[4] = {}; + MG_Impl::GLImpl::GetDoublei_v(GL_VIEWPORT, i, asDouble); + for (int c = 0; c < 4; ++c) { + EXPECT_EQ(asDouble[c], static_cast(written[i * 4 + c])) << "index " << i << " component " << c; + } + + // The integer widths round to nearest rather than truncate; the .5+ case is pinned by + // ViewportRoundsRatherThanTruncatesForIntegerQueries below. + GLint asInt[4] = {}; + MG_Impl::GLImpl::GetIntegeri_v(GL_VIEWPORT, i, asInt); + EXPECT_EQ(asInt[2], static_cast(64 + i)) << "index " << i; + EXPECT_EQ(asInt[3], static_cast(32 + i)) << "index " << i; + + GLint64 asInt64[4] = {}; + MG_Impl::GLImpl::GetInteger64i_v(GL_VIEWPORT, i, asInt64); + for (int c = 0; c < 4; ++c) { + EXPECT_EQ(asInt64[c], static_cast(asInt[c])) << "index " << i << " component " << c; + } + + GLboolean asBool[4] = {}; + MG_Impl::GLImpl::GetBooleani_v(GL_VIEWPORT, i, asBool); + EXPECT_EQ(asBool[2], GL_TRUE) << "index " << i << ": a non-zero width is GL_TRUE"; + } + ExpectSingleGlError(GL_NO_ERROR); +} + +TEST_F(RenderStateTest, ViewportRoundsRatherThanTruncatesForIntegerQueries) { + MG_Impl::GLImpl::ViewportIndexedf(2, 0.0f, 0.0f, 255.875f, 63.5f); + ExpectSingleGlError(GL_NO_ERROR); + + GLint asInt[4] = {}; + MG_Impl::GLImpl::GetIntegeri_v(GL_VIEWPORT, 2, asInt); + EXPECT_EQ(asInt[2], 256); + EXPECT_EQ(asInt[3], 64); + + GLfloat asFloat[4] = {}; + MG_Impl::GLImpl::GetFloati_v(GL_VIEWPORT, 2, asFloat); + EXPECT_EQ(asFloat[2], 255.875f) << "the integer query must not disturb the stored float"; + ExpectSingleGlError(GL_NO_ERROR); +} + +TEST_F(RenderStateTest, ViewportIndexedWritesTouchExactlyOneIndex) { + MG_Impl::GLImpl::Viewport(0, 0, 8, 8); + const auto before = ReadAllViewports(); + + for (GLuint target = 0; target < kMaxViewports; ++target) { + const GLfloat value[4] = {0.375f, 0.375f, 0.625f, 0.625f}; + // Alternate the two indexed entry points so both are covered by the isolation claim. + if (target % 2 == 0) { + MG_Impl::GLImpl::ViewportIndexedf(target, value[0], value[1], value[2], value[3]); + } else { + MG_Impl::GLImpl::ViewportIndexedfv(target, value); + } + ExpectSingleGlError(GL_NO_ERROR); + + const auto after = ReadAllViewports(); + for (GLuint i = 0; i < kMaxViewports; ++i) { + if (i == target) { + EXPECT_EQ(after[i][0], value[0]) << "index " << i; + EXPECT_EQ(after[i][2], value[2]) << "index " << i; + } else { + EXPECT_EQ(after[i], before[i]) << "write to " << target << " disturbed index " << i; + } + } + MG_Impl::GLImpl::ViewportIndexedf(target, before[target][0], before[target][1], before[target][2], + before[target][3]); + } + ExpectSingleGlError(GL_NO_ERROR); +} + +TEST_F(RenderStateTest, ClassicViewportWritesEveryIndexAndIsVisibleThroughIndexZero) { + // Both directions of the aliasing. ARB_viewport_array defines glViewport as ViewportIndexedf + // on every index, and glGetIntegerv(GL_VIEWPORT) as viewport 0. + MG_Impl::GLImpl::ViewportIndexedf(5, 1.0f, 2.0f, 3.0f, 4.0f); + MG_Impl::GLImpl::Viewport(0, 0, 1, 1); + ExpectSingleGlError(GL_NO_ERROR); + for (GLuint i = 0; i < kMaxViewports; ++i) { + GLfloat data[4] = {}; + MG_Impl::GLImpl::GetFloati_v(GL_VIEWPORT, i, data); + EXPECT_EQ(data[0], 0.0f) << "index " << i; + EXPECT_EQ(data[2], 1.0f) << "glViewport must overwrite index " << i; + } + + MG_Impl::GLImpl::ViewportIndexedf(0, 4.0f, 5.0f, 6.0f, 7.0f); + GLint classic[4] = {}; + MG_Impl::GLImpl::GetIntegerv(GL_VIEWPORT, classic); + EXPECT_EQ(classic[0], 4); + EXPECT_EQ(classic[2], 6); + GLfloat classicFloat[4] = {}; + MG_Impl::GLImpl::GetFloatv(GL_VIEWPORT, classicFloat); + EXPECT_EQ(classicFloat[2], 6.0f); + // Index 5 keeps its own value: writing index 0 is not a broadcast. + GLfloat other[4] = {}; + MG_Impl::GLImpl::GetFloati_v(GL_VIEWPORT, 5, other); + EXPECT_EQ(other[2], 1.0f); + ExpectSingleGlError(GL_NO_ERROR); +} + +TEST_F(RenderStateTest, ScissorBoxRoundTripsPerIndexAndAliasesIndexZero) { + Array written{}; + for (GLuint i = 0; i < kMaxViewports; ++i) { + written[i * 4 + 0] = static_cast(i); + written[i * 4 + 1] = static_cast(i * 2); + written[i * 4 + 2] = static_cast(16 + i); + written[i * 4 + 3] = static_cast(8 + i); + } + MG_Impl::GLImpl::ScissorArrayv(0, kMaxViewports, written.data()); + ExpectSingleGlError(GL_NO_ERROR); + + for (GLuint i = 0; i < kMaxViewports; ++i) { + GLint readBack[4] = {}; + MG_Impl::GLImpl::GetIntegeri_v(GL_SCISSOR_BOX, i, readBack); + for (int c = 0; c < 4; ++c) { + EXPECT_EQ(readBack[c], written[i * 4 + c]) << "index " << i << " component " << c; + } + } + + // Indexed writes stay indexed; both spellings. + MG_Impl::GLImpl::ScissorIndexed(4, 4, 4, 8, 8); + const GLint indexedV[4] = {9, 9, 12, 12}; + MG_Impl::GLImpl::ScissorIndexedv(7, indexedV); + ExpectSingleGlError(GL_NO_ERROR); + GLint probe[4] = {}; + MG_Impl::GLImpl::GetIntegeri_v(GL_SCISSOR_BOX, 4, probe); + EXPECT_EQ(probe[2], 8); + MG_Impl::GLImpl::GetIntegeri_v(GL_SCISSOR_BOX, 7, probe); + EXPECT_EQ(probe[2], 12); + MG_Impl::GLImpl::GetIntegeri_v(GL_SCISSOR_BOX, 5, probe); + EXPECT_EQ(probe[2], static_cast(16 + 5)) << "index 5 must be untouched"; + + // glScissor writes every rectangle, and glGetIntegerv(GL_SCISSOR_BOX) reports rectangle 0. + MG_Impl::GLImpl::Scissor(2, 3, 5, 6); + for (GLuint i = 0; i < kMaxViewports; ++i) { + MG_Impl::GLImpl::GetIntegeri_v(GL_SCISSOR_BOX, i, probe); + EXPECT_EQ(probe[0], 2) << "index " << i; + EXPECT_EQ(probe[2], 5) << "index " << i; + } + GLint classic[4] = {}; + MG_Impl::GLImpl::GetIntegerv(GL_SCISSOR_BOX, classic); + EXPECT_EQ(classic[2], 5); + ExpectSingleGlError(GL_NO_ERROR); +} + +TEST_F(RenderStateTest, DepthRangeRoundTripsPerIndexAndAliasesIndexZero) { + Array written{}; + for (GLuint i = 0; i < kMaxViewports; ++i) { + // Exact binary fractions, like the CTS uses: a float-backed store round-trips them. + written[i * 2 + 0] = static_cast(i) / 16.0; + written[i * 2 + 1] = 1.0 - static_cast(i) / 16.0; + } + MG_Impl::GLImpl::DepthRangeArrayv(0, kMaxViewports, written.data()); + ExpectSingleGlError(GL_NO_ERROR); + + const auto readBack = ReadAllDepthRanges(); + for (GLuint i = 0; i < kMaxViewports; ++i) { + EXPECT_EQ(readBack[i][0], written[i * 2 + 0]) << "index " << i; + EXPECT_EQ(readBack[i][1], written[i * 2 + 1]) << "index " << i; + } + + MG_Impl::GLImpl::DepthRangeIndexed(9, 0.25, 0.75); + ExpectSingleGlError(GL_NO_ERROR); + GLdouble probe[2] = {}; + MG_Impl::GLImpl::GetDoublei_v(GL_DEPTH_RANGE, 9, probe); + EXPECT_EQ(probe[0], 0.25); + EXPECT_EQ(probe[1], 0.75); + MG_Impl::GLImpl::GetDoublei_v(GL_DEPTH_RANGE, 8, probe); + EXPECT_EQ(probe[0], 8.0 / 16.0) << "index 8 must be untouched"; + + GLfloat asFloat[2] = {}; + MG_Impl::GLImpl::GetFloati_v(GL_DEPTH_RANGE, 9, asFloat); + EXPECT_EQ(asFloat[0], 0.25f); + EXPECT_EQ(asFloat[1], 0.75f); + + // glDepthRange writes every range; glGetDoublev(GL_DEPTH_RANGE) reports range 0. + MG_Impl::GLImpl::DepthRange(0.0, 1.0); + for (GLuint i = 0; i < kMaxViewports; ++i) { + MG_Impl::GLImpl::GetDoublei_v(GL_DEPTH_RANGE, i, probe); + EXPECT_EQ(probe[0], 0.0) << "index " << i; + EXPECT_EQ(probe[1], 1.0) << "index " << i; + } + MG_Impl::GLImpl::DepthRangeIndexed(0, 0.125, 0.875); + GLdouble classic[2] = {}; + MG_Impl::GLImpl::GetDoublev(GL_DEPTH_RANGE, classic); + EXPECT_EQ(classic[0], 0.125); + EXPECT_EQ(classic[1], 0.875); + MG_Impl::GLImpl::DepthRange(0.0, 1.0); + ExpectSingleGlError(GL_NO_ERROR); +} + +TEST_F(RenderStateTest, IndexedRectangleSettersRejectAnOutOfRangeIndex) { + const GLfloat viewport[4] = {0.0f, 0.0f, 1.0f, 1.0f}; + const GLint scissor[4] = {0, 0, 1, 1}; + + for (const GLuint index : {kMaxViewports, kMaxViewports + 1}) { + MG_Impl::GLImpl::ViewportIndexedf(index, 0.0f, 0.0f, 1.0f, 1.0f); + ExpectSingleGlError(GL_INVALID_VALUE); + MG_Impl::GLImpl::ViewportIndexedfv(index, viewport); + ExpectSingleGlError(GL_INVALID_VALUE); + MG_Impl::GLImpl::ScissorIndexed(index, 0, 0, 1, 1); + ExpectSingleGlError(GL_INVALID_VALUE); + MG_Impl::GLImpl::ScissorIndexedv(index, scissor); + ExpectSingleGlError(GL_INVALID_VALUE); + MG_Impl::GLImpl::DepthRangeIndexed(index, 0.0, 1.0); + ExpectSingleGlError(GL_INVALID_VALUE); + } + + // The last legal index must stay silent - api_errors checks both sides of the boundary. + MG_Impl::GLImpl::ViewportIndexedf(kMaxViewports - 1, 0.0f, 0.0f, 1.0f, 1.0f); + ExpectSingleGlError(GL_NO_ERROR); + MG_Impl::GLImpl::ScissorIndexed(kMaxViewports - 1, 0, 0, 1, 1); + ExpectSingleGlError(GL_NO_ERROR); + MG_Impl::GLImpl::DepthRangeIndexed(kMaxViewports - 1, 0.0, 1.0); + ExpectSingleGlError(GL_NO_ERROR); +} + +TEST_F(RenderStateTest, ArraySettersRejectAnOutOfRangeRangeButAcceptAnExactlyFullOne) { + Array viewports{}; + Array scissors{}; + Array depths{}; + for (GLuint i = 0; i < kMaxViewports; ++i) { + viewports[i * 4 + 2] = 1.0f; + viewports[i * 4 + 3] = 1.0f; + scissors[i * 4 + 2] = 1; + scissors[i * 4 + 3] = 1; + depths[i * 2 + 1] = 1.0; + } + + // first == MAX_VIEWPORTS, and first + count > MAX_VIEWPORTS. + MG_Impl::GLImpl::ViewportArrayv(kMaxViewports, 1, viewports.data()); + ExpectSingleGlError(GL_INVALID_VALUE); + MG_Impl::GLImpl::ViewportArrayv(1, kMaxViewports, viewports.data()); + ExpectSingleGlError(GL_INVALID_VALUE); + MG_Impl::GLImpl::ScissorArrayv(kMaxViewports, 1, scissors.data()); + ExpectSingleGlError(GL_INVALID_VALUE); + MG_Impl::GLImpl::ScissorArrayv(1, kMaxViewports, scissors.data()); + ExpectSingleGlError(GL_INVALID_VALUE); + MG_Impl::GLImpl::DepthRangeArrayv(kMaxViewports, 1, depths.data()); + ExpectSingleGlError(GL_INVALID_VALUE); + MG_Impl::GLImpl::DepthRangeArrayv(1, kMaxViewports, depths.data()); + ExpectSingleGlError(GL_INVALID_VALUE); + + // first + count == MAX_VIEWPORTS is LEGAL - the off-by-one an ">=" bound would get wrong, + // and one KHR-GL43.viewport_array.api_errors asserts explicitly. + MG_Impl::GLImpl::ViewportArrayv(1, kMaxViewports - 1, viewports.data()); + ExpectSingleGlError(GL_NO_ERROR); + MG_Impl::GLImpl::ScissorArrayv(1, kMaxViewports - 1, scissors.data()); + ExpectSingleGlError(GL_NO_ERROR); + MG_Impl::GLImpl::DepthRangeArrayv(1, kMaxViewports - 1, depths.data()); + ExpectSingleGlError(GL_NO_ERROR); + + // A negative count is GL_INVALID_VALUE and must not be read as a huge unsigned length. + MG_Impl::GLImpl::ViewportArrayv(0, -1, viewports.data()); + ExpectSingleGlError(GL_INVALID_VALUE); + MG_Impl::GLImpl::ScissorArrayv(0, -1, scissors.data()); + ExpectSingleGlError(GL_INVALID_VALUE); + MG_Impl::GLImpl::DepthRangeArrayv(0, -1, depths.data()); + ExpectSingleGlError(GL_INVALID_VALUE); +} + +TEST_F(RenderStateTest, NegativeExtentsAreRejectedWithoutDisturbingState) { + MG_Impl::GLImpl::Viewport(0, 0, 4, 4); + MG_Impl::GLImpl::Scissor(0, 0, 4, 4); + ExpectSingleGlError(GL_NO_ERROR); + + MG_Impl::GLImpl::Viewport(0, 0, -1, 1); + ExpectSingleGlError(GL_INVALID_VALUE); + MG_Impl::GLImpl::Viewport(0, 0, 1, -1); + ExpectSingleGlError(GL_INVALID_VALUE); + MG_Impl::GLImpl::Scissor(0, 0, -1, 1); + ExpectSingleGlError(GL_INVALID_VALUE); + MG_Impl::GLImpl::Scissor(0, 0, 1, -1); + ExpectSingleGlError(GL_INVALID_VALUE); + + for (GLuint index = 0; index < kMaxViewports; ++index) { + MG_Impl::GLImpl::ViewportIndexedf(index, 0.0f, 0.0f, -1.0f, 1.0f); + ExpectSingleGlError(GL_INVALID_VALUE); + MG_Impl::GLImpl::ViewportIndexedf(index, 0.0f, 0.0f, 1.0f, -1.0f); + ExpectSingleGlError(GL_INVALID_VALUE); + + const GLfloat badW[4] = {0.0f, 0.0f, -1.0f, 1.0f}; + MG_Impl::GLImpl::ViewportIndexedfv(index, badW); + ExpectSingleGlError(GL_INVALID_VALUE); + + MG_Impl::GLImpl::ScissorIndexed(index, 0, 0, -1, 1); + ExpectSingleGlError(GL_INVALID_VALUE); + const GLint badH[4] = {0, 0, 1, -1}; + MG_Impl::GLImpl::ScissorIndexedv(index, badH); + ExpectSingleGlError(GL_INVALID_VALUE); + + // The array form must reject the WHOLE call for one bad element, exactly once, and + // leave every rectangle alone - api_errors submits a full 16-element array with a + // single negative extent and then requires the error queue to hold one entry. + Array viewports{}; + Array scissors{}; + for (GLuint i = 0; i < kMaxViewports; ++i) { + viewports[i * 4 + 2] = 1.0f; + viewports[i * 4 + 3] = 1.0f; + scissors[i * 4 + 2] = 1; + scissors[i * 4 + 3] = 1; + } + viewports[index * 4 + 2] = -1.0f; + scissors[index * 4 + 3] = -1; + MG_Impl::GLImpl::ViewportArrayv(0, kMaxViewports, viewports.data()); + ExpectSingleGlError(GL_INVALID_VALUE); + MG_Impl::GLImpl::ScissorArrayv(0, kMaxViewports, scissors.data()); + ExpectSingleGlError(GL_INVALID_VALUE); + } + + // Nothing above may have landed. + GLint viewport[4] = {}; + MG_Impl::GLImpl::GetIntegeri_v(GL_VIEWPORT, 0, viewport); + EXPECT_EQ(viewport[2], 4); + EXPECT_EQ(viewport[3], 4); + GLint scissor[4] = {}; + MG_Impl::GLImpl::GetIntegeri_v(GL_SCISSOR_BOX, 0, scissor); + EXPECT_EQ(scissor[2], 4); + EXPECT_EQ(scissor[3], 4); + ExpectSingleGlError(GL_NO_ERROR); +} + +TEST_F(RenderStateTest, IndexedRectangleQueriesRejectAnOutOfRangeIndex) { + GLint ints[4] = {}; + GLfloat floats[4] = {}; + GLdouble doubles[4] = {}; + + MG_Impl::GLImpl::GetIntegeri_v(GL_SCISSOR_BOX, kMaxViewports, ints); + ExpectSingleGlError(GL_INVALID_VALUE); + MG_Impl::GLImpl::GetFloati_v(GL_VIEWPORT, kMaxViewports, floats); + ExpectSingleGlError(GL_INVALID_VALUE); + MG_Impl::GLImpl::GetDoublei_v(GL_DEPTH_RANGE, kMaxViewports, doubles); + ExpectSingleGlError(GL_INVALID_VALUE); + + MG_Impl::GLImpl::GetIntegeri_v(GL_SCISSOR_BOX, kMaxViewports - 1, ints); + ExpectSingleGlError(GL_NO_ERROR); + MG_Impl::GLImpl::GetFloati_v(GL_VIEWPORT, kMaxViewports - 1, floats); + ExpectSingleGlError(GL_NO_ERROR); + MG_Impl::GLImpl::GetDoublei_v(GL_DEPTH_RANGE, kMaxViewports - 1, doubles); + ExpectSingleGlError(GL_NO_ERROR); +}