diff --git a/MobileGL/MG_Impl/GLImpl/Buffer/GL_Buffer.cpp b/MobileGL/MG_Impl/GLImpl/Buffer/GL_Buffer.cpp index beab9250..50dc0f5d 100644 --- a/MobileGL/MG_Impl/GLImpl/Buffer/GL_Buffer.cpp +++ b/MobileGL/MG_Impl/GLImpl/Buffer/GL_Buffer.cpp @@ -9,6 +9,7 @@ #include "GL_Buffer.h" #include "Validators.h" #include "../Texture/GL_Texture.h" +#include "../Getter/GL_Getter.h" #include #include #include @@ -861,6 +862,10 @@ namespace MobileGL::MG_Impl::GLImpl { Range1D mappedRange = bufferObject->GetMappedRange(); auto mappingAccess = bufferObject->GetMappingAccess(); + // GL 4.6 6.5: the error is on OVERLAP with the mapped range, i.e. a half-open + // intersection test. There used to be a second test below this one asking only + // `offset + size >= mappedRange.start`, which rejects every write that starts + // before a mapped tail as well - it made a legal disjoint glBufferSubData fail. if (bufferObject->IsMapped() && !(mappingAccess & BufferMappingAccessBit::Persistent) && (offset < mappedRange.end) && (offset + size > mappedRange.start)) { MG_State::pGLContext->RecordError( @@ -871,18 +876,6 @@ namespace MobileGL::MG_Impl::GLImpl { return; } - if (bufferObject->IsMapped() && !(mappingAccess & BufferMappingAccessBit::Persistent)) { - Range1D mappedRange = bufferObject->GetMappedRange(); - if (offset + size >= mappedRange.start) { - MG_State::pGLContext->RecordError( - ErrorCode::InvalidOperation, - MakeUnique("MG_Impl/GLImpl", "BufferSubData_State", - "Cannot modify a mapped buffer object unless it was " - "mapped with GL_MAP_PERSISTENT_BIT.")); - return; - } - } - bufferObject->UploadSubData({(void*)data, (SizeT)size}, offset); } @@ -1013,6 +1006,11 @@ namespace MobileGL::MG_Impl::GLImpl { } void BufferStorage_State(GLenum target, GLsizeiptr size, const void* data, GLbitfield flags) { + // Error precedence: "no buffer is bound to target" outranks a bad size or bad + // flags, so the binding has to be resolved before either is validated. + auto bufferObject = GetBoundBufferObject(target, BufferOp::BufferStorage); + if (!bufferObject) return; + if (size <= 0) { MG_State::pGLContext->RecordError( ErrorCode::InvalidValue, @@ -1021,8 +1019,6 @@ namespace MobileGL::MG_Impl::GLImpl { } if (!ValidateStorageFlags(flags, BufferOp::BufferStorage)) return; - auto bufferObject = GetBoundBufferObject(target, BufferOp::BufferStorage); - if (!bufferObject) return; if (bufferObject->IsImmutableStorage()) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, @@ -1057,6 +1053,10 @@ namespace MobileGL::MG_Impl::GLImpl { } void NamedBufferStorage_State(GLuint buffer, GLsizeiptr size, const void* data, GLbitfield flags) { + // Same precedence as BufferStorage_State: the buffer-name error comes first. + auto bufferObject = GetNamedBufferObject(buffer, BufferOp::NamedBufferStorage); + if (!bufferObject) return; + if (size <= 0) { MG_State::pGLContext->RecordError( ErrorCode::InvalidValue, @@ -1065,8 +1065,6 @@ namespace MobileGL::MG_Impl::GLImpl { } if (!ValidateStorageFlags(flags, BufferOp::NamedBufferStorage)) return; - auto bufferObject = GetNamedBufferObject(buffer, BufferOp::NamedBufferStorage); - if (!bufferObject) return; if (bufferObject->IsImmutableStorage()) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, @@ -1486,12 +1484,71 @@ namespace MobileGL::MG_Impl::GLImpl { GetBufferBindingSlot(bufferTarget).Bind(bufferObject); } + // GL 4.6 core 6.1.1: the constraints glBindBufferRange puts on the (offset, size) pair. + // Every one of them is INVALID_VALUE, and all of them are checked before a single piece + // of state is written - a rejected bind must leave the binding point exactly as it was. + // They apply only to a non-zero buffer: buffer 0 detaches the binding point and ignores + // offset and size, which is also how glBindBuffersRange spells "reset this element" + // (a NULL buffers array, or a zero entry inside one). + static Bool ValidateBufferRangeOffsetAndSize(GLenum target, GLintptr offset, GLsizeiptr size, + const char* funcName) { + if (size <= 0) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique("MG_Impl/GLImpl", funcName, + std::format("size ({}) must be greater than zero.", size))); + return false; + } + if (offset < 0) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique("MG_Impl/GLImpl", funcName, + std::format("offset ({}) must not be negative.", offset))); + return false; + } + // GL_UNIFORM_BUFFER and GL_SHADER_STORAGE_BUFFER each constrain the offset to their own + // implementation-defined alignment, which glGetIntegerv already answers. + GLenum alignmentQuery = GL_NONE; + if (target == GL_SHADER_STORAGE_BUFFER) { + alignmentQuery = GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT; + } else if (target == GL_UNIFORM_BUFFER) { + alignmentQuery = GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT; + } + if (alignmentQuery != GL_NONE) { + GLint alignment = 0; + GetIntegerv(alignmentQuery, &alignment); + if (alignment > 0 && (offset % static_cast(alignment)) != 0) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique( + "MG_Impl/GLImpl", funcName, + std::format("offset ({}) must be a multiple of {} ({}).", offset, + MG_Util::ConvertGLEnumToString(alignmentQuery), alignment))); + return false; + } + } + // A transform feedback capture binding is addressed in 32-bit components, so BOTH the + // offset and the size must be multiples of 4. + if (target == GL_TRANSFORM_FEEDBACK_BUFFER && ((offset % 4) != 0 || (size % 4) != 0)) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique( + "MG_Impl/GLImpl", funcName, + std::format("offset ({}) and size ({}) must both be multiples of 4 for " + "GL_TRANSFORM_FEEDBACK_BUFFER.", + offset, size))); + return false; + } + return true; + } + void BindBufferRange_State(GLenum target, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size) { MGLOG_D("%s: target = %s, index = %u, buffer = %u, offset = %d, size = %d", __func__, MG_Util::ConvertGLEnumToString(target).c_str(), index, buffer, offset, size); BufferTarget bufferTarget = MG_Util::ConvertGLEnumToBufferTarget(target); if (!BufferImpl::ValidateBufferBindingPointTarget(bufferTarget)) return; if (!BufferImpl::ValidateBufferBindingPointIndex(bufferTarget, index)) return; + if (buffer != 0 && !ValidateBufferRangeOffsetAndSize(target, offset, size, __func__)) return; if (bufferTarget == BufferTarget::TransformFeedback && MG_State::pGLContext->IsTransformFeedbackActive()) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, @@ -1665,15 +1722,32 @@ namespace MobileGL::MG_Impl::GLImpl { } // ARB_multi_bind: defined by the spec as equivalent to a loop over the single-bind entry - // points (with buffer 0 resetting the binding point). + // points (with buffer 0 resetting the binding point) - but only AFTER an up-front check + // of the whole [first, first + count) range. Looping straight into the single-bind entry + // points reports the single-bind INVALID_VALUE for an out-of-range index instead of the + // multi-bind INVALID_OPERATION, and binds the in-range prefix before failing. + static Bool ValidateMultiBindBufferRange(GLenum target, GLuint first, GLsizei count, const char* funcName) { + BufferTarget bufferTarget = MG_Util::ConvertGLEnumToBufferTarget(target); + if (!BufferImpl::ValidateBufferBindingPointTarget(bufferTarget)) return false; + return BufferImpl::ValidateBufferBindingPointRange(bufferTarget, first, count, funcName); + } + void BindBuffersBase(GLenum target, GLuint first, GLsizei count, const GLuint* buffers) { + if (!ValidateMultiBindBufferRange(target, first, count, __func__)) return; for (GLsizei i = 0; i < count; ++i) { BindBufferBase_State(target, first + i, buffers ? buffers[i] : 0); } } + // The (offset, size) constraints are the one part of glBindBuffersRange that stays + // per-element: ARB_multi_bind checks them separately for each binding point, leaves that + // point unchanged on failure, and still applies the remaining elements - which is exactly + // what looping into BindBufferRange_State does. Only the [first, first + count) range is + // an up-front, all-or-nothing check. Elements that name buffer 0 (or a NULL buffers array) + // reset the binding point through BindBufferBase_State and carry no offset/size to check. void BindBuffersRange(GLenum target, GLuint first, GLsizei count, const GLuint* buffers, const GLintptr* offsets, const GLsizeiptr* sizes) { + if (!ValidateMultiBindBufferRange(target, first, count, __func__)) return; for (GLsizei i = 0; i < count; ++i) { if (!buffers || buffers[i] == 0) { BindBufferBase_State(target, first + i, 0); diff --git a/MobileGL/MG_Impl/GLImpl/Buffer/Validators.cpp b/MobileGL/MG_Impl/GLImpl/Buffer/Validators.cpp index 0282d559..50df5b1e 100644 --- a/MobileGL/MG_Impl/GLImpl/Buffer/Validators.cpp +++ b/MobileGL/MG_Impl/GLImpl/Buffer/Validators.cpp @@ -53,18 +53,46 @@ namespace MobileGL::MG_Impl::GLImpl::BufferImpl { return true; } + namespace { + // The GL-visible number of indexed binding points for `target`. + SizeT GetBufferBindingPointLimit(BufferTarget target) { + SizeT pointCount = MG_State::pGLContext->GetBufferBindingPointCount(target); + if (target == BufferTarget::ShaderStorage && MG_Backend::pActiveBackendObject) { + const Int backendCount = + MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxShaderStorageBufferBindings; + pointCount = std::min(pointCount, static_cast(std::max(backendCount, 0))); + } + if (target == BufferTarget::TransformFeedback) { + // GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS bounds the indexed capture + // binding points in GL 3.3 (no ARB_transform_feedback3). + pointCount = std::min(pointCount, 4); + } + return pointCount; + } + } // namespace + + Bool ValidateBufferBindingPointRange(BufferTarget target, Uint first, GLsizei count, const char* funcName) { + if (count < 0) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, MakeUnique("MG_Impl/GLImpl/BufferImpl", funcName, + "count must be non-negative.")); + return false; + } + const SizeT pointCount = GetBufferBindingPointLimit(target); + if (static_cast(first) + static_cast(count) > static_cast(pointCount)) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique( + "MG_Impl/GLImpl/BufferImpl", funcName, + std::format("first + count ({} + {}) exceeds the {} indexed binding points of target {}.", first, + count, pointCount, MG_Util::ConvertBufferTargetToString(target)))); + return false; + } + return true; + } + Bool ValidateBufferBindingPointIndex(BufferTarget target, Uint index) { - SizeT pointCount = MG_State::pGLContext->GetBufferBindingPointCount(target); - if (target == BufferTarget::ShaderStorage && MG_Backend::pActiveBackendObject) { - const Int backendCount = - MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxShaderStorageBufferBindings; - pointCount = std::min(pointCount, static_cast(std::max(backendCount, 0))); - } - if (target == BufferTarget::TransformFeedback) { - // GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS bounds the indexed capture - // binding points in GL 3.3 (no ARB_transform_feedback3). - pointCount = std::min(pointCount, 4); - } + const SizeT pointCount = GetBufferBindingPointLimit(target); if (index < pointCount) { return true; diff --git a/MobileGL/MG_Impl/GLImpl/Buffer/Validators.h b/MobileGL/MG_Impl/GLImpl/Buffer/Validators.h index 65ebe5ce..1d33a77a 100644 --- a/MobileGL/MG_Impl/GLImpl/Buffer/Validators.h +++ b/MobileGL/MG_Impl/GLImpl/Buffer/Validators.h @@ -17,4 +17,8 @@ namespace MobileGL::MG_Impl::GLImpl::BufferImpl { Bool ValidateBufferMappingAccess(Flags accessBits); Bool ValidateBufferBindingPointTarget(BufferTarget target); Bool ValidateBufferBindingPointIndex(BufferTarget target, Uint index); + // ARB_multi_bind: glBindBuffersBase/Range validate the whole [first, first + count) range + // up front and report INVALID_OPERATION, where a single out-of-range index would be + // INVALID_VALUE. Naively looping the single-bind entry points reports the wrong class. + Bool ValidateBufferBindingPointRange(BufferTarget target, Uint first, GLsizei count, const char* funcName); } // namespace MobileGL::MG_Impl::GLImpl::BufferImpl diff --git a/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp b/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp index 39ab46f9..51e0aa49 100644 --- a/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp +++ b/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp @@ -474,6 +474,21 @@ namespace MobileGL::MG_Impl::GLImpl { return; } if (!ValidateCurrentProgramForCompute(__func__)) return; + // GL 4.6 core 19: each num_groups_* must be within GL_MAX_COMPUTE_WORK_GROUP_COUNT + // for its dimension. GetIntegeri_v already floors that at the spec minimum. + const GLuint numGroups[3] = {numGroupsX, numGroupsY, numGroupsZ}; + for (GLuint dimension = 0; dimension < 3; ++dimension) { + GLint maxGroups = 0; + GetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_COUNT, dimension, &maxGroups); + if (numGroups[dimension] > static_cast(std::max(maxGroups, 0))) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique("MG_Impl/GLImpl", __func__, + "num_groups exceeds GL_MAX_COMPUTE_WORK_GROUP_COUNT for dimension " + + std::to_string(dimension) + ".")); + return; + } + } dispatchCompute(numGroupsX, numGroupsY, numGroupsZ); } @@ -487,6 +502,24 @@ namespace MobileGL::MG_Impl::GLImpl { return; } if (!ValidateCurrentProgramForCompute(__func__)) return; + // GL 4.6 core 19: `indirect` is a byte offset into GL_DISPATCH_INDIRECT_BUFFER - + // negative or misaligned is INVALID_VALUE, nothing bound is INVALID_OPERATION. + if (indirect < 0 || (indirect % 4) != 0) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique("MG_Impl/GLImpl", __func__, + "indirect must be non-negative and a multiple of 4.")); + return; + } + const auto& indirectBuffer = + MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DispatchIndirect).GetBoundObject(); + if (!indirectBuffer) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique("MG_Impl/GLImpl", __func__, + "No buffer is bound to GL_DISPATCH_INDIRECT_BUFFER.")); + return; + } dispatchComputeIndirect(indirect); } diff --git a/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp b/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp index 8c56b65d..020b186f 100644 --- a/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp +++ b/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp @@ -977,8 +977,8 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, ScissorIndexed, GLuint index, GLint left, GL 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_STUB_HEAD(void, GetFloati_v, GLenum target, GLuint index, GLfloat* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetFloati_v, target, index, data) -DECLARE_GL_FUNCTION_STUB_HEAD(void, GetDoublei_v, GLenum target, GLuint index, GLdouble* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetDoublei_v, target, index, data) +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) DECLARE_GL_FUNCTION_HEAD(void, DrawElementsInstancedBaseInstance, GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawElementsInstancedBaseInstance, mode, count, type, indices, instancecount, baseinstance) DECLARE_GL_FUNCTION_HEAD(void, DrawElementsInstancedBaseVertexBaseInstance, GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLint basevertex, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawElementsInstancedBaseVertexBaseInstance, mode, count, type, indices, instancecount, basevertex, baseinstance) diff --git a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp index 61d259d7..a5694457 100644 --- a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp +++ b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp @@ -51,6 +51,14 @@ namespace MobileGL::MG_Impl::GLImpl { constexpr GLint kFrontendMaxTessControlAtomicCounters = 0; constexpr GLint kFrontendMaxTessEvaluationAtomicCounters = 0; constexpr GLint kFrontendMaxVertexAtomicCounters = 0; + // One atomic counter is a uint, and a buffer never has to hold more counters than the + // combined limit the frontend advertises. GL 4.6 table 23.63 floors this at 32 bytes. + constexpr GLint kFrontendMaxAtomicCounterBufferSize = + kFrontendMaxCombinedAtomicCounters * static_cast(sizeof(GLuint)); + // KHR_debug minima (GL 4.6 table 23.66); the debug entry points are stubs, but the + // limits they advertise still have to be legal. + constexpr GLint kFrontendMaxDebugGroupStackDepth = 64; + constexpr GLint kFrontendMaxDebugLoggedMessages = 1; constexpr GLint kFrontendMaxVertexUniformComponents = 4096; constexpr GLint kFrontendMaxVertexUniformVectors = 128; constexpr GLint kFrontendMaxVertexUniformBlocks = 14; @@ -264,6 +272,60 @@ namespace MobileGL::MG_Impl::GLImpl { return true; } + // GL_TEXTURE_BINDING_* is per-texture-unit state: glGetIntegerv answers for the + // active unit, glGetIntegeri_v answers for unit `index`. Both need the same + // pname -> target decode, so it lives here instead of being spelled out twice. + bool TryDecodeTextureUnitBindingPname(GLenum pname, TextureTarget& outTarget) { + switch (pname) { + case GL_TEXTURE_BINDING_1D: outTarget = TextureTarget::Texture1D; return true; + case GL_TEXTURE_BINDING_1D_ARRAY: outTarget = TextureTarget::Texture1DArray; return true; + case GL_TEXTURE_BINDING_2D: outTarget = TextureTarget::Texture2D; return true; + case GL_TEXTURE_BINDING_2D_ARRAY: outTarget = TextureTarget::Texture2DArray; return true; + case GL_TEXTURE_BINDING_2D_MULTISAMPLE: outTarget = TextureTarget::Texture2DMultisample; return true; + case GL_TEXTURE_BINDING_2D_MULTISAMPLE_ARRAY: + outTarget = TextureTarget::Texture2DMultisampleArray; + return true; + case GL_TEXTURE_BINDING_3D: outTarget = TextureTarget::Texture3D; return true; + case GL_TEXTURE_BINDING_BUFFER: outTarget = TextureTarget::TextureBuffer; return true; + case GL_TEXTURE_BINDING_CUBE_MAP: outTarget = TextureTarget::TextureCubeMap; return true; + case GL_TEXTURE_BINDING_CUBE_MAP_ARRAY: outTarget = TextureTarget::TextureCubeMapArray; return true; + case GL_TEXTURE_BINDING_RECTANGLE: outTarget = TextureTarget::TextureRectangle; return true; + default: return false; + } + } + + GLint QueryTextureBindingOnUnit(Int unit, TextureTarget target) { + auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit); + const auto& obj = textureUnit.GetBindingSlot(target).GetBoundObject(); + return obj ? static_cast(obj->GetExternalIndex()) : 0; + } + + GLint QuerySamplerBindingOnUnit(Int unit) { + const auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit); + const auto& sampler = textureUnit.GetSamplerObject(); + 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. + 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. + 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; + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique("MG_Impl/GLImpl", caller, "Viewport index is out of range.")); + return false; + } + void CopyIntsToBooleans(const GLint* src, SizeT count, GLboolean* dst) { for (SizeT i = 0; i < count; ++i) { dst[i] = src[i] ? GL_TRUE : GL_FALSE; @@ -671,7 +733,37 @@ namespace MobileGL::MG_Impl::GLImpl { } } + // Per-texture-unit bindings: GL 4.6 core table 23.19 makes every GL_TEXTURE_BINDING_* + // and GL_SAMPLER_BINDING indexed by texture unit. Without this they fell through to + // the raw backend passthrough at the bottom, which knows nothing about the + // frontend's binding state. + if (TextureTarget textureBindingTarget = TextureTarget::Unknown; + TryDecodeTextureUnitBindingPname(target, textureBindingTarget) || target == GL_SAMPLER_BINDING) { + GLint maxUnits = 0; + GetIntegerv(GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, &maxUnits); + maxUnits = std::min(maxUnits, MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS); + if (index >= static_cast(std::max(maxUnits, 0))) { + *data = 0; + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique("MG_Impl/GLImpl", __func__, "Texture unit index is out of range.")); + return; + } + *data = target == GL_SAMPLER_BINDING + ? QuerySamplerBindingOnUnit(static_cast(index)) + : QueryTextureBindingOnUnit(static_cast(index), textureBindingTarget); + 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. + case GL_VIEWPORT: + case GL_SCISSOR_BOX: + if (!ValidateViewportQueryIndex(index, __func__)) return; + GetIntegerv(target, data); + 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: @@ -783,6 +875,46 @@ namespace MobileGL::MG_Impl::GLImpl { getIntegeri(target, index, data); } + // GL_ARB_viewport_array's typed indexed getters. They were no-op stubs, which left the + // caller's output buffer holding whatever was on the stack. The multi-component indexed + // rectangles are answered from the frontend's own viewport/scissor/depth-range state, via + // the non-indexed getter of the matching type - GL_DEPTH_RANGE is float state, so putting + // it through the integer query would round it to 0/1. Everything else MobileGL answers + // indexed is scalar integer-domain state, where converting the integer query is exact. + void GetFloati_v(GLenum target, GLuint index, GLfloat* data) { + if (!data) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique("MG_Impl/GLImpl", __func__, "data pointer cannot be null")); + return; + } + if (IsIndexedViewportQuery(target)) { + if (!ValidateViewportQueryIndex(index, __func__)) return; + GetFloatv(target, data); + return; + } + GLint ints[4] = {}; + GetIntegeri_v(target, index, ints); + data[0] = static_cast(ints[0]); + } + + void GetDoublei_v(GLenum target, GLuint index, GLdouble* data) { + if (!data) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique("MG_Impl/GLImpl", __func__, "data pointer cannot be null")); + return; + } + if (IsIndexedViewportQuery(target)) { + if (!ValidateViewportQueryIndex(index, __func__)) return; + GetDoublev(target, data); + return; + } + GLint ints[4] = {}; + GetIntegeri_v(target, index, ints); + data[0] = static_cast(ints[0]); + } + void GetInteger64i_v(GLenum target, GLuint index, GLint64* data) { if (!data) { MG_State::pGLContext->RecordError( @@ -959,6 +1091,13 @@ namespace MobileGL::MG_Impl::GLImpl { return; } + // Per-texture-unit bindings: the non-indexed query reports the active unit. + if (TextureTarget textureBindingTarget = TextureTarget::Unknown; + TryDecodeTextureUnitBindingPname(pname, textureBindingTarget)) { + *params = QueryTextureBindingOnUnit(MG_State::pGLContext->GetActiveTextureUnit(), textureBindingTarget); + return; + } + switch (pname) { case GL_ACTIVE_TEXTURE: *params = MG_State::pGLContext->GetActiveTextureUnit() + GL_TEXTURE0; @@ -1085,11 +1224,17 @@ namespace MobileGL::MG_Impl::GLImpl { : 0; return; case GL_MAX_DEBUG_GROUP_STACK_DEPTH: - *params = 0; // debug-group entrypoints are stubbed + // KHR_debug floors this at 64 even when the group entry points are stubs: the + // limit describes how deep glPushDebugGroup may nest, and 0 is not a legal answer. + *params = kFrontendMaxDebugGroupStackDepth; return; case GL_MAX_DEBUG_MESSAGE_LENGTH: *params = 1024; // debug-message entrypoints are stubbed, but KHR_debug requires a valid limit return; + case GL_MAX_DEBUG_LOGGED_MESSAGES: + // Size of the message log ring; KHR_debug requires at least 1. + *params = kFrontendMaxDebugLoggedMessages; + return; case GL_DEBUG_GROUP_STACK_DEPTH: *params = 0; // debug-group entrypoints are stubbed return; @@ -1531,13 +1676,9 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_SAMPLE_MASK_VALUE: *params = static_cast(MG_State::pGLContext->GetSampleMaskValue()); return; - case GL_SAMPLER_BINDING: { - Int unit = MG_State::pGLContext->GetActiveTextureUnit(); - const auto& tu = MG_State::pGLContext->GetTextureUnitObject(unit); - const auto& sampler = tu.GetSamplerObject(); - *params = sampler ? static_cast(sampler->GetExternalIndex()) : 0; + case GL_SAMPLER_BINDING: + *params = QuerySamplerBindingOnUnit(MG_State::pGLContext->GetActiveTextureUnit()); return; - } case GL_SAMPLES: *params = ResolveDrawFramebufferSampleCount(); return; @@ -1633,87 +1774,6 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_STEREO: *params = 0; // stereo surfaces are not exposed return; - case GL_TEXTURE_BINDING_1D: { - Int unit = MG_State::pGLContext->GetActiveTextureUnit(); - auto& tu = MG_State::pGLContext->GetTextureUnitObject(unit); - const auto& slot = tu.GetBindingSlot(TextureTarget::Texture1D); - const auto& obj = slot.GetBoundObject(); - *params = obj ? static_cast(obj->GetExternalIndex()) : 0; - return; - } - case GL_TEXTURE_BINDING_1D_ARRAY: { - Int unit = MG_State::pGLContext->GetActiveTextureUnit(); - auto& tu = MG_State::pGLContext->GetTextureUnitObject(unit); - const auto& slot = tu.GetBindingSlot(TextureTarget::Texture1DArray); - const auto& obj = slot.GetBoundObject(); - *params = obj ? static_cast(obj->GetExternalIndex()) : 0; - return; - } - case GL_TEXTURE_BINDING_2D: { - Int unit = MG_State::pGLContext->GetActiveTextureUnit(); - auto& tu = MG_State::pGLContext->GetTextureUnitObject(unit); - const auto& slot = tu.GetBindingSlot(TextureTarget::Texture2D); - const auto& obj = slot.GetBoundObject(); - *params = obj ? static_cast(obj->GetExternalIndex()) : 0; - MGLOG_D("Get GL_TEXTURE_BINDING_2D: %d", *params); - return; - } - case GL_TEXTURE_BINDING_2D_ARRAY: { - Int unit = MG_State::pGLContext->GetActiveTextureUnit(); - auto& tu = MG_State::pGLContext->GetTextureUnitObject(unit); - const auto& slot = tu.GetBindingSlot(TextureTarget::Texture2DArray); - const auto& obj = slot.GetBoundObject(); - *params = obj ? static_cast(obj->GetExternalIndex()) : 0; - return; - } - case GL_TEXTURE_BINDING_2D_MULTISAMPLE: { - Int unit = MG_State::pGLContext->GetActiveTextureUnit(); - auto& tu = MG_State::pGLContext->GetTextureUnitObject(unit); - const auto& slot = tu.GetBindingSlot(TextureTarget::Texture2DMultisample); - const auto& obj = slot.GetBoundObject(); - *params = obj ? static_cast(obj->GetExternalIndex()) : 0; - return; - } - case GL_TEXTURE_BINDING_2D_MULTISAMPLE_ARRAY: { - Int unit = MG_State::pGLContext->GetActiveTextureUnit(); - auto& tu = MG_State::pGLContext->GetTextureUnitObject(unit); - const auto& slot = tu.GetBindingSlot(TextureTarget::Texture2DMultisampleArray); - const auto& obj = slot.GetBoundObject(); - *params = obj ? static_cast(obj->GetExternalIndex()) : 0; - return; - } - case GL_TEXTURE_BINDING_3D: { - Int unit = MG_State::pGLContext->GetActiveTextureUnit(); - auto& tu = MG_State::pGLContext->GetTextureUnitObject(unit); - const auto& slot = tu.GetBindingSlot(TextureTarget::Texture3D); - const auto& obj = slot.GetBoundObject(); - *params = obj ? static_cast(obj->GetExternalIndex()) : 0; - return; - } - case GL_TEXTURE_BINDING_BUFFER: { - Int unit = MG_State::pGLContext->GetActiveTextureUnit(); - auto& tu = MG_State::pGLContext->GetTextureUnitObject(unit); - const auto& slot = tu.GetBindingSlot(TextureTarget::TextureBuffer); - const auto& obj = slot.GetBoundObject(); - *params = obj ? static_cast(obj->GetExternalIndex()) : 0; - return; - } - case GL_TEXTURE_BINDING_CUBE_MAP: { - Int unit = MG_State::pGLContext->GetActiveTextureUnit(); - auto& tu = MG_State::pGLContext->GetTextureUnitObject(unit); - const auto& slot = tu.GetBindingSlot(TextureTarget::TextureCubeMap); - const auto& obj = slot.GetBoundObject(); - *params = obj ? static_cast(obj->GetExternalIndex()) : 0; - return; - } - case GL_TEXTURE_BINDING_RECTANGLE: { - Int unit = MG_State::pGLContext->GetActiveTextureUnit(); - auto& tu = MG_State::pGLContext->GetTextureUnitObject(unit); - const auto& slot = tu.GetBindingSlot(TextureTarget::TextureRectangle); - const auto& obj = slot.GetBoundObject(); - *params = obj ? static_cast(obj->GetExternalIndex()) : 0; - return; - } case GL_TEXTURE_COMPRESSION_HINT: *params = static_cast(MG_State::pGLContext->GetHint(pname)); return; @@ -1986,6 +2046,18 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS: *params = static_cast(GetIndexedBufferQueryPointCount(BufferTarget::ShaderStorage)); break; + case GL_MAX_SHADER_STORAGE_BLOCK_SIZE: + // 64-bit state (see GetInteger64v); the 32-bit query saturates, per the GL + // state-query conversion rules. + *params = static_cast(std::min(dynamicParameters.MaxShaderStorageBlockSize, + static_cast(INT32_MAX))); + break; + case GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS: + *params = static_cast(GetIndexedBufferQueryPointCount(BufferTarget::AtomicCounter)); + break; + case GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE: + *params = kFrontendMaxAtomicCounterBufferSize; + break; case GL_MAX_TEXTURE_BUFFER_SIZE: *params = dynamicParameters.MaxTextureBufferSize; break; diff --git a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.h b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.h index fa8dc1d7..29df6e87 100644 --- a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.h +++ b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.h @@ -19,6 +19,8 @@ namespace MobileGL::MG_Impl::GLImpl { void GetIntegerv(GLenum pname, GLint* params); void GetInteger64v(GLenum pname, GLint64* params); void GetIntegeri_v(GLenum target, GLuint index, GLint* data); + void GetFloati_v(GLenum target, GLuint index, GLfloat* data); + void GetDoublei_v(GLenum target, GLuint index, GLdouble* data); void GetInteger64i_v(GLenum target, GLuint index, GLint64* data); GLenum GetError(); GLenum GetGraphicsResetStatus(); diff --git a/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp b/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp index f2f989c6..121da8da 100644 --- a/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp +++ b/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp @@ -409,14 +409,19 @@ namespace MobileGL::MG_Impl::GLImpl { } GLuint CreateShader_State(GLenum type) { - auto shaderId = MG_State::pGLContext->CreateShader(MG_Util::ConvertGLEnumToShaderStage(type)); - if (shaderId == 0) { + // GL 4.6 core 7.1: shaderType is an enum, so an unrecognised one is INVALID_ENUM (it + // used to be documented as INVALID_VALUE). The check has to happen HERE: the state + // layer hands out a name for ShaderStage::Unknown just as happily as for a real + // stage, so the old "shaderId == 0 means bad type" test could never fire and an + // unknown shaderType silently produced a usable shader name and no error at all. + const ShaderStage stage = MG_Util::ConvertGLEnumToShaderStage(type); + if (stage == ShaderStage::Unknown) { MG_State::pGLContext->RecordError( - ErrorCode::InvalidValue, + ErrorCode::InvalidEnum, MakeUnique("MG_Impl/GLImpl", __func__, "`shaderType` is not an accepted value.")); return 0; } - return shaderId; + return MG_State::pGLContext->CreateShader(stage); } void DeleteProgram_State(GLuint program) { @@ -855,6 +860,31 @@ namespace MobileGL::MG_Impl::GLImpl { return loc; } + // A float matrix lives in the global UBO under std140 rules - one 16-byte-aligned column + // vector per column - while the value glGetUniform* must return is tightly packed + // columns * rows floats. Only mat4 is the same either way; every other shape needs the + // padding undone, and the readback has to undo exactly what UniformMatrixfv_Object put + // there. Returns false when `ttype` is not a float matrix (nothing to unpack). + Bool TryGatherFloatMatrixColumns(const glslang::TType* ttype, const char* pBase, void* params) { + if (ttype == nullptr || !ttype->isMatrix() || ttype->getBasicType() == glslang::EbtDouble) return false; + const Int columns = ttype->getMatrixCols(); + const Int rows = ttype->getMatrixRows(); + for (Int column = 0; column < columns; ++column) { + Memcpy(static_cast(params) + static_cast(column) * rows * sizeof(GLfloat), + pBase + static_cast(column) * 4 * sizeof(GLfloat), rows * sizeof(GLfloat)); + } + return true; + } + + // Bytes a uniform actually occupies in the global UBO. It is the tight GL type size for + // everything except a float matrix, whose padded columns make it wider. + SizeT UniformStorageSpanInBytes(const glslang::TType* ttype, SizeT tightSize) { + if (ttype != nullptr && ttype->isMatrix() && ttype->getBasicType() != glslang::EbtDouble) { + return static_cast(ttype->getMatrixCols()) * 4 * sizeof(GLfloat); + } + return tightSize; + } + void GetUniform_State(GLuint program, GLint location, void* params) { auto& programObject = TryToGetProgramObject(program); if (!programObject) return; @@ -885,23 +915,16 @@ namespace MobileGL::MG_Impl::GLImpl { auto size = programObject->GetUniformSizesInBytes(location); char* pUBO = (char*)programObject->MapUBO(); auto* ttype = programObject->GetUniformTType(location); + const SizeT span = UniformStorageSpanInBytes(ttype, size); if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset || - offset + size > programObject->GetUBOSize()) { + offset + span > programObject->GetUBOSize()) { MGLOG_E("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__, program, location); return; } - if (!ttype->isMatrix() || ttype->getMatrixCols() != 3) + if (!TryGatherFloatMatrixColumns(ttype, pUBO + offset, params)) { Memcpy(params, pUBO + offset, size); - else { - // TODO: we only deal with mat3 yet, deal with other types later - // assuming float here, which may not be the case - auto* pBase = pUBO + offset; - for (int i = 0; i < ttype->getMatrixRows(); i++) { - Memcpy((char*)params + ttype->getMatrixCols() * sizeof(float) * i, pBase + 4 * sizeof(float) * i, - ttype->getMatrixCols() * sizeof(float)); - } } } // TODO: handle 1i variant as texture unit @@ -940,23 +963,16 @@ namespace MobileGL::MG_Impl::GLImpl { auto size = programObject->GetUniformSizesInBytes(location); char* pUBO = static_cast(programObject->MapUBO()); auto* ttype = programObject->GetUniformTType(location); + const SizeT span = UniformStorageSpanInBytes(ttype, size); if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset || - offset + size > programObject->GetUBOSize()) { + offset + span > programObject->GetUBOSize()) { MGLOG_E("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__, program, location); return; } if constexpr (std::is_same_v) { - if (ttype->getBasicType() != glslang::EbtDouble && ttype->isMatrix() && - ttype->getMatrixCols() == 3) { - auto* pBase = pUBO + offset; - for (int i = 0; i < ttype->getMatrixRows(); i++) { - Memcpy(reinterpret_cast(params) + ttype->getMatrixCols() * sizeof(GLfloat) * i, - pBase + 4 * sizeof(GLfloat) * i, ttype->getMatrixCols() * sizeof(GLfloat)); - } - return; - } + if (TryGatherFloatMatrixColumns(ttype, pUBO + offset, params)) return; } // A double-precision uniform is the one case where the stored component type can @@ -1257,6 +1273,52 @@ namespace MobileGL::MG_Impl::GLImpl { } } + // glUniformMatrix*fv / glProgramUniformMatrix*fv, every shape (square and non-square). + // A float matrix sits in the global UBO under std140 rules: each of its `columns` + // column vectors starts on its own 16-byte boundary no matter how many rows it has, so + // the only shape that may be written as one contiguous block is mat4. Writing a matNxM + // as N*M packed floats puts every column after the first at the wrong byte offset. + template + void UniformMatrixfv_Object(Program& programObject, const char* caller, GLint location, GLsizei count, + GLboolean transpose, const GLfloat* value, Int columns, Int rows, + const String& ownerDescription) { + // std140: a column vector of a float matrix is padded out to a vec4. + constexpr SizeT kColumnStride = 4 * sizeof(GLfloat); + const SizeT componentCount = static_cast(columns) * static_cast(rows); + GLfloat column[4] = {}; + for (GLint matrix = 0; matrix < count; ++matrix) { + if (matrix > 0 && !programObject.UniformLocationsAliasSameUniform(location, location + matrix)) { + // GL 3.3 2.11.4: values for elements beyond the end of the uniform array + // are ignored. Never step onto a neighboring uniform's location. + break; + } + if (!programObject.IsValidUniformLocation(location + matrix)) { + RecordInvalidUniformLocationError(caller, location + matrix, ownerDescription); + return; + } + if (programObject.IsUniformOpaqueAtLocation(location + matrix)) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique("MG_Impl/GLImpl", caller, + "Opaque uniforms cannot be set with matrix Uniform calls.")); + return; + } + if (value == nullptr) return; + const GLfloat* source = value + static_cast(matrix) * componentCount; + for (Int c = 0; c < columns; ++c) { + for (Int r = 0; r < rows; ++r) { + column[r] = transpose == GL_TRUE ? source[r * columns + c] : source[c * rows + r]; + } + const SizeT byteOffset = static_cast(c) * kColumnStride; + switch (rows) { + case 2: Uniform_State<2>(programObject, location + matrix, column, byteOffset); break; + case 3: Uniform_State<3>(programObject, location + matrix, column, byteOffset); break; + default: Uniform_State<4>(programObject, location + matrix, column, byteOffset); break; + } + } + } + } + // Helper function to transpose a 2x2 matrix void TransposeMatrix2x2(const GLfloat* input, GLfloat* output) { // Input matrix is in column-major order (OpenGL default) @@ -1362,8 +1424,8 @@ namespace MobileGL::MG_Impl::GLImpl { } void UniformMatrix2fv_State(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) { - // For 2x2 matrices, we have 4 elements per matrix - // If transpose is GL_TRUE, we need to transpose the matrix data + // A mat2 is NOT four contiguous floats in the global UBO: std140 pads each column + // vector out to 16 bytes, so column 1 starts at byte 16, not byte 8. if (location == -1) return; auto& programObject = MG_State::pGLContext->GetProgramForUniform(); @@ -1374,26 +1436,8 @@ namespace MobileGL::MG_Impl::GLImpl { return; } - // For matrix uniforms, we handle each matrix individually - for (GLint i = 0; i < count; i++) { - if (i > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + i)) { - // Values for elements beyond the end of the uniform array are ignored. - break; - } - if (!programObject->IsValidUniformLocation(location + i)) { - RecordInvalidUniformLocationError(__func__, location + i, "the current program object"); - return; - } - if (transpose == GL_TRUE) { - // Transpose the matrix before uploading - GLfloat transposedMatrix[4]; - TransposeMatrix2x2(value + i * 4, transposedMatrix); - Uniform_State<4>(*programObject, location + i, transposedMatrix); - } else { - // No transpose needed, directly copy the matrix data - Uniform_State<4>(*programObject, location + i, value + i * 4); - } - } + UniformMatrixfv_Object(*programObject, __func__, location, count, transpose, value, 2, 2, + "the current program object"); } void UniformMatrix3fv_State(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) { @@ -1471,7 +1515,8 @@ namespace MobileGL::MG_Impl::GLImpl { } } - void UniformMatrixNonSquarefv_State(const char* caller, GLint location, GLsizei count) { + void UniformMatrixNonSquarefv_State(const char* caller, GLint location, GLsizei count, GLboolean transpose, + const GLfloat* value, Int columns, Int rows) { if (location == -1) return; auto& programObject = MG_State::pGLContext->GetProgramForUniform(); @@ -1482,21 +1527,8 @@ namespace MobileGL::MG_Impl::GLImpl { return; } - for (GLint i = 0; i < count; i++) { - if (!programObject->IsValidUniformLocation(location + i)) { - RecordInvalidUniformLocationError(caller, location + i, "the current program object"); - return; - } - if (programObject->IsUniformOpaqueAtLocation(location + i)) { - MG_State::pGLContext->RecordError( - ErrorCode::InvalidOperation, - MakeUnique("MG_Impl/GLImpl", caller, - "Opaque uniforms cannot be set with matrix Uniform calls.")); - return; - } - } - - // TODO: Implement non-square matrix uniform uploads for non-opaque uniforms. + UniformMatrixfv_Object(*programObject, caller, location, count, transpose, value, columns, rows, + "the current program object"); } void ProgramUniformMatrix2fv_State(GLuint program, GLint location, GLsizei count, GLboolean transpose, @@ -1514,23 +1546,8 @@ namespace MobileGL::MG_Impl::GLImpl { return; } - for (GLint i = 0; i < count; i++) { - if (i > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + i)) { - // Values for elements beyond the end of the uniform array are ignored. - break; - } - if (!programObject->IsValidUniformLocation(location + i)) { - RecordInvalidUniformLocationError(__func__, location + i, "program " + std::to_string(program)); - return; - } - if (transpose == GL_TRUE) { - GLfloat transposedMatrix[4]; - TransposeMatrix2x2(value + i * 4, transposedMatrix); - Uniform_State<4>(*programObject, location + i, transposedMatrix); - } else { - Uniform_State<4>(*programObject, location + i, value + i * 4); - } - } + UniformMatrixfv_Object(*programObject, __func__, location, count, transpose, value, 2, 2, + "program " + std::to_string(program)); } void ProgramUniformMatrix3fv_State(GLuint program, GLint location, GLsizei count, GLboolean transpose, @@ -1605,7 +1622,8 @@ namespace MobileGL::MG_Impl::GLImpl { } } - void ProgramUniformMatrixNonSquarefv_State(const char* caller, GLuint program, GLint location, GLsizei count) { + void ProgramUniformMatrixNonSquarefv_State(const char* caller, GLuint program, GLint location, GLsizei count, + GLboolean transpose, const GLfloat* value, Int columns, Int rows) { if (location == -1) return; auto& programObject = TryToGetProgramObject(program); @@ -1619,21 +1637,8 @@ namespace MobileGL::MG_Impl::GLImpl { return; } - for (GLint i = 0; i < count; i++) { - if (!programObject->IsValidUniformLocation(location + i)) { - RecordInvalidUniformLocationError(caller, location + i, "program " + std::to_string(program)); - return; - } - if (programObject->IsUniformOpaqueAtLocation(location + i)) { - MG_State::pGLContext->RecordError( - ErrorCode::InvalidOperation, - MakeUnique("MG_Impl/GLImpl", caller, - "Opaque uniforms cannot be set with matrix Uniform calls.")); - return; - } - } - - // TODO: Implement non-square matrix uniform uploads for non-opaque uniforms. + UniformMatrixfv_Object(*programObject, caller, location, count, transpose, value, columns, rows, + "program " + std::to_string(program)); } GLuint GetUniformBlockIndex_State(GLuint program, const GLchar* uniformBlockName) { @@ -2442,27 +2447,27 @@ namespace MobileGL::MG_Impl::GLImpl { } void UniformMatrix2x3fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) { - UniformMatrixNonSquarefv_State(__func__, location, count); + UniformMatrixNonSquarefv_State(__func__, location, count, transpose, value, 2, 3); } void UniformMatrix3x2fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) { - UniformMatrixNonSquarefv_State(__func__, location, count); + UniformMatrixNonSquarefv_State(__func__, location, count, transpose, value, 3, 2); } void UniformMatrix2x4fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) { - UniformMatrixNonSquarefv_State(__func__, location, count); + UniformMatrixNonSquarefv_State(__func__, location, count, transpose, value, 2, 4); } void UniformMatrix4x2fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) { - UniformMatrixNonSquarefv_State(__func__, location, count); + UniformMatrixNonSquarefv_State(__func__, location, count, transpose, value, 4, 2); } void UniformMatrix3x4fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) { - UniformMatrixNonSquarefv_State(__func__, location, count); + UniformMatrixNonSquarefv_State(__func__, location, count, transpose, value, 3, 4); } void UniformMatrix4x3fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) { - UniformMatrixNonSquarefv_State(__func__, location, count); + UniformMatrixNonSquarefv_State(__func__, location, count, transpose, value, 4, 3); } void ProgramUniform1f(GLuint program, GLint location, GLfloat v0) { @@ -2587,32 +2592,32 @@ namespace MobileGL::MG_Impl::GLImpl { void ProgramUniformMatrix2x3fv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) { - ProgramUniformMatrixNonSquarefv_State(__func__, program, location, count); + ProgramUniformMatrixNonSquarefv_State(__func__, program, location, count, transpose, value, 2, 3); } void ProgramUniformMatrix3x2fv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) { - ProgramUniformMatrixNonSquarefv_State(__func__, program, location, count); + ProgramUniformMatrixNonSquarefv_State(__func__, program, location, count, transpose, value, 3, 2); } void ProgramUniformMatrix2x4fv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) { - ProgramUniformMatrixNonSquarefv_State(__func__, program, location, count); + ProgramUniformMatrixNonSquarefv_State(__func__, program, location, count, transpose, value, 2, 4); } void ProgramUniformMatrix4x2fv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) { - ProgramUniformMatrixNonSquarefv_State(__func__, program, location, count); + ProgramUniformMatrixNonSquarefv_State(__func__, program, location, count, transpose, value, 4, 2); } void ProgramUniformMatrix3x4fv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) { - ProgramUniformMatrixNonSquarefv_State(__func__, program, location, count); + ProgramUniformMatrixNonSquarefv_State(__func__, program, location, count, transpose, value, 3, 4); } void ProgramUniformMatrix4x3fv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) { - ProgramUniformMatrixNonSquarefv_State(__func__, program, location, count); + ProgramUniformMatrixNonSquarefv_State(__func__, program, location, count, transpose, value, 4, 3); } GLuint GetUniformBlockIndex(GLuint program, const GLchar* uniformBlockName) { @@ -2859,6 +2864,16 @@ namespace MobileGL::MG_Impl::GLImpl { // is written as that sequence rather than as a private shortcut - every error it can // raise is one of theirs, raised at the point they would raise it. GLuint CreateShaderProgramv(GLenum type, GLsizei count, const GLchar* const* strings) { + // GL 4.6 core 7.3: a negative count is INVALID_VALUE and is checked before anything + // is created, so a bad count never leaks a shader name. An unrecognised type is + // INVALID_ENUM, which CreateShader_State raises below. + if (count < 0) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique("MG_Impl/GLImpl", __func__, "count must be non-negative.")); + return 0; + } + const GLuint shader = CreateShader_State(type); if (shader == 0) return 0; diff --git a/MobileGL/MG_Impl/GLImpl/Sampler/GL_Sampler.cpp b/MobileGL/MG_Impl/GLImpl/Sampler/GL_Sampler.cpp index 9ef1c277..f5ad6ad3 100644 --- a/MobileGL/MG_Impl/GLImpl/Sampler/GL_Sampler.cpp +++ b/MobileGL/MG_Impl/GLImpl/Sampler/GL_Sampler.cpp @@ -8,6 +8,7 @@ #include "GL_Sampler.h" #include "Validators.h" +#include "../Getter/GL_Getter.h" #include #include #include @@ -268,9 +269,20 @@ namespace MobileGL::MG_Impl::GLImpl { } } + // The number of texture units a sampler may be bound to. GL 3.3 core 3.8.2 names + // GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, which is what the backend advertises; the frontend's + // MAX_TEXTURE_IMAGE_UNITS is only the capacity of the unit array, so it is a clamp on the + // answer and never the answer itself - gating on it alone accepts every unit up to 192 no + // matter what the driver reports. + static GLint GetSamplerBindableTextureUnitCount() { + GLint maxTextureUnits = 0; + GetIntegerv(GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, &maxTextureUnits); + return std::min(std::max(maxTextureUnits, 0), MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS); + } + void BindSampler_State(GLuint unit, GLuint sampler) { MGLOG_D("BindSampler_State: unit = %u, sampler = %u", unit, sampler); - if (unit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) { + if (static_cast(unit) >= static_cast(GetSamplerBindableTextureUnitCount())) { MG_State::pGLContext->RecordError( ErrorCode::InvalidValue, MakeUnique("MG_Impl/GLImpl", "BindSampler", "texture unit out of range")); @@ -309,6 +321,20 @@ namespace MobileGL::MG_Impl::GLImpl { MakeUnique("MG_Impl/GLImpl", "BindSamplers", "count must be non-negative")); return; } + // ARB_multi_bind: the whole [first, first + count) range is checked up front and a + // range that runs past the last texture unit is INVALID_OPERATION - not the + // INVALID_VALUE the single-bind BindSampler_State reports per element, and nothing is + // bound when it fails. Both gates read the same limit (see + // GetSamplerBindableTextureUnitCount), so an out-of-range multi-bind can no longer slip + // past this check and be caught one element at a time with the wrong error class. + const GLint maxTextureUnits = GetSamplerBindableTextureUnitCount(); + if (static_cast(first) + static_cast(count) > static_cast(maxTextureUnits)) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique("MG_Impl/GLImpl", "BindSamplers", + "first + count exceeds the number of texture units.")); + return; + } for (GLsizei i = 0; i < count; ++i) { BindSampler_State(first + i, samplers ? samplers[i] : 0); diff --git a/MobileGL/MG_Impl/GLImpl/Sampler/Validators.cpp b/MobileGL/MG_Impl/GLImpl/Sampler/Validators.cpp index 9a028d31..a1ef8cc2 100644 --- a/MobileGL/MG_Impl/GLImpl/Sampler/Validators.cpp +++ b/MobileGL/MG_Impl/GLImpl/Sampler/Validators.cpp @@ -75,7 +75,11 @@ namespace MobileGL::MG_Impl::GLImpl::SamplerImpl { break; case GL_TEXTURE_COMPARE_FUNC: - if (param < GL_LEQUAL || param > GL_ALWAYS) { + // The eight depth-compare functions are contiguous from GL_NEVER (0x0200) to + // GL_ALWAYS (0x0207); GL_LEQUAL sits in the middle of that block, so starting + // the range there rejected NEVER/LESS/EQUAL and let GREATER/NOTEQUAL/GEQUAL + // through only by accident of them being above LEQUAL. + if (param < GL_NEVER || param > GL_ALWAYS) { MG_State::pGLContext->RecordError(ErrorCode::InvalidEnum, MakeUnique("MG_Impl/GLImpl", "ValidateSamplerParam", "Invalid compare function parameter")); diff --git a/MobileGL/MG_Impl/GLImpl/VertexArray/GL_VertexArray.cpp b/MobileGL/MG_Impl/GLImpl/VertexArray/GL_VertexArray.cpp index ca011da3..e7677708 100644 --- a/MobileGL/MG_Impl/GLImpl/VertexArray/GL_VertexArray.cpp +++ b/MobileGL/MG_Impl/GLImpl/VertexArray/GL_VertexArray.cpp @@ -106,6 +106,21 @@ namespace MobileGL::MG_Impl::GLImpl { return pname == GL_CURRENT_VERTEX_ATTRIB; } + // The two ARB_vertex_attrib_binding per-attribute queries. They do not live on the + // resolved VertexAttribute (which is the flat, already-combined view) but on the VAO's + // binding-point mapping, so they need the object, not the attribute. + static bool TryGetVertexAttribBindingQuery(GLuint index, GLenum pname, GLint& out) { + if (pname != GL_VERTEX_ATTRIB_BINDING && pname != GL_VERTEX_ATTRIB_RELATIVE_OFFSET) return false; + const auto& vao = MG_State::pGLContext->GetBoundVertexArray(); + if (!vao) { + out = 0; + return true; + } + out = pname == GL_VERTEX_ATTRIB_BINDING ? static_cast(vao->GetAttributeBindingIndex(index)) + : static_cast(vao->GetAttributeRelativeOffset(index)); + return true; + } + // The stride a pointer-style call gives its binding point: the argument when it is non-zero, // otherwise the tightly packed element size (GL 4.6 core 10.3.2). A packed 2_10_10_10 or // 10F_11F_11F attribute is one 32-bit word regardless of its component count. @@ -179,6 +194,11 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_VERTEX_ATTRIB_ARRAY_LONG: case GL_VERTEX_ATTRIB_ARRAY_DIVISOR: case GL_VERTEX_ATTRIB_ARRAY_POINTER: + // ARB_vertex_attrib_binding (core since GL 4.3). The binding-point view is real + // state on the VAO (GetAttributeBindingIndex / GetAttributeRelativeOffset), so + // both of its per-attribute queries are answerable. + case GL_VERTEX_ATTRIB_BINDING: + case GL_VERTEX_ATTRIB_RELATIVE_OFFSET: return true; default: MG_State::pGLContext->RecordError( @@ -944,6 +964,13 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_VERTEX_ATTRIB_ARRAY_DIVISOR: params[0] = static_cast(attr->Divisor); return; + case GL_VERTEX_ATTRIB_BINDING: + case GL_VERTEX_ATTRIB_RELATIVE_OFFSET: { + GLint value = 0; + TryGetVertexAttribBindingQuery(index, pname, value); + params[0] = static_cast(value); + return; + } default: MG_State::pGLContext->RecordError( ErrorCode::InvalidEnum, @@ -1007,6 +1034,13 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_VERTEX_ATTRIB_ARRAY_DIVISOR: params[0] = static_cast(attr->Divisor); return; + case GL_VERTEX_ATTRIB_BINDING: + case GL_VERTEX_ATTRIB_RELATIVE_OFFSET: { + GLint value = 0; + TryGetVertexAttribBindingQuery(index, pname, value); + params[0] = static_cast(value); + return; + } default: MG_State::pGLContext->RecordError( ErrorCode::InvalidEnum, @@ -1066,6 +1100,10 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_VERTEX_ATTRIB_ARRAY_DIVISOR: params[0] = static_cast(attr->Divisor); return; + case GL_VERTEX_ATTRIB_BINDING: + case GL_VERTEX_ATTRIB_RELATIVE_OFFSET: + TryGetVertexAttribBindingQuery(index, pname, params[0]); + return; default: MG_State::pGLContext->RecordError( ErrorCode::InvalidEnum, @@ -1204,6 +1242,9 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_VERTEX_ATTRIB_RELATIVE_OFFSET: *param = static_cast(vao->GetAttributeRelativeOffset(index)); return; + case GL_VERTEX_ATTRIB_BINDING: + *param = static_cast(vao->GetAttributeBindingIndex(index)); + return; default: MG_State::pGLContext->RecordError( ErrorCode::InvalidEnum, diff --git a/MobileGL/MG_Impl/GLImpl/VertexArray/Validators.cpp b/MobileGL/MG_Impl/GLImpl/VertexArray/Validators.cpp index f4accd01..564d7b9c 100644 --- a/MobileGL/MG_Impl/GLImpl/VertexArray/Validators.cpp +++ b/MobileGL/MG_Impl/GLImpl/VertexArray/Validators.cpp @@ -147,6 +147,30 @@ namespace MobileGL::MG_Impl::GLImpl::VertexArrayImpl { return false; } + // The integer path takes exactly the six signed/unsigned integer types (GL 4.6 + // core 10.3.2): BYTE, UNSIGNED_BYTE, SHORT, UNSIGNED_SHORT, INT, UNSIGNED_INT. + // A blacklist could not express that: GL_FLOAT, GL_HALF_FLOAT, + // GL_DOUBLE and GL_FIXED all convert to a perfectly valid DataType, so they slipped + // through and were recorded as integer attributes. + if (integerPath) { + switch (type) { + case DataType::Int8: + case DataType::Uint8: + case DataType::Int16: + case DataType::Uint16: + case DataType::Int32: + case DataType::Uint32: + break; + default: + MG_State::pGLContext->RecordError( + ErrorCode::InvalidEnum, + MakeUnique( + "MG_Impl/GLImpl", fn, + std::format("Type is not an integer vertex attribute type (attribute {}).", index))); + return false; + } + } + if (sizeRaw == static_cast(GL_BGRA)) { // GL_BGRA is a float-path-only size: it needs GL_UNSIGNED_BYTE or a 2_10_10_10 type and // normalized == GL_TRUE. On the integer path it is simply an out-of-range size. diff --git a/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp b/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp index 8bd8fa61..5ec71a9c 100644 --- a/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp +++ b/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp @@ -206,8 +206,11 @@ namespace MobileGL::MG_State::GLState { } void BufferObject::UploadSubData(DataPtr data, SizeT atOffset) { - MOBILEGL_ASSERT(!m_isMapped || (m_mappingAccess & BufferMappingAccessBit::Persistent), - "Cannot upload sub data while buffer is non-persistently mapped."); + // GL 4.6 core 6.5 forbids only the OVERLAPPING write: a glBufferSubData that stays + // clear of a non-persistent mapping is legal, and the frontend lets it through. + MOBILEGL_ASSERT(!m_isMapped || (m_mappingAccess & BufferMappingAccessBit::Persistent) || + atOffset >= m_mappedRange.end || atOffset + data.size <= m_mappedRange.start, + "Cannot upload sub data overlapping a non-persistent mapping."); MOBILEGL_ASSERT(atOffset + data.size <= m_size, "UploadSubData out of bounds: atOffset (%zu) + data.size (%zu) > m_size (%zu)", atOffset, data.size, m_size); diff --git a/MobileGL/MG_Test/Buffer/BufferTest.cpp b/MobileGL/MG_Test/Buffer/BufferTest.cpp index fcd72d04..b62bdc9e 100644 --- a/MobileGL/MG_Test/Buffer/BufferTest.cpp +++ b/MobileGL/MG_Test/Buffer/BufferTest.cpp @@ -455,16 +455,24 @@ TEST_F(BufferTest, BindBufferBaseZeroUnbindsBindingPoint) { } TEST_F(BufferTest, BindBufferRangeZeroUnbindsBindingPoint) { + // GL_SHADER_STORAGE_BUFFER offsets must be a multiple of + // GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT, so the offset cannot be a literal. + GLint ssboAlignment = 0; + MobileGL::MG_Impl::GLImpl::GetIntegerv(GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT, &ssboAlignment); + ASSERT_GT(ssboAlignment, 0); + const GLintptr offset = ssboAlignment; + const GLsizeiptr size = 8; + GLuint buffer = 0; MobileGL::MG_Impl::GLImpl::GenBuffers(1, &buffer); MobileGL::MG_Impl::GLImpl::BindBuffer(GL_SHADER_STORAGE_BUFFER, buffer); - MobileGL::MG_Impl::GLImpl::BufferData(GL_SHADER_STORAGE_BUFFER, 16, nullptr, GL_DYNAMIC_DRAW); + MobileGL::MG_Impl::GLImpl::BufferData(GL_SHADER_STORAGE_BUFFER, offset + size, nullptr, GL_DYNAMIC_DRAW); - MobileGL::MG_Impl::GLImpl::BindBufferRange(GL_SHADER_STORAGE_BUFFER, 3, buffer, 4, 8); + MobileGL::MG_Impl::GLImpl::BindBufferRange(GL_SHADER_STORAGE_BUFFER, 3, buffer, offset, size); auto& point = MobileGL::MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, 3); ASSERT_NE(point.GetBoundObject(), nullptr); - EXPECT_EQ(point.GetRange().start, 4); - EXPECT_EQ(point.GetRange().end, 12); + EXPECT_EQ(point.GetRange().start, static_cast(offset)); + EXPECT_EQ(point.GetRange().end, static_cast(offset + size)); MobileGL::MG_Impl::GLImpl::BindBufferRange(GL_SHADER_STORAGE_BUFFER, 3, 0, 0, 0); EXPECT_EQ(point.GetBoundObject(), nullptr); @@ -540,6 +548,305 @@ TEST_F(BufferTest, ClearNamedBufferSubDataRepeatsPattern) { EXPECT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR); } + +// GL 4.6 core 6.5: glBufferSubData fails only when the written range OVERLAPS the mapped range. +// A second, wrong test used to sit next to the correct one and reject any write whose end reached +// the start of the mapping - which killed every legal disjoint update in front of a mapped tail. +TEST_F(BufferTest, BufferSubDataRejectsOnlyRangesOverlappingTheMapping) { + GLuint buffer = 0; + MobileGL::MG_Impl::GLImpl::GenBuffers(1, &buffer); + MobileGL::MG_Impl::GLImpl::BindBuffer(GL_ARRAY_BUFFER, buffer); + MobileGL::MG_Impl::GLImpl::BufferData(GL_ARRAY_BUFFER, 64, nullptr, GL_DYNAMIC_DRAW); + ASSERT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + void* mapped = MobileGL::MG_Impl::GLImpl::MapBufferRange(GL_ARRAY_BUFFER, 32, 32, GL_MAP_WRITE_BIT); + ASSERT_NE(mapped, nullptr); + ASSERT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + // Entirely before the mapping: legal, and the bytes must land. + const Uint32 payload[4] = {1u, 2u, 3u, 4u}; + MobileGL::MG_Impl::GLImpl::BufferSubData(GL_ARRAY_BUFFER, 0, sizeof(payload), payload); + EXPECT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + // Touching the first mapped byte: overlap, so INVALID_OPERATION. + MobileGL::MG_Impl::GLImpl::BufferSubData(GL_ARRAY_BUFFER, 16, 32, payload); + ExpectSingleGlError(GL_INVALID_OPERATION); + + EXPECT_TRUE(MobileGL::MG_Impl::GLImpl::UnmapBuffer(GL_ARRAY_BUFFER)); + + Vector actual(4, 0); + auto bufferObject = MobileGL::MG_State::pGLContext->GetBufferObject(buffer); + ASSERT_NE(bufferObject, nullptr); + Memcpy(actual.data(), bufferObject->AcquireMemory(false, true, false), sizeof(payload)); + EXPECT_EQ(actual, (Vector{1u, 2u, 3u, 4u})); + + MobileGL::MG_Impl::GLImpl::BindBuffer(GL_ARRAY_BUFFER, 0); + MobileGL::MG_Impl::GLImpl::DeleteBuffers(1, &buffer); + DrainPendingGlErrors(); +} + +// GL 4.6 core 6.2: "no buffer bound to target" outranks a bad size or bad flags, so the binding has +// to be resolved before either is validated. It used to be checked last, which turned every +// unbound-target call into INVALID_VALUE. +TEST_F(BufferTest, BufferStorageReportsTheUnboundTargetBeforeSizeAndFlags) { + MobileGL::MG_Impl::GLImpl::BindBuffer(GL_ARRAY_BUFFER, 0); + DrainPendingGlErrors(); + + // Both a zero size and a nonsense flag set are present; the unbound target still wins. + MobileGL::MG_Impl::GLImpl::BufferStorage(GL_ARRAY_BUFFER, 0, nullptr, GL_MAP_PERSISTENT_BIT); + ExpectSingleGlError(GL_INVALID_OPERATION); + + // With a buffer bound, the size check is reachable again. + GLuint buffer = 0; + MobileGL::MG_Impl::GLImpl::GenBuffers(1, &buffer); + MobileGL::MG_Impl::GLImpl::BindBuffer(GL_ARRAY_BUFFER, buffer); + MobileGL::MG_Impl::GLImpl::BufferStorage(GL_ARRAY_BUFFER, 0, nullptr, GL_MAP_READ_BIT); + ExpectSingleGlError(GL_INVALID_VALUE); + + MobileGL::MG_Impl::GLImpl::BindBuffer(GL_ARRAY_BUFFER, 0); + MobileGL::MG_Impl::GLImpl::DeleteBuffers(1, &buffer); + DrainPendingGlErrors(); +} + +// GL 4.6 core 6.1.1: glBindBufferRange on GL_SHADER_STORAGE_BUFFER must reject an offset that is +// not a multiple of GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT. +TEST_F(BufferTest, BindBufferRangeRejectsMisalignedShaderStorageOffset) { + GLint ssboAlignment = 0; + MobileGL::MG_Impl::GLImpl::GetIntegerv(GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT, &ssboAlignment); + ASSERT_GT(ssboAlignment, 1) << "a 1-byte alignment cannot express a misaligned offset"; + + GLuint buffer = 0; + MobileGL::MG_Impl::GLImpl::GenBuffers(1, &buffer); + MobileGL::MG_Impl::GLImpl::BindBuffer(GL_SHADER_STORAGE_BUFFER, buffer); + MobileGL::MG_Impl::GLImpl::BufferData(GL_SHADER_STORAGE_BUFFER, ssboAlignment * 4, nullptr, GL_DYNAMIC_DRAW); + ASSERT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MobileGL::MG_Impl::GLImpl::BindBufferRange(GL_SHADER_STORAGE_BUFFER, 1, buffer, 1, ssboAlignment); + ExpectSingleGlError(GL_INVALID_VALUE); + auto& point = MobileGL::MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, 1); + EXPECT_EQ(point.GetBoundObject(), nullptr) << "a rejected bind must not take effect"; + + // The uniform target has its own alignment and must not inherit the SSBO rule's rejection. + MobileGL::MG_Impl::GLImpl::BindBufferRange(GL_SHADER_STORAGE_BUFFER, 1, buffer, ssboAlignment, ssboAlignment); + EXPECT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + EXPECT_NE(point.GetBoundObject(), nullptr); + + MobileGL::MG_Impl::GLImpl::BindBufferRange(GL_SHADER_STORAGE_BUFFER, 1, 0, 0, 0); + MobileGL::MG_Impl::GLImpl::BindBuffer(GL_SHADER_STORAGE_BUFFER, 0); + MobileGL::MG_Impl::GLImpl::DeleteBuffers(1, &buffer); + DrainPendingGlErrors(); +} + +// ARB_multi_bind: the [first, first + count) range is checked up front and reports +// INVALID_OPERATION - not the per-element INVALID_VALUE a naive loop over glBindBufferBase would +// produce, and nothing may be bound when it fails. +TEST_F(BufferTest, BindBuffersBaseChecksTheWholeRangeBeforeBindingAnything) { + GLint maxBindings = 0; + MobileGL::MG_Impl::GLImpl::GetIntegerv(GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS, &maxBindings); + ASSERT_GT(maxBindings, 1); + + GLuint buffer = 0; + MobileGL::MG_Impl::GLImpl::GenBuffers(1, &buffer); + MobileGL::MG_Impl::GLImpl::BindBuffer(GL_SHADER_STORAGE_BUFFER, buffer); + MobileGL::MG_Impl::GLImpl::BufferData(GL_SHADER_STORAGE_BUFFER, 16, nullptr, GL_DYNAMIC_DRAW); + ASSERT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + // first is in range but first + count is not: one error, of the multi-bind class. + const GLuint first = static_cast(maxBindings - 1); + const GLuint buffers[2] = {buffer, buffer}; + MobileGL::MG_Impl::GLImpl::BindBuffersBase(GL_SHADER_STORAGE_BUFFER, first, 2, buffers); + ExpectSingleGlError(GL_INVALID_OPERATION); + auto& point = MobileGL::MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, first); + EXPECT_EQ(point.GetBoundObject(), nullptr) << "the in-range prefix must not be bound either"; + + MobileGL::MG_Impl::GLImpl::BindBuffersRange(GL_SHADER_STORAGE_BUFFER, first, 2, buffers, nullptr, nullptr); + ExpectSingleGlError(GL_INVALID_OPERATION); + + // A range that fits binds normally. + MobileGL::MG_Impl::GLImpl::BindBuffersBase(GL_SHADER_STORAGE_BUFFER, first, 1, buffers); + EXPECT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + ASSERT_NE(point.GetBoundObject(), nullptr); + EXPECT_EQ(point.GetBoundObject()->GetExternalIndex(), buffer); + + MobileGL::MG_Impl::GLImpl::BindBufferBase(GL_SHADER_STORAGE_BUFFER, first, 0); + MobileGL::MG_Impl::GLImpl::BindBuffer(GL_SHADER_STORAGE_BUFFER, 0); + MobileGL::MG_Impl::GLImpl::DeleteBuffers(1, &buffer); + DrainPendingGlErrors(); +} + +// These limits were reachable only through glGetInteger64v (SSBO block size) or not at all (the +// atomic-counter pair), so glGetIntegerv answered them with INVALID_ENUM out of its default arm. +TEST_F(BufferTest, GetIntegervAnswersSsboAndAtomicCounterLimits) { + GLint ssboBlockSize = 0; + MobileGL::MG_Impl::GLImpl::GetIntegerv(GL_MAX_SHADER_STORAGE_BLOCK_SIZE, &ssboBlockSize); + EXPECT_GT(ssboBlockSize, 0); + EXPECT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + // The 32-bit query saturates rather than truncating what glGetInteger64v reports. + GLint64 ssboBlockSize64 = 0; + MobileGL::MG_Impl::GLImpl::GetInteger64v(GL_MAX_SHADER_STORAGE_BLOCK_SIZE, &ssboBlockSize64); + EXPECT_EQ(static_cast(ssboBlockSize), std::min(ssboBlockSize64, INT32_MAX)); + + GLint atomicBindings = 0; + MobileGL::MG_Impl::GLImpl::GetIntegerv(GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS, &atomicBindings); + EXPECT_GE(atomicBindings, 1); + EXPECT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + GLint atomicBufferSize = 0; + MobileGL::MG_Impl::GLImpl::GetIntegerv(GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE, &atomicBufferSize); + EXPECT_GE(atomicBufferSize, 32); // GL 4.6 table 23.63 minimum + EXPECT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + // KHR_debug requires these to be legal even while the debug entry points are stubs. + GLint debugGroupDepth = 0; + MobileGL::MG_Impl::GLImpl::GetIntegerv(GL_MAX_DEBUG_GROUP_STACK_DEPTH, &debugGroupDepth); + EXPECT_GE(debugGroupDepth, 64); + GLint debugLoggedMessages = 0; + MobileGL::MG_Impl::GLImpl::GetIntegerv(GL_MAX_DEBUG_LOGGED_MESSAGES, &debugLoggedMessages); + EXPECT_GE(debugLoggedMessages, 1); + EXPECT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +// GL 4.6 core 6.1.1: glBindBufferRange validates the (offset, size) pair before it writes any +// state. Nothing validated either one, so a negative offset reached Range1D(offset, offset + size) +// - which has no ordering check of its own - and a zero or negative size installed an empty or +// backwards range on the binding point. +TEST_F(BufferTest, BindBufferRangeRejectsNegativeOffsetAndNonPositiveSize) { + GLint ssboAlignment = 0; + MobileGL::MG_Impl::GLImpl::GetIntegerv(GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT, &ssboAlignment); + ASSERT_GT(ssboAlignment, 0); + + GLuint buffer = 0; + MobileGL::MG_Impl::GLImpl::GenBuffers(1, &buffer); + MobileGL::MG_Impl::GLImpl::BindBuffer(GL_SHADER_STORAGE_BUFFER, buffer); + MobileGL::MG_Impl::GLImpl::BufferData(GL_SHADER_STORAGE_BUFFER, ssboAlignment * 4, nullptr, GL_DYNAMIC_DRAW); + ASSERT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + auto& point = MobileGL::MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, 2); + + // A negative offset is INVALID_VALUE - including one that is a multiple of the alignment, which + // the modulo gate alone waves through (-alignment % alignment == 0). + MobileGL::MG_Impl::GLImpl::BindBufferRange(GL_SHADER_STORAGE_BUFFER, 2, buffer, -ssboAlignment, ssboAlignment); + ExpectSingleGlError(GL_INVALID_VALUE); + EXPECT_EQ(point.GetBoundObject(), nullptr) << "a rejected bind must not take effect"; + + // size must be strictly positive. + MobileGL::MG_Impl::GLImpl::BindBufferRange(GL_SHADER_STORAGE_BUFFER, 2, buffer, 0, 0); + ExpectSingleGlError(GL_INVALID_VALUE); + MobileGL::MG_Impl::GLImpl::BindBufferRange(GL_SHADER_STORAGE_BUFFER, 2, buffer, 0, -4); + ExpectSingleGlError(GL_INVALID_VALUE); + EXPECT_EQ(point.GetBoundObject(), nullptr); + + // The well-formed bind still goes through. + MobileGL::MG_Impl::GLImpl::BindBufferRange(GL_SHADER_STORAGE_BUFFER, 2, buffer, ssboAlignment, ssboAlignment); + EXPECT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + ASSERT_NE(point.GetBoundObject(), nullptr); + EXPECT_EQ(point.GetRange().start, static_cast(ssboAlignment)); + EXPECT_EQ(point.GetRange().end, static_cast(ssboAlignment * 2)); + + // Buffer 0 detaches with offset and size ignored: the one case the size rule must not fire on, + // and the shape glBindBuffersRange uses to reset an element. + MobileGL::MG_Impl::GLImpl::BindBufferRange(GL_SHADER_STORAGE_BUFFER, 2, 0, 0, 0); + EXPECT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + EXPECT_EQ(point.GetBoundObject(), nullptr); + + MobileGL::MG_Impl::GLImpl::BindBuffer(GL_SHADER_STORAGE_BUFFER, 0); + MobileGL::MG_Impl::GLImpl::DeleteBuffers(1, &buffer); + DrainPendingGlErrors(); +} + +// GL 4.6 core 6.1.1 gives GL_UNIFORM_BUFFER its own offset alignment +// (GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT) and requires BOTH offset and size to be multiples of 4 on +// GL_TRANSFORM_FEEDBACK_BUFFER. Only the shader-storage half of the rule was implemented, so a +// misaligned uniform range bound happily. +TEST_F(BufferTest, BindBufferRangeEnforcesUniformAndTransformFeedbackAlignment) { + GLint uboAlignment = 0; + MobileGL::MG_Impl::GLImpl::GetIntegerv(GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT, &uboAlignment); + ASSERT_GT(uboAlignment, 1) << "a 1-byte alignment cannot express a misaligned offset"; + + GLuint buffer = 0; + MobileGL::MG_Impl::GLImpl::GenBuffers(1, &buffer); + MobileGL::MG_Impl::GLImpl::BindBuffer(GL_UNIFORM_BUFFER, buffer); + MobileGL::MG_Impl::GLImpl::BufferData(GL_UNIFORM_BUFFER, uboAlignment * 4, nullptr, GL_DYNAMIC_DRAW); + ASSERT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + auto& uniformPoint = MobileGL::MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::Uniform, 1); + MobileGL::MG_Impl::GLImpl::BindBufferRange(GL_UNIFORM_BUFFER, 1, buffer, 1, uboAlignment); + ExpectSingleGlError(GL_INVALID_VALUE); + EXPECT_EQ(uniformPoint.GetBoundObject(), nullptr) << "a misaligned uniform range must not bind"; + + MobileGL::MG_Impl::GLImpl::BindBufferRange(GL_UNIFORM_BUFFER, 1, buffer, uboAlignment, uboAlignment); + EXPECT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + EXPECT_NE(uniformPoint.GetBoundObject(), nullptr); + MobileGL::MG_Impl::GLImpl::BindBufferRange(GL_UNIFORM_BUFFER, 1, 0, 0, 0); + EXPECT_EQ(uniformPoint.GetBoundObject(), nullptr); + + // Transform feedback captures 32-bit components: offset and size are both constrained, and the + // size half has no analogue on any other target. + auto& feedbackPoint = + MobileGL::MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::TransformFeedback, 0); + MobileGL::MG_Impl::GLImpl::BindBufferRange(GL_TRANSFORM_FEEDBACK_BUFFER, 0, buffer, 2, 4); + ExpectSingleGlError(GL_INVALID_VALUE); + EXPECT_EQ(feedbackPoint.GetBoundObject(), nullptr); + MobileGL::MG_Impl::GLImpl::BindBufferRange(GL_TRANSFORM_FEEDBACK_BUFFER, 0, buffer, 4, 2); + ExpectSingleGlError(GL_INVALID_VALUE); + EXPECT_EQ(feedbackPoint.GetBoundObject(), nullptr); + + MobileGL::MG_Impl::GLImpl::BindBufferRange(GL_TRANSFORM_FEEDBACK_BUFFER, 0, buffer, 4, 4); + EXPECT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + EXPECT_NE(feedbackPoint.GetBoundObject(), nullptr); + + MobileGL::MG_Impl::GLImpl::BindBufferRange(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0, 0, 0); + MobileGL::MG_Impl::GLImpl::BindBuffer(GL_UNIFORM_BUFFER, 0); + MobileGL::MG_Impl::GLImpl::DeleteBuffers(1, &buffer); + DrainPendingGlErrors(); +} + +// ARB_multi_bind checks offsets and sizes separately for each binding point: the offending element +// is left unchanged and reports INVALID_VALUE while every other element still binds. Only the +// [first, first + count) range is the up-front, all-or-nothing check - so glBindBuffersRange gets +// the new gates by looping over the single-bind entry point, and must keep going after one fails. +TEST_F(BufferTest, BindBuffersRangeAppliesTheOffsetAndSizeGatesPerElement) { + GLint ssboAlignment = 0; + MobileGL::MG_Impl::GLImpl::GetIntegerv(GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT, &ssboAlignment); + ASSERT_GT(ssboAlignment, 1) << "a 1-byte alignment cannot express a misaligned offset"; + + GLuint buffer = 0; + MobileGL::MG_Impl::GLImpl::GenBuffers(1, &buffer); + MobileGL::MG_Impl::GLImpl::BindBuffer(GL_SHADER_STORAGE_BUFFER, buffer); + MobileGL::MG_Impl::GLImpl::BufferData(GL_SHADER_STORAGE_BUFFER, ssboAlignment * 8, nullptr, GL_DYNAMIC_DRAW); + ASSERT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + auto& firstPoint = MobileGL::MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, 0); + auto& secondPoint = MobileGL::MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, 1); + const GLuint buffers[2] = {buffer, buffer}; + + // Element 0 is misaligned; element 1 is well formed and must still be bound. + const GLintptr misalignedOffsets[2] = {1, ssboAlignment}; + const GLsizeiptr sizes[2] = {ssboAlignment, ssboAlignment}; + MobileGL::MG_Impl::GLImpl::BindBuffersRange(GL_SHADER_STORAGE_BUFFER, 0, 2, buffers, misalignedOffsets, sizes); + ExpectSingleGlError(GL_INVALID_VALUE); + EXPECT_EQ(firstPoint.GetBoundObject(), nullptr) << "the rejected element must not bind"; + ASSERT_NE(secondPoint.GetBoundObject(), nullptr) << "a per-element error must not abort the rest of the range"; + EXPECT_EQ(secondPoint.GetRange().start, static_cast(ssboAlignment)); + + MobileGL::MG_Impl::GLImpl::BindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, 0); + ASSERT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + // Same for a non-positive size, on the other element this time. + const GLintptr offsets[2] = {0, ssboAlignment}; + const GLsizeiptr badSizes[2] = {ssboAlignment, 0}; + MobileGL::MG_Impl::GLImpl::BindBuffersRange(GL_SHADER_STORAGE_BUFFER, 0, 2, buffers, offsets, badSizes); + ExpectSingleGlError(GL_INVALID_VALUE); + EXPECT_NE(firstPoint.GetBoundObject(), nullptr); + EXPECT_EQ(secondPoint.GetBoundObject(), nullptr); + + MobileGL::MG_Impl::GLImpl::BindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, 0); + MobileGL::MG_Impl::GLImpl::BindBuffer(GL_SHADER_STORAGE_BUFFER, 0); + MobileGL::MG_Impl::GLImpl::DeleteBuffers(1, &buffer); + DrainPendingGlErrors(); +} + using namespace MobileGL::MG_Impl::GLImpl; class GeneralBufferTest : public ::testing::Test { diff --git a/MobileGL/MG_Test/Program/ProgramTest.cpp b/MobileGL/MG_Test/Program/ProgramTest.cpp index d415f34c..f960cfbf 100644 --- a/MobileGL/MG_Test/Program/ProgramTest.cpp +++ b/MobileGL/MG_Test/Program/ProgramTest.cpp @@ -3091,3 +3091,148 @@ TEST_F(ProgramTest, PreprocessCacheOverflowKeepsCompilingCorrectly) { EXPECT_GE(GetUniformLocation(programB, "uColor"), 0); EXPECT_EQ(GetError(), GL_NO_ERROR); } + +// glUniformMatrix{2x3,2x4,3x2,3x4,4x2,4x3}fv and their twelve glProgramUniformMatrix* twins were +// validate-only no-ops: they never took the value pointer at all. They upload column-at-a-time at +// the std140 16-byte column stride, honouring `transpose`, and glGetUniformfv undoes that padding. +TEST_F(ProgramTest, NonSquareMatrixUniformsRoundTripThroughTheGlobalUbo) { + const char* vsSource = R"(#version 430 core +uniform mat2x3 uM2x3; +uniform mat3x2 uM3x2; +uniform mat4x3 uM4x3; +uniform mat2 uM2; +void main() { + vec3 a = uM2x3 * vec2(1.0); + vec2 b = uM3x2 * vec3(1.0); + vec3 c = uM4x3 * vec4(1.0); + vec2 d = uM2 * vec2(1.0); + gl_Position = vec4(a.xy + b + c.xy + d, 0.0, 1.0); +} +)"; + const char* fsSource = R"(#version 430 core +out vec4 fragColor; +void main() { fragColor = vec4(1.0); } +)"; + GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, vsSource); + GLuint fs = CompileShaderChecked(GL_FRAGMENT_SHADER, fsSource); + GLuint program = LinkVsFs(vs, fs, GL_TRUE); + UseProgram(program); + ASSERT_EQ(GetError(), GL_NO_ERROR); + + // matCxR is C columns of R rows, column-major: value[c * R + r]. + const GLfloat m2x3[6] = {1, 2, 3, 4, 5, 6}; + const GLfloat m3x2[6] = {1, 2, 3, 4, 5, 6}; + const GLfloat m4x3[12] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}; + + const GLint loc2x3 = GetUniformLocation(program, "uM2x3"); + const GLint loc3x2 = GetUniformLocation(program, "uM3x2"); + const GLint loc4x3 = GetUniformLocation(program, "uM4x3"); + ASSERT_GE(loc2x3, 0); + ASSERT_GE(loc3x2, 0); + ASSERT_GE(loc4x3, 0); + + UniformMatrix2x3fv(loc2x3, 1, GL_FALSE, m2x3); + UniformMatrix3x2fv(loc3x2, 1, GL_FALSE, m3x2); + UniformMatrix4x3fv(loc4x3, 1, GL_FALSE, m4x3); + ASSERT_EQ(GetError(), GL_NO_ERROR); + + GLfloat readBack[12] = {}; + GetUniformfv(program, loc2x3, readBack); + EXPECT_EQ(std::memcmp(readBack, m2x3, sizeof(m2x3)), 0); + std::memset(readBack, 0, sizeof(readBack)); + GetUniformfv(program, loc3x2, readBack); + EXPECT_EQ(std::memcmp(readBack, m3x2, sizeof(m3x2)), 0); + std::memset(readBack, 0, sizeof(readBack)); + GetUniformfv(program, loc4x3, readBack); + EXPECT_EQ(std::memcmp(readBack, m4x3, sizeof(m4x3)), 0); + EXPECT_EQ(GetError(), GL_NO_ERROR); + + // transpose = GL_TRUE means the source is row-major: a mat3x2 (3 columns, 2 rows) is then + // given as 2 rows of 3, so {1,2,3, 4,5,6} is the column-major {1,4, 2,5, 3,6}. + UniformMatrix3x2fv(loc3x2, 1, GL_TRUE, m3x2); + const GLfloat expectedTransposed3x2[6] = {1, 4, 2, 5, 3, 6}; + std::memset(readBack, 0, sizeof(readBack)); + GetUniformfv(program, loc3x2, readBack); + EXPECT_EQ(std::memcmp(readBack, expectedTransposed3x2, sizeof(expectedTransposed3x2)), 0); + EXPECT_EQ(GetError(), GL_NO_ERROR); + + // The glProgramUniform* twin writes the same bytes without the program being current. + UseProgram(0); + const GLfloat other2x3[6] = {9, 8, 7, 6, 5, 4}; + ProgramUniformMatrix2x3fv(program, loc2x3, 1, GL_FALSE, other2x3); + std::memset(readBack, 0, sizeof(readBack)); + GetUniformfv(program, loc2x3, readBack); + EXPECT_EQ(std::memcmp(readBack, other2x3, sizeof(other2x3)), 0); + EXPECT_EQ(GetError(), GL_NO_ERROR); +} + +// A mat2 is not four contiguous floats in the global UBO: std140 pads each column vector out to +// 16 bytes, so column 1 starts at byte 16. Writing it packed put column 1 on top of column 0's +// padding, where the shader never reads it. +TEST_F(ProgramTest, Mat2UniformUsesTheStd140ColumnStride) { + const char* vsSource = R"(#version 430 core +uniform mat2 uM2; +void main() { gl_Position = vec4(uM2 * vec2(1.0), 0.0, 1.0); } +)"; + const char* fsSource = R"(#version 430 core +out vec4 fragColor; +void main() { fragColor = vec4(1.0); } +)"; + GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, vsSource); + GLuint fs = CompileShaderChecked(GL_FRAGMENT_SHADER, fsSource); + GLuint program = LinkVsFs(vs, fs, GL_TRUE); + UseProgram(program); + const GLint loc = GetUniformLocation(program, "uM2"); + ASSERT_GE(loc, 0); + + const GLfloat m2[4] = {1, 2, 3, 4}; + UniformMatrix2fv(loc, 1, GL_FALSE, m2); + ASSERT_EQ(GetError(), GL_NO_ERROR); + + // The GL-visible value is tightly packed... + GLfloat readBack[4] = {}; + GetUniformfv(program, loc, readBack); + EXPECT_EQ(std::memcmp(readBack, m2, sizeof(m2)), 0); + + // ...while the bytes in the UBO put column 1 at offset 16, not 8. + const auto& programObject = MG_State::pGLContext->GetProgramObject(program); + ASSERT_NE(programObject, nullptr); + const auto* ubo = static_cast(programObject->MapUBO()); + ASSERT_NE(ubo, nullptr); + const Uint offset = programObject->GetUniformOffset(static_cast(loc)); + ASSERT_NE(offset, MG_State::GLState::ProgramObject::kInvalidUniformOffset); + GLfloat column0[2] = {}; + GLfloat column1[2] = {}; + std::memcpy(column0, ubo + offset, sizeof(column0)); + std::memcpy(column1, ubo + offset + 16, sizeof(column1)); + EXPECT_FLOAT_EQ(column0[0], 1.0f); + EXPECT_FLOAT_EQ(column0[1], 2.0f); + EXPECT_FLOAT_EQ(column1[0], 3.0f); + EXPECT_FLOAT_EQ(column1[1], 4.0f); + EXPECT_EQ(GetError(), GL_NO_ERROR); +} + +// GL 4.6 core 7.1: shaderType is an enum, so an unrecognised one is INVALID_ENUM - it used to be +// reported as INVALID_VALUE. glCreateShaderProgramv adds a count < 0 gate ahead of everything. +TEST_F(ProgramTest, CreateShaderAndCreateShaderProgramvReportTheRightErrorClasses) { + while (GetError() != GL_NO_ERROR) { + } + + EXPECT_EQ(CreateShader(GL_FLOAT), 0u); + EXPECT_EQ(GetError(), GL_INVALID_ENUM); + EXPECT_EQ(GetError(), GL_NO_ERROR) << "the call recorded more than one error"; + + const char* source = "#version 330 core\nvoid main() { gl_Position = vec4(1.0); }\n"; + EXPECT_EQ(CreateShaderProgramv(GL_FLOAT, 1, &source), 0u); + EXPECT_EQ(GetError(), GL_INVALID_ENUM); + EXPECT_EQ(GetError(), GL_NO_ERROR) << "the call recorded more than one error"; + + EXPECT_EQ(CreateShaderProgramv(GL_VERTEX_SHADER, -1, &source), 0u); + EXPECT_EQ(GetError(), GL_INVALID_VALUE); + EXPECT_EQ(GetError(), GL_NO_ERROR) << "the call recorded more than one error"; + + // A well-formed call still works. + const GLuint program = CreateShaderProgramv(GL_VERTEX_SHADER, 1, &source); + EXPECT_NE(program, 0u); + EXPECT_EQ(GetError(), GL_NO_ERROR); +} diff --git a/MobileGL/MG_Test/Texture/TextureTest.cpp b/MobileGL/MG_Test/Texture/TextureTest.cpp index 93ff6982..1dedbe70 100644 --- a/MobileGL/MG_Test/Texture/TextureTest.cpp +++ b/MobileGL/MG_Test/Texture/TextureTest.cpp @@ -2672,3 +2672,166 @@ TEST_F(TextureTest, DecodeShadowDataToWideRGBACoversComponentAndPackedLayouts) { EXPECT_EQ(rgba[3], 2u); } } + +// GL 4.6 core table 23.18: GL_TEXTURE_COMPARE_FUNC takes the whole eight-function depth-compare +// range. The validator used to start it at GL_LEQUAL, which sits in the middle of the contiguous +// GL_NEVER..GL_ALWAYS block, so NEVER/LESS/EQUAL were rejected while GREATER/NOTEQUAL/GEQUAL only +// got through because they happen to be numerically above LEQUAL. +TEST_F(TextureTest, SamplerCompareFuncAcceptsTheWholeNeverToAlwaysRange) { + GLuint sampler = 0; + MG_Impl::GLImpl::GenSamplers(1, &sampler); + ASSERT_NE(sampler, 0u); + + const GLenum compareFuncs[] = {GL_NEVER, GL_LESS, GL_EQUAL, GL_LEQUAL, + GL_GREATER, GL_NOTEQUAL, GL_GEQUAL, GL_ALWAYS}; + for (GLenum func : compareFuncs) { + MG_Impl::GLImpl::SamplerParameteri(sampler, GL_TEXTURE_COMPARE_FUNC, static_cast(func)); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "compare func " << func << " was rejected"; + GLint readBack = 0; + MG_Impl::GLImpl::GetSamplerParameteriv(sampler, GL_TEXTURE_COMPARE_FUNC, &readBack); + EXPECT_EQ(static_cast(readBack), func); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + } + + // Just outside the block on both sides is still INVALID_ENUM. + MG_Impl::GLImpl::SamplerParameteri(sampler, GL_TEXTURE_COMPARE_FUNC, GL_NEVER - 1); + ExpectSingleGlError(GL_INVALID_ENUM); + MG_Impl::GLImpl::SamplerParameteri(sampler, GL_TEXTURE_COMPARE_FUNC, GL_ALWAYS + 1); + ExpectSingleGlError(GL_INVALID_ENUM); + + MG_Impl::GLImpl::DeleteSamplers(1, &sampler); +} + +// GL 4.6 core table 23.19: GL_TEXTURE_BINDING_* and GL_SAMPLER_BINDING are per-texture-unit, so +// glGetIntegeri_v must answer for unit `index` - not fall through to the backend, which knows +// nothing about the frontend's binding state. +TEST_F(TextureTest, GetIntegeriVReportsPerUnitTextureAndSamplerBindings) { + GLuint textures[2] = {0, 0}; + MG_Impl::GLImpl::GenTextures(2, textures); + ASSERT_NE(textures[0], 0u); + ASSERT_NE(textures[1], 0u); + + MG_Impl::GLImpl::ActiveTexture(GL_TEXTURE0); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, textures[0]); + MG_Impl::GLImpl::ActiveTexture(GL_TEXTURE3); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, textures[1]); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + GLint binding = -1; + MG_Impl::GLImpl::GetIntegeri_v(GL_TEXTURE_BINDING_2D, 0, &binding); + EXPECT_EQ(static_cast(binding), textures[0]); + MG_Impl::GLImpl::GetIntegeri_v(GL_TEXTURE_BINDING_2D, 3, &binding); + EXPECT_EQ(static_cast(binding), textures[1]); + // An unbound unit reports 0, and a target nothing was bound to reports 0 as well. + MG_Impl::GLImpl::GetIntegeri_v(GL_TEXTURE_BINDING_2D, 2, &binding); + EXPECT_EQ(binding, 0); + MG_Impl::GLImpl::GetIntegeri_v(GL_TEXTURE_BINDING_3D, 0, &binding); + EXPECT_EQ(binding, 0); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + // The non-indexed query keeps reporting the ACTIVE unit, which is still unit 3. + GLint activeUnitBinding = -1; + MG_Impl::GLImpl::GetIntegerv(GL_TEXTURE_BINDING_2D, &activeUnitBinding); + EXPECT_EQ(static_cast(activeUnitBinding), textures[1]); + + GLuint sampler = 0; + MG_Impl::GLImpl::GenSamplers(1, &sampler); + ASSERT_NE(sampler, 0u); + MG_Impl::GLImpl::BindSampler(2, sampler); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + MG_Impl::GLImpl::GetIntegeri_v(GL_SAMPLER_BINDING, 2, &binding); + EXPECT_EQ(static_cast(binding), sampler); + MG_Impl::GLImpl::GetIntegeri_v(GL_SAMPLER_BINDING, 1, &binding); + EXPECT_EQ(binding, 0); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + // Out of range is INVALID_VALUE, not a backend passthrough. + GLint maxUnits = 0; + MG_Impl::GLImpl::GetIntegerv(GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, &maxUnits); + ASSERT_GT(maxUnits, 0); + MG_Impl::GLImpl::GetIntegeri_v(GL_TEXTURE_BINDING_2D, static_cast(maxUnits) + 1024u, &binding); + ExpectSingleGlError(GL_INVALID_VALUE); + + MG_Impl::GLImpl::BindSampler(2, 0); + MG_Impl::GLImpl::DeleteSamplers(1, &sampler); + MG_Impl::GLImpl::ActiveTexture(GL_TEXTURE0); + MG_Impl::GLImpl::DeleteTextures(2, textures); + DrainPendingGlErrors(); +} + +// glGetFloati_v / glGetDoublei_v were no-op stubs: they left the caller's buffer holding whatever +// was on the stack. They are converters over the integer indexed query. +TEST_F(TextureTest, GetFloatiVAndGetDoubleiVConvertTheIndexedIntegerQuery) { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + ASSERT_NE(texture, 0u); + MG_Impl::GLImpl::ActiveTexture(GL_TEXTURE1); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + GLfloat asFloat = -1.0f; + MG_Impl::GLImpl::GetFloati_v(GL_TEXTURE_BINDING_2D, 1, &asFloat); + EXPECT_FLOAT_EQ(asFloat, static_cast(texture)); + + GLdouble asDouble = -1.0; + MG_Impl::GLImpl::GetDoublei_v(GL_TEXTURE_BINDING_2D, 1, &asDouble); + EXPECT_DOUBLE_EQ(asDouble, static_cast(texture)); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::ActiveTexture(GL_TEXTURE0); + MG_Impl::GLImpl::DeleteTextures(1, &texture); + DrainPendingGlErrors(); +} + +// GL 3.3 core 3.8.2: the unit glBindSampler accepts is bounded by +// GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS. The gate read the frontend's MAX_TEXTURE_IMAGE_UNITS +// instead - the capacity of the unit array, 192 - so every unit the backend does not have was +// accepted, and the single-bind path disagreed with the multi-bind twin about where the units end. +// The backend is stood in so the two limits are distinguishable no matter what the real one +// advertises. +TEST_F(TextureTest, BindSamplerRejectsUnitsBeyondMaxCombinedTextureImageUnits) { + GLuint sampler = 0; + MG_Impl::GLImpl::GenSamplers(1, &sampler); + ASSERT_NE(sampler, 0u); + + constexpr GLint kCombinedUnits = 24; + static_assert(kCombinedUnits < MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS, + "the stand-in limit has to be below the unit array capacity to tell the two apart"); + auto backend = MakeUnique(); + FormatCapabilityBackend::MutableDynamicParameters().MaxCombinedTextureImageUnits = kCombinedUnits; + ScopedBackendOverride backendOverride(Move(backend)); + + GLint reportedUnits = 0; + MG_Impl::GLImpl::GetIntegerv(GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, &reportedUnits); + ASSERT_EQ(reportedUnits, kCombinedUnits); + + // The last unit that exists still binds. + const GLuint lastUnit = static_cast(kCombinedUnits - 1); + MG_Impl::GLImpl::BindSampler(lastUnit, sampler); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + EXPECT_NE(MG_State::pGLContext->GetTextureUnitObject(static_cast(lastUnit)).GetSamplerObject(), nullptr); + + // One past it does not - this is the unit the old gate accepted. + MG_Impl::GLImpl::BindSampler(static_cast(kCombinedUnits), sampler); + ExpectSingleGlError(GL_INVALID_VALUE); + EXPECT_EQ(MG_State::pGLContext->GetTextureUnitObject(kCombinedUnits).GetSamplerObject(), nullptr); + + // Past the unit array as well is the same error, not an out-of-bounds index. + MG_Impl::GLImpl::BindSampler( + static_cast(MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) + 4u, sampler); + ExpectSingleGlError(GL_INVALID_VALUE); + + // Both gates now read the same limit: a multi-bind that ends exactly at it binds, and one that + // runs a single unit past it is the multi-bind's INVALID_OPERATION, reported up front - not the + // single-bind INVALID_VALUE from somewhere inside the loop. + const GLuint samplers[2] = {sampler, sampler}; + MG_Impl::GLImpl::BindSamplers(static_cast(kCombinedUnits - 2), 2, samplers); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + MG_Impl::GLImpl::BindSamplers(lastUnit, 2, samplers); + ExpectSingleGlError(GL_INVALID_OPERATION); + + MG_Impl::GLImpl::BindSampler(lastUnit, 0); + MG_Impl::GLImpl::BindSampler(static_cast(kCombinedUnits - 2), 0); + MG_Impl::GLImpl::DeleteSamplers(1, &sampler); + DrainPendingGlErrors(); +} diff --git a/MobileGL/MG_Test/VertexArray/VertexArrayTest.cpp b/MobileGL/MG_Test/VertexArray/VertexArrayTest.cpp index 736a3f6a..6fb42058 100644 --- a/MobileGL/MG_Test/VertexArray/VertexArrayTest.cpp +++ b/MobileGL/MG_Test/VertexArray/VertexArrayTest.cpp @@ -1290,3 +1290,77 @@ TEST_F(GeneralVertexArrayTest, ArrayFormat_IntegerPathRejectsPackedAndBgra) { EXPECT_FALSE(a0.IsBgra); } + +// The integer path takes exactly the eight signed/unsigned integer types (GL 4.6 core 10.3.2). +// The old check was a blacklist of Unknown + the packed types, so GL_FLOAT / GL_HALF_FLOAT / +// GL_DOUBLE / GL_FIXED all converted to a valid DataType and were silently recorded as integer +// attributes. +TEST_F(GeneralVertexArrayTest, ArrayFormat_IntegerPathRejectsFloatTypes) { + CreateVAO(); + CreateVBO(GL_ARRAY_BUFFER, 64); + + // Establish a known-good integer format first, so a rejected call is visible as "unchanged". + VertexAttribIPointer(0, 4, GL_INT, 0, nullptr); + ASSERT_EQ(GetError(), GL_NO_ERROR); + + const GLenum floatTypes[] = {GL_FLOAT, GL_HALF_FLOAT, GL_DOUBLE, GL_FIXED}; + for (GLenum type : floatTypes) { + VertexAttribIPointer(0, 4, type, 0, nullptr); + EXPECT_EQ(GetError(), GL_INVALID_ENUM) << "type " << type << " was accepted on the integer path"; + const auto& attribute = MG_State::pGLContext->GetBoundVertexArray()->GetAttribute(0); + EXPECT_EQ(attribute.Type, DataType::Int32) << "a rejected format must not take effect"; + EXPECT_TRUE(attribute.IsInteger); + } + + // The float path still accepts them. + VertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, nullptr); + EXPECT_EQ(GetError(), GL_NO_ERROR); + EXPECT_EQ(MG_State::pGLContext->GetBoundVertexArray()->GetAttribute(0).Type, DataType::Float32); +} + +// ARB_vertex_attrib_binding's two per-attribute queries. They were missing from +// ValidateVertexAttribPname (so glGetVertexAttribiv answered INVALID_ENUM) and from +// glGetVertexArrayIndexediv's switch (GL_VERTEX_ATTRIB_BINDING only). +TEST_F(GeneralVertexArrayTest, ArrayFormat_BindingAndRelativeOffsetAreQueryable) { + const GLuint vao = CreateVAO(); + CreateVBO(GL_ARRAY_BUFFER, 256); + + VertexAttribFormat(2, 3, GL_FLOAT, GL_FALSE, 12); + VertexAttribBinding(2, 5); + ASSERT_EQ(GetError(), GL_NO_ERROR); + + GLint binding = -1; + GetVertexAttribiv(2, GL_VERTEX_ATTRIB_BINDING, &binding); + EXPECT_EQ(binding, 5); + EXPECT_EQ(GetError(), GL_NO_ERROR); + + GLint relativeOffset = -1; + GetVertexAttribiv(2, GL_VERTEX_ATTRIB_RELATIVE_OFFSET, &relativeOffset); + EXPECT_EQ(relativeOffset, 12); + EXPECT_EQ(GetError(), GL_NO_ERROR); + + // The float and double views convert the same value. + GLfloat bindingAsFloat = -1.0f; + GetVertexAttribfv(2, GL_VERTEX_ATTRIB_BINDING, &bindingAsFloat); + EXPECT_FLOAT_EQ(bindingAsFloat, 5.0f); + GLdouble offsetAsDouble = -1.0; + GetVertexAttribdv(2, GL_VERTEX_ATTRIB_RELATIVE_OFFSET, &offsetAsDouble); + EXPECT_DOUBLE_EQ(offsetAsDouble, 12.0); + EXPECT_EQ(GetError(), GL_NO_ERROR); + + // The by-name (DSA) indexed query answers both as well. + GLint namedBinding = -1; + GetVertexArrayIndexediv(vao, 2, GL_VERTEX_ATTRIB_BINDING, &namedBinding); + EXPECT_EQ(namedBinding, 5); + GLint namedRelativeOffset = -1; + GetVertexArrayIndexediv(vao, 2, GL_VERTEX_ATTRIB_RELATIVE_OFFSET, &namedRelativeOffset); + EXPECT_EQ(namedRelativeOffset, 12); + EXPECT_EQ(GetError(), GL_NO_ERROR); + + // An attribute nobody re-bound keeps the default attribute-i -> binding-i mapping. + GetVertexAttribiv(1, GL_VERTEX_ATTRIB_BINDING, &binding); + EXPECT_EQ(binding, 1); + GetVertexAttribiv(1, GL_VERTEX_ATTRIB_RELATIVE_OFFSET, &relativeOffset); + EXPECT_EQ(relativeOffset, 0); + EXPECT_EQ(GetError(), GL_NO_ERROR); +}