[Fix] (Review): bound copies by the requested level, reach every cube face, keep array layer counts, and give glSpecializeShader its spec error surface

This commit is contained in:
2026-08-27 05:51:58 -04:00
parent e430e1b3be
commit 9ef33f4274
16 changed files with 977 additions and 127 deletions
+75 -28
View File
@@ -32,10 +32,22 @@ namespace MobileGL::MG_Impl::GLImpl {
static bool CheckShaderNameValidity(Uint shader) {
if (shader == 0 || !MG_State::pGLContext->ValidateShaderName(shader)) {
// The mirror of CheckProgramNameValidity below, and for the same reason: programs and
// shaders are drawn from ONE name space (ProgramState hands both out of a single
// generator), so a name that exists but belongs to a PROGRAM is the wrong kind of
// object - GL 3.3 core 2.11.x makes that INVALID_OPERATION - while a name GL never
// handed out is INVALID_VALUE. This half of the split was missing, so every shader
// entry point handed a program name reported INVALID_VALUE; the conformance suite
// reads exactly that code back from glSpecializeShader.
const ErrorCode error = (shader != 0 && MG_State::pGLContext->ValidateProgramName(shader))
? ErrorCode::InvalidOperation
: ErrorCode::InvalidValue;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
error,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
std::to_string(shader) + " is not a valid name."));
std::to_string(shader) +
(error == ErrorCode::InvalidOperation ? " is not a shader object."
: " is not a valid name.")));
return false;
}
return true;
@@ -451,6 +463,25 @@ namespace MobileGL::MG_Impl::GLImpl {
" has no SPIR-V binary; call glShaderBinary first."));
return;
}
// ARB_gl_spirv: a shader that has already been specialized may not be specialized again
// until glShaderBinary re-associates a module with it.
if (shaderObject->HasBeenSpecialized()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"shader " + std::to_string(shader) +
" has already been specialized; re-associate its module with "
"glShaderBinary before specializing it again."));
return;
}
// pEntryPoint names the entry point to specialize; there is no default. A null pointer
// cannot name one, and neither can the empty string.
if (pEntryPoint == nullptr || *pEntryPoint == '\0') {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "pEntryPoint must name an entry point."));
return;
}
if (numSpecializationConstants > 0 && (pConstantIndex == nullptr || pConstantValue == nullptr)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
@@ -475,20 +506,32 @@ namespace MobileGL::MG_Impl::GLImpl {
}
}
const String entryPoint = pEntryPoint ? String(pEntryPoint) : String{};
const String entryPoint(pEntryPoint);
const GLenum shaderType = MG_Util::ConvertShaderStageToGLEnum(shaderObject->GetShaderStage());
using SpecializationFailure = MG_Util::ShaderTranspiler::ShaderCompiler::SpecializationFailure;
SpecializationFailure failure = SpecializationFailure::None;
auto specialized = MG_Util::ShaderTranspiler::ShaderCompiler::SpecializeAndDecompileSpirvModule(
shaderObject->GetSpirvBinary(), shaderType, entryPoint, constantIds, constantValues);
shaderObject->GetSpirvBinary(), shaderType, entryPoint, constantIds, constantValues, failure);
if (!specialized) {
// Specialization failure is a COMPILE failure, not a GL error: ARB_gl_spirv routes it
// through COMPILE_STATUS and the info log exactly as glCompileShader does, so an
// application that checks the status the usual way sees it.
MGLOG_D("%s: specialization failed for shader %u: %s", __func__, shader,
specialized.error().log.c_str());
// The two conditions ARB_gl_spirv ENUMERATES are GL errors, and an erroring GL command
// must have no other effect - so the shader object is left exactly as it was rather
// than being pushed into a failed-compile state. Anything else is a genuine compile
// failure of a well-formed request, which the extension routes through COMPILE_STATUS
// and the info log exactly as glCompileShader does.
if (failure == SpecializationFailure::UnknownEntryPoint ||
failure == SpecializationFailure::UnknownConstantId) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, specialized.error().log));
return;
}
shaderObject->RecordSpecializationFailure(String(specialized.error().log));
return;
}
shaderObject->SpecializeFromSpirv(Move(specialized.value()));
shaderObject->SpecializeFromSpirv(Move(specialized.value().glsl), Move(specialized.value().xfbVaryings),
specialized.value().xfbBufferMode);
}
// glMaxShaderCompilerThreadsKHR / glMaxShaderCompilerThreadsARB - one implementation,
@@ -1057,9 +1100,13 @@ namespace MobileGL::MG_Impl::GLImpl {
}
*params = shaderObject->GetInfoLog().empty() ? 0 : (GLint)shaderObject->GetInfoLog().length() + 1;
break;
case GL_SHADER_SOURCE_LENGTH:
*params = shaderObject->GetShaderSource().empty() ? 0 : (GLint)shaderObject->GetShaderSource().length() + 1;
case GL_SHADER_SOURCE_LENGTH: {
// The APPLICATION's source, which is empty for a shader that came from glShaderBinary -
// see ShaderObject::GetApplicationShaderSource.
const auto& source = shaderObject->GetApplicationShaderSource();
*params = source.empty() ? 0 : (GLint)source.length() + 1;
break;
}
// GL_ARB_gl_spirv. GL_SPIR_V_BINARY and GL_SPIR_V_BINARY_ARB are the same token: TRUE
// while the object stands for an application-supplied module. It is the FIRST thing the
// conformance suite asks after glShaderBinary, and it used to fall into the terminal
@@ -1111,7 +1158,7 @@ namespace MobileGL::MG_Impl::GLImpl {
auto& shaderObject = TryToGetShaderObject(shader);
if (!shaderObject) return;
auto& src = shaderObject->GetShaderSource();
auto& src = shaderObject->GetApplicationShaderSource();
CopyStr(bufSize, length, source, src.c_str(), (GLsizei)src.length());
}
@@ -1968,8 +2015,6 @@ namespace MobileGL::MG_Impl::GLImpl {
void ProgramUniformMatrix2fv_State(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLfloat* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program);
if (!programObject) return;
@@ -1981,14 +2026,14 @@ namespace MobileGL::MG_Impl::GLImpl {
return;
}
if (location == -1) return;
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,
const GLfloat* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program);
if (!programObject) return;
@@ -2000,6 +2045,8 @@ namespace MobileGL::MG_Impl::GLImpl {
return;
}
if (location == -1) 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.
@@ -2025,8 +2072,6 @@ namespace MobileGL::MG_Impl::GLImpl {
void ProgramUniformMatrix4fv_State(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLfloat* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program);
if (!programObject) return;
@@ -2038,6 +2083,8 @@ namespace MobileGL::MG_Impl::GLImpl {
return;
}
if (location == -1) 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.
@@ -2059,8 +2106,6 @@ namespace MobileGL::MG_Impl::GLImpl {
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);
if (!programObject) return;
@@ -2072,6 +2117,8 @@ namespace MobileGL::MG_Impl::GLImpl {
return;
}
if (location == -1) return;
UniformMatrixfv_Object(*programObject, caller, location, count, transpose, value, columns, rows,
"program " + std::to_string(program));
}
@@ -2621,7 +2668,6 @@ namespace MobileGL::MG_Impl::GLImpl {
void ProgramUniformMatrix2dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLdouble* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program);
if (!programObject) return;
if (!programObject->GetLinkStatus()) {
@@ -2631,6 +2677,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"program " + std::to_string(program) + " is not linked."));
return;
}
if (location == -1) return;
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 2, 2);
}
void UniformMatrix3dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
@@ -2647,7 +2694,6 @@ namespace MobileGL::MG_Impl::GLImpl {
void ProgramUniformMatrix3dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLdouble* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program);
if (!programObject) return;
if (!programObject->GetLinkStatus()) {
@@ -2657,6 +2703,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"program " + std::to_string(program) + " is not linked."));
return;
}
if (location == -1) return;
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 3, 3);
}
void UniformMatrix4dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
@@ -2673,7 +2720,6 @@ namespace MobileGL::MG_Impl::GLImpl {
void ProgramUniformMatrix4dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLdouble* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program);
if (!programObject) return;
if (!programObject->GetLinkStatus()) {
@@ -2683,6 +2729,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"program " + std::to_string(program) + " is not linked."));
return;
}
if (location == -1) return;
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 4, 4);
}
void UniformMatrix2x3dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
@@ -2699,7 +2746,6 @@ namespace MobileGL::MG_Impl::GLImpl {
void ProgramUniformMatrix2x3dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLdouble* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program);
if (!programObject) return;
if (!programObject->GetLinkStatus()) {
@@ -2709,6 +2755,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"program " + std::to_string(program) + " is not linked."));
return;
}
if (location == -1) return;
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 2, 3);
}
void UniformMatrix2x4dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
@@ -2725,7 +2772,6 @@ namespace MobileGL::MG_Impl::GLImpl {
void ProgramUniformMatrix2x4dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLdouble* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program);
if (!programObject) return;
if (!programObject->GetLinkStatus()) {
@@ -2735,6 +2781,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"program " + std::to_string(program) + " is not linked."));
return;
}
if (location == -1) return;
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 2, 4);
}
void UniformMatrix3x2dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
@@ -2751,7 +2798,6 @@ namespace MobileGL::MG_Impl::GLImpl {
void ProgramUniformMatrix3x2dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLdouble* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program);
if (!programObject) return;
if (!programObject->GetLinkStatus()) {
@@ -2761,6 +2807,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"program " + std::to_string(program) + " is not linked."));
return;
}
if (location == -1) return;
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 3, 2);
}
void UniformMatrix3x4dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
@@ -2777,7 +2824,6 @@ namespace MobileGL::MG_Impl::GLImpl {
void ProgramUniformMatrix3x4dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLdouble* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program);
if (!programObject) return;
if (!programObject->GetLinkStatus()) {
@@ -2787,6 +2833,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"program " + std::to_string(program) + " is not linked."));
return;
}
if (location == -1) return;
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 3, 4);
}
void UniformMatrix4x2dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
@@ -2803,7 +2850,6 @@ namespace MobileGL::MG_Impl::GLImpl {
void ProgramUniformMatrix4x2dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLdouble* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program);
if (!programObject) return;
if (!programObject->GetLinkStatus()) {
@@ -2813,6 +2859,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"program " + std::to_string(program) + " is not linked."));
return;
}
if (location == -1) return;
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 4, 2);
}
void UniformMatrix4x3dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
@@ -2829,7 +2876,6 @@ namespace MobileGL::MG_Impl::GLImpl {
void ProgramUniformMatrix4x3dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLdouble* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program);
if (!programObject) return;
if (!programObject->GetLinkStatus()) {
@@ -2839,6 +2885,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"program " + std::to_string(program) + " is not linked."));
return;
}
if (location == -1) return;
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 4, 3);
}
void GetUniformdv(GLuint program, GLint location, GLdouble* params) {
+167 -30
View File
@@ -403,10 +403,46 @@ namespace MobileGL::MG_Impl::GLImpl {
MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS));
}
// Array targets store their layer count in z; layers never participate in mip
// reduction (GL 3.3 §3.8.14), only true 3D textures halve their depth per level.
// How many components of a GL-space texel size actually halve down the mip chain.
//
// An array texture's LAYER COUNT is not a dimension of the image (GL 4.6 core 8.14.3): it
// stays put all the way down, and it is stored in whichever component sits after the
// image's own dimensions - z for a 2D array or a cube array, and HEIGHT for a 1D array,
// whose level is recorded as {width, layers, 1}.
//
// THE one statement of that rule on the frontend side, because three readers have to agree
// on it or a chain is allocated under one and judged under another: this allocator,
// ComputeMipmapCompleteForFilter (MG_State/GLState/TextureState/TextureObject.cpp, which
// uses the identical 1/2/3 split) and DirectVulkan's MipShrinkingComponentCount. It used to
// be a two-way `depthMips` flag, which had no way to say "height is not a dimension" - so
// glGenerateMipmap on a GL_TEXTURE_1D_ARRAY allocated a chain whose LAYER COUNT halved,
// and the completeness rule then rejected the texture the generate was supposed to make
// complete. The backend allocator could not repair it either: it only ever GROWS a chain,
// and the frontend's (wrong) count is always the longer of the two.
Int MipShrinkingAxisCount(TextureTarget target) {
switch (target) {
case TextureTarget::Texture1D:
// {width, 1, 1} - the other two are already 1, but say so rather than rely on it.
return 1;
case TextureTarget::Texture1DArray:
// {width, layers, 1}: height IS the layer count.
return 1;
case TextureTarget::Texture2DArray:
case TextureTarget::TextureCubeMapArray:
// {width, height, layers}: depth IS the layer count.
return 2;
case TextureTarget::Texture3D:
return 3;
default:
// 2D, cube faces, rectangle, multisample: a plain two-dimensional image.
return 2;
}
}
// Only true 3D textures halve their depth per level; every array target keeps its layer
// count. Expressed through the rule above so the two cannot drift.
Bool DepthParticipatesInMipmapping(TextureTarget target) {
return target == TextureTarget::Texture3D;
return MipShrinkingAxisCount(target) == 3;
}
// Which targets each glTextureStorage*D accepts (GL 4.6 core 8.19). A texture whose target
@@ -427,20 +463,20 @@ namespace MobileGL::MG_Impl::GLImpl {
}
}
// The longest mip chain the level-0 size admits. A 1D array keeps its layer count in
// height, so unlike a 2D texture its height takes no part in the reduction.
Uint ComputeFullMipmapLevelCount(const IntVec3& baseTexelSize, Bool depthMips);
// The longest mip chain the level-0 size admits, over the axes that actually reduce.
Uint ComputeFullMipmapLevelCount(const IntVec3& baseTexelSize, Int shrinkingAxes);
Uint MaxTextureStorageLevels(TextureTarget target, GLsizei width, GLsizei height, GLsizei depth) {
const Int mipHeight = (target == TextureTarget::Texture1DArray) ? 1 : std::max<Int>(height, 1);
return ComputeFullMipmapLevelCount({std::max<Int>(width, 1), mipHeight, std::max<Int>(depth, 1)},
DepthParticipatesInMipmapping(target));
return ComputeFullMipmapLevelCount(
{std::max<Int>(width, 1), std::max<Int>(height, 1), std::max<Int>(depth, 1)},
MipShrinkingAxisCount(target));
}
Uint ComputeFullMipmapLevelCount(const IntVec3& baseTexelSize, Bool depthMips) {
Int maxDimension = std::max<Int>(
baseTexelSize.x(),
std::max<Int>(baseTexelSize.y(), depthMips ? std::max<Int>(baseTexelSize.z(), 1) : 1));
Uint ComputeFullMipmapLevelCount(const IntVec3& baseTexelSize, Int shrinkingAxes) {
Int maxDimension = 1;
for (Int axis = 0; axis < shrinkingAxes && axis < 3; ++axis) {
maxDimension = std::max<Int>(maxDimension, baseTexelSize[axis]);
}
Uint mipLevelCount = 1;
while (maxDimension > 1) {
maxDimension = std::max<Int>(maxDimension / 2, 1);
@@ -449,13 +485,13 @@ namespace MobileGL::MG_Impl::GLImpl {
return mipLevelCount;
}
IntVec3 ComputeMipmapTexelSize(const IntVec3& baseTexelSize, Uint relativeLevel, Bool depthMips) {
return {
std::max<Int>(baseTexelSize.x() >> static_cast<Int>(relativeLevel), 1),
std::max<Int>(baseTexelSize.y() >> static_cast<Int>(relativeLevel), 1),
depthMips ? std::max<Int>(baseTexelSize.z() >> static_cast<Int>(relativeLevel), 1)
: std::max<Int>(baseTexelSize.z(), 1),
};
IntVec3 ComputeMipmapTexelSize(const IntVec3& baseTexelSize, Uint relativeLevel, Int shrinkingAxes) {
IntVec3 size = {std::max<Int>(baseTexelSize.x(), 1), std::max<Int>(baseTexelSize.y(), 1),
std::max<Int>(baseTexelSize.z(), 1)};
for (Int axis = 0; axis < shrinkingAxes && axis < 3; ++axis) {
size[axis] = std::max<Int>(size[axis] >> static_cast<Int>(relativeLevel), 1);
}
return size;
}
Bool EnsureGeneratedMipmapStorageAllocated(
@@ -477,10 +513,10 @@ namespace MobileGL::MG_Impl::GLImpl {
}
const SizeT bytesPerTexel = baseByteSize / baseTexelCount;
const Bool depthMips = DepthParticipatesInMipmapping(texture.GetTarget());
const Uint requiredLevelCount = ComputeFullMipmapLevelCount(baseTexelSize, depthMips);
const Int shrinkingAxes = MipShrinkingAxisCount(texture.GetTarget());
const Uint requiredLevelCount = ComputeFullMipmapLevelCount(baseTexelSize, shrinkingAxes);
for (Uint level = 1; level < requiredLevelCount; ++level) {
const IntVec3 levelTexelSize = ComputeMipmapTexelSize(baseTexelSize, level, depthMips);
const IntVec3 levelTexelSize = ComputeMipmapTexelSize(baseTexelSize, level, shrinkingAxes);
const SizeT levelByteSize = bytesPerTexel * static_cast<SizeT>(levelTexelSize.x()) *
static_cast<SizeT>(levelTexelSize.y()) *
static_cast<SizeT>(levelTexelSize.z());
@@ -3686,6 +3722,59 @@ namespace MobileGL::MG_Impl::GLImpl {
GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height,
GLsizei depth, const char* caller);
// The destination box of a copy has to lie inside the storage the copy actually WRITES, which
// is the requested (uploadTarget, level) pair's - not level 0's.
//
// This exists because the general-purpose ValidateTextureSubImageOffsets bounds everything by
// ITextureObject::GetBaseSize(), which is hardcoded to level 0 (TextureObject::GetBaseSize ->
// GetTexelSize(0, 0)). CopyReadFramebufferIntoMipmapRegion, meanwhile, sizes its rows and
// slices from GetMipmapTexelSize(uploadTarget, level) and memcpys into the exact-sized
// std::vector MipmapStorage allocated for that level, with no clamp of its own. A box that is
// legal at level 0 and out of range at level N therefore passed validation and wrote past the
// end of the heap allocation - e.g. a 4x4 copy at offset (4,4) into level 2 of an 8x8x4
// GL_RGBA8 array texture ran 24 bytes past a 64-byte buffer. Every level > 0 of every
// mipmapped texture was reachable that way, and both entry points had been no-ops before, so
// the whole exposure arrived with their implementation.
static Bool ValidateCopySubImageRegionAtLevel(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
TextureUploadTarget uploadTarget, GLint level, GLint xoffset,
GLint yoffset, GLint zoffset, GLsizei width, GLsizei height,
GLsizei depth, const char* caller) {
const auto* mipmapTexture = MG_State::GLState::AsMipmapTexture(textureObject.get());
if (mipmapTexture == nullptr) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "The destination texture has no mipmap storage."));
return false;
}
const IntVec3 levelSize = mipmapTexture->GetMipmapTexelSize(uploadTarget, static_cast<Uint>(level));
// A level that was never defined reports a degenerate extent. GL 4.6 core 8.6 makes
// copying into an undefined texture image INVALID_OPERATION, and it is also what keeps the
// writer below from indexing an empty allocation.
if (levelSize.x() <= 0 || levelSize.y() <= 0 || levelSize.z() <= 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"The requested texture level has no storage."));
return false;
}
// Signed 64-bit sums: xoffset and width are both GLint and an application may pass values
// whose sum overflows a GLint, which would otherwise compare as negative and pass.
const Int64 lastX = static_cast<Int64>(xoffset) + static_cast<Int64>(width);
const Int64 lastY = static_cast<Int64>(yoffset) + static_cast<Int64>(height);
const Int64 lastZ = static_cast<Int64>(zoffset) + static_cast<Int64>(depth);
if (xoffset < 0 || yoffset < 0 || zoffset < 0 || lastX > levelSize.x() || lastY > levelSize.y() ||
lastZ > levelSize.z()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", caller,
std::format("The destination region does not lie inside level {} ({}x{}x{}).", level,
levelSize.x(), levelSize.y(), levelSize.z())));
return false;
}
return true;
}
// The shared body of glCopyTexSubImage3D and glCopyTextureSubImage3D once the caller has
// resolved the destination texture. `allowCubeFaceFromZOffset` is the ONE difference between
// the two forms: the DSA form takes a cube map and selects the face with zoffset (GL 4.6 core
@@ -3696,20 +3785,72 @@ namespace MobileGL::MG_Impl::GLImpl {
GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x,
GLint y, GLsizei width, GLsizei height, Bool allowCubeFaceFromZOffset,
const char* caller) {
if (!ValidateCopyTextureSubImage(textureObject, level, xoffset, yoffset, zoffset, width, height, 1, caller)) {
if (!TextureImpl::ValidateTextureLevelNumber(level)) return;
if (width < 0 || height < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Copy dimensions must be non-negative."));
return;
}
// THE FACE MAPPING HAS TO HAPPEN BEFORE THE BOUNDS CHECK, not after it. A cube map stores
// its six faces as six upload targets of ONE z-slice each, so its GetBaseSize().z() is 1 -
// and the generic offset validator, whose z bound always comes from that, rejected every
// zoffset in 1..5 with GL_INVALID_VALUE before the mapping below could run. Five of six
// faces were unreachable through glCopyTextureSubImage3D even though the entry point
// documents zoffset as the face selector (GL 4.6 core 8.6). The cube bound is the FACE
// COUNT, which the generic validator has no way to express because its `depth` parameter
// is the copy extent; glClearTexSubImage already special-cases the same shape.
TextureUploadTarget uploadTarget = GetPrimaryUploadTarget(textureObject);
GLint sliceOffset = zoffset;
if (allowCubeFaceFromZOffset && textureObject->GetTarget() == TextureTarget::TextureCubeMap) {
const SizeT faceCount = textureObject->GetUploadTargets().size();
if (zoffset < 0 || static_cast<SizeT>(zoffset) >= faceCount) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", caller,
"zoffset selects the cube map face and must be in [0, " + std::to_string(faceCount) + ")."));
return;
}
uploadTarget = static_cast<TextureUploadTarget>(
static_cast<SizeT>(TextureUploadTarget::CubeMapPositiveX) + static_cast<SizeT>(zoffset));
sliceOffset = 0;
}
if (!ValidateCopySubImageRegionAtLevel(textureObject, uploadTarget, level, xoffset, yoffset, sliceOffset,
width, height, /*depth=*/1, caller)) {
return;
}
if (!FramebufferImpl::ValidateReadFramebufferForCopy(caller)) return;
CopyReadFramebufferIntoMipmapRegion(textureObject, uploadTarget, level, xoffset, yoffset, sliceOffset, x, y,
width, height, caller);
}
// The same for the one-dimensional pair. A 1D level is {width, 1, 1}, so the y and z arms of
// the check above are trivially satisfied and the x arm is the whole rule - which is exactly
// the one that overflowed: level 2 of an 8-texel GL_RGBA8 1D texture is 8 bytes, and a 4-texel
// copy at xoffset 4 wrote 16 bytes starting 16 bytes in, entirely outside the allocation.
static void CopyTextureSubImage1DResolved(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
GLint level, GLint xoffset, GLint x, GLint y, GLsizei width,
const char* caller) {
if (!TextureImpl::ValidateTextureLevelNumber(level)) return;
if (width < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Copy dimensions must be non-negative."));
return;
}
const TextureUploadTarget uploadTarget = GetPrimaryUploadTarget(textureObject);
if (!ValidateCopySubImageRegionAtLevel(textureObject, uploadTarget, level, xoffset, /*yoffset=*/0,
/*zoffset=*/0, width, /*height=*/1, /*depth=*/1, caller)) {
return;
}
if (!FramebufferImpl::ValidateReadFramebufferForCopy(caller)) return;
CopyReadFramebufferIntoMipmapRegion(textureObject, uploadTarget, level, xoffset, /*yoffset=*/0,
/*zoffset=*/0, x, y, width, /*height=*/1, caller);
}
void CopyTexSubImage3D_State(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x,
GLint y, GLsizei width, GLsizei height) {
// GL 4.6 core 8.6 table: the three-dimensional form of the bound-texture copy accepts
@@ -4108,9 +4249,7 @@ namespace MobileGL::MG_Impl::GLImpl {
const auto textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
if (!textureObject) return;
if (!ValidateCopyTextureSubImage(textureObject, level, xoffset, 0, 0, width, 1, 1, __func__)) return;
CopyReadFramebufferIntoMipmapRegion(textureObject, GetPrimaryUploadTarget(textureObject), level, xoffset,
/*yoffset=*/0, /*zoffset=*/0, x, y, width, /*height=*/1, __func__);
CopyTextureSubImage1DResolved(textureObject, level, xoffset, x, y, width, __func__);
}
Bool CopyTexImage2D_State(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width,
@@ -6725,9 +6864,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"CopyTextureSubImage1D requires a 1D texture."));
return;
}
if (!ValidateCopyTextureSubImage(textureObject, level, xoffset, 0, 0, width, 1, 1, __func__)) return;
CopyReadFramebufferIntoMipmapRegion(textureObject, GetPrimaryUploadTarget(textureObject), level, xoffset,
/*yoffset=*/0, /*zoffset=*/0, x, y, width, /*height=*/1, __func__);
CopyTextureSubImage1DResolved(textureObject, level, xoffset, x, y, width, __func__);
}
void CopyTextureSubImage3D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x,