[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
+22 -6
View File
@@ -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 "<Name>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() {
@@ -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<Uint32>(preTransform);
@@ -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)
+115 -16
View File
@@ -16,6 +16,7 @@
#include <MG_State/GLState/ErrorState/ErrorInfo.h>
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
#include <MG_Util/Converters/GLToMG/BufferEnumConverter.h>
#include <MG_Util/Converters/GLToMG/RenderStateEnumConverter.h>
#include <MG_Util/Converters/MGToGL/FramebufferEnumConverter.h>
#include <MG_Util/Converters/MGToGL/ErrorCodeConverter.h>
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
@@ -27,6 +28,11 @@
#include <MG_Backend/BackendObjects.h>
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<GLint>(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<GLuint>(std::max(maxViewports, 1))) return true;
if (index < RenderStateParameters::MAX_VIEWPORTS) return true;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("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<GLfloat>(box.x());
out[1] = static_cast<GLfloat>(box.y());
out[2] = static_cast<GLfloat>(box.z());
out[3] = static_cast<GLfloat>(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<Uint32>(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<GLint>(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<GLdouble>(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<GLint64>(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<GLint64>(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<GLint>(RenderStateParameters::MAX_VIEWPORTS);
break;
case GL_MINOR_VERSION:
*params = rendererInfo.RendererGLInfo.TargetGLVersion.Minor;
@@ -20,28 +20,118 @@ namespace MobileGL::MG_Impl::GLImpl {
return std::clamp(static_cast<Float>(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<GenericErrorInfo>("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<GenericErrorInfo>(
"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<GenericErrorInfo>("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<GenericErrorInfo>("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<GenericErrorInfo>("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<Uint64>(first) + static_cast<Uint64>(count);
if (last > RenderStateParameters::MAX_VIEWPORTS) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("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 <typename T>
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<GenericErrorInfo>("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 <typename T>
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<GenericErrorInfo>("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<GenericErrorInfo>("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<GenericErrorInfo>("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<GLuint>(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<GLuint>(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<GLuint>(i),
FloatVec2(ClampUnitFloat(static_cast<GLfloat>(v[i * 2 + 0])),
ClampUnitFloat(static_cast<GLfloat>(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<GLfloat>(n)), ClampUnitFloat(static_cast<GLfloat>(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<GenericErrorInfo>("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);
}
@@ -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);
+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
+477 -7
View File
@@ -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 <gtest/gtest.h>
@@ -21,6 +30,7 @@
#include <MG_Impl/GLImpl/RenderState/GL_RenderState.h>
#include <MG_State/GLState/Core.h>
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
#include <MG_State/GLState/RenderState/RenderState.h>
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<Array<GLfloat, 4>, kMaxViewports> ReadAllViewports() {
Array<Array<GLfloat, 4>, kMaxViewports> out{};
for (GLuint i = 0; i < kMaxViewports; ++i) {
MG_Impl::GLImpl::GetFloati_v(GL_VIEWPORT, i, out[i].data());
}
return out;
}
Array<Array<GLdouble, 2>, kMaxViewports> ReadAllDepthRanges() {
Array<Array<GLdouble, 2>, 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<GLint>(kMaxViewports));
EXPECT_GE(maxViewports, 16) << "GL 4.3 core requires MAX_VIEWPORTS >= 16";
}
TEST_F(RenderStateTest, ViewportArrayvRoundTripsThroughEveryGetterWidth) {
Array<GLfloat, kMaxViewports * 4> written{};
for (GLuint i = 0; i < kMaxViewports; ++i) {
written[i * 4 + 0] = static_cast<GLfloat>(i) + 0.125f;
written[i * 4 + 1] = static_cast<GLfloat>(i) + 0.25f;
written[i * 4 + 2] = static_cast<GLfloat>(64 + i);
written[i * 4 + 3] = static_cast<GLfloat>(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<GLdouble>(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<GLint>(64 + i)) << "index " << i;
EXPECT_EQ(asInt[3], static_cast<GLint>(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<GLint64>(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<GLint, kMaxViewports * 4> written{};
for (GLuint i = 0; i < kMaxViewports; ++i) {
written[i * 4 + 0] = static_cast<GLint>(i);
written[i * 4 + 1] = static_cast<GLint>(i * 2);
written[i * 4 + 2] = static_cast<GLint>(16 + i);
written[i * 4 + 3] = static_cast<GLint>(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<GLint>(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<GLdouble, kMaxViewports * 2> 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<GLdouble>(i) / 16.0;
written[i * 2 + 1] = 1.0 - static_cast<GLdouble>(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<GLfloat, kMaxViewports * 4> viewports{};
Array<GLint, kMaxViewports * 4> scissors{};
Array<GLdouble, kMaxViewports * 2> 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<GLfloat, kMaxViewports * 4> viewports{};
Array<GLint, kMaxViewports * 4> 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);
}