[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

This commit is contained in:
2026-08-13 04:44:23 -04:00
parent 92d8f7269b
commit 5fbb17f6b9
11 changed files with 1091 additions and 111 deletions
+25 -1
View File
@@ -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<Uint>& framebuffers) {
m_framebufferState.GenerateNames(number, framebuffers);
+11 -5
View File
@@ -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
@@ -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<Float>(viewport.x()), static_cast<Float>(viewport.y()),
static_cast<Float>(viewport.z()), static_cast<Float>(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<Int>(std::lround(viewport.x())), static_cast<Int>(std::lround(viewport.y())),
static_cast<Int>(std::lround(viewport.z())), static_cast<Int>(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<int>(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
@@ -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<FloatVec4, MAX_VIEWPORTS> 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<FloatVec2, MAX_VIEWPORTS> 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<IntVec4, MAX_VIEWPORTS> 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 "<Name>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