[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,
+44 -10
View File
@@ -650,6 +650,12 @@ namespace MobileGL::MG_State {
// a graphics program carrying a compute module, which Adreno 830 does not reject
// from vkCreateGraphicsPipelines - it SIGSEGVs inside it.
Bool anyStage = false;
// Which stages the composite ACTUALLY got a shader for. Not the same question as
// "which stages have a stage program bound": one program bound with
// GL_ALL_SHADER_BITS occupies every slot while contributing a shader to only the
// stages it was linked with. The transform-feedback capture stage is chosen off this,
// because it has to be the stage that will exist in the composite's own link.
Bool compositeHasStage[ProgramPipelineObject::kGraphicsStageCount] = {};
for (SizeT stage = 0; stage < ProgramPipelineObject::kGraphicsStageCount; ++stage) {
const auto& stageProgram = pipeline->GetStageProgram(static_cast<ShaderStage>(stage));
if (!stageProgram) continue;
@@ -665,6 +671,7 @@ namespace MobileGL::MG_State {
if (!ref.shader || static_cast<SizeT>(ref.shader->GetShaderStage()) != stage) continue;
composite->AttachShaderWithPinnedLinkInput(ref);
anyStage = true;
compositeHasStage[stage] = true;
}
}
if (!anyStage) return nullProgram;
@@ -672,20 +679,47 @@ namespace MobileGL::MG_State {
// (GL 4.6 core 11.1.2.1), and glTransformFeedbackVaryings is per-PROGRAM state that
// only the stage program carrying that stage can have been given. The composite is
// assembled out of the stage programs' shaders and inherits none of their
// GL-thread-owned request state, so without this the composite links with an empty
// capture list and glBeginTransformFeedback rejects the draw with INVALID_OPERATION
// ("the program has no transform feedback varyings") even though
// glValidateProgramPipeline had just passed. Same resolution order as
// ProgramLinkTask::ResolveTransformFeedbackVaryings: geometry, else tessellation
// evaluation, else vertex.
// GL-thread-owned state, so without this it links with an empty capture list and
// glBeginTransformFeedback rejects the draw with INVALID_OPERATION ("the program has
// no transform feedback varyings") even though glValidateProgramPipeline had passed.
//
// TWO RULES, both easy to get subtly wrong and both load-bearing:
//
// (1) THE LINKED LIST, NOT THE PENDING REQUEST. glTransformFeedbackVaryings does not
// take effect until the program's next link (GL 4.6 core 7.3/11.1.2.1), and it
// deliberately bumps no version - so a request written after the stage program's
// last link is invisible to the composite cache's signature yet would be picked up
// by the next rebuild, making the capture list depend on whether some unrelated
// event happened to invalidate the cache. Worse, a name that is not an output of
// the capture stage fails the composite's OWN link, and a failed composite makes
// every draw through the pipeline report INVALID_OPERATION. Reading the LINKED
// snapshot removes the whole class: linked state only moves at a link, and a link
// is exactly what ComputeDrawProgramSignature's per-stage link version tracks, so
// the existing cache key is sufficient by construction.
// GetTransformFeedbackInterfaceNames() is the right accessor rather than the
// resolved xfbVaryings: it is the request as that link consumed it, pseudo-varyings
// (gl_NextBuffer / gl_SkipComponentsN) included, which is what re-issuing it needs.
//
// (2) THE FIRST STAGE THAT EXISTS, not the first with something to capture. This is
// the rule ProgramLinkTask::ResolveTransformFeedbackVaryings applies (it breaks on
// getIntermediate(stage) != nullptr), and the two MUST agree: this loop picks
// WHOSE list, the link task picks WHICH stage's outputs the names resolve against.
// Skipping a geometry stage that has no capture list and installing the vertex
// stage's instead made them disagree, and the composite then resolved a vertex
// program's names against the geometry intermediate - capturing where GL says it
// must not, or failing the link and killing every draw. A capture stage with an
// empty list is not a reason to look further down: it is the answer, and
// glBeginTransformFeedback's INVALID_OPERATION is the correct consequence.
for (const ShaderStage captureStage:
{ShaderStage::Geometry, ShaderStage::TessEval, ShaderStage::Vertex}) {
if (!compositeHasStage[static_cast<SizeT>(captureStage)]) continue;
const auto& captureProgram = pipeline->GetStageProgram(captureStage);
if (!captureProgram) continue;
const auto& requested = captureProgram->GetRequestedTransformFeedbackVaryings();
if (requested.empty()) continue;
composite->SetTransformFeedbackVaryings(Vector<String>(requested),
captureProgram->GetRequestedTransformFeedbackBufferMode());
const auto& linkedNames = captureProgram->GetTransformFeedbackInterfaceNames();
if (!linkedNames.empty()) {
composite->SetTransformFeedbackVaryings(Vector<String>(linkedNames),
captureProgram->GetTransformFeedbackBufferMode());
}
break;
}
// A pipeline with no fragment stage still rasterises, so the default fragment
@@ -540,6 +540,33 @@ namespace MobileGL::MG_State::GLState {
task->in.explicitFragDataIndex = m_explicitFragDataIndex;
task->in.requestedXfbVaryings = m_requestedXfbVaryings;
task->in.requestedXfbBufferMode = m_requestedXfbBufferMode;
// ARB_gl_spirv: a program built from SPIR-V declares its transform feedback through
// XfbBuffer/XfbStride/Offset DECORATIONS, and glTransformFeedbackVaryings has no effect on
// it at all. glSpecializeShader translated those decorations into the equivalent name
// request (ShaderCompiler::SpecializeAndDecompileSpirvModule), and this is where it enters
// the link - so everything downstream, the frontend packer and both backends, sees one
// declaration form instead of two.
//
// The capture stage is the LAST vertex-processing stage the program has, which is the same
// rule ProgramLinkTask::ResolveTransformFeedbackVaryings resolves the names against. The
// application's own request wins if it made one: that can only happen on a mixed program,
// which is not a shape ARB_gl_spirv defines, and honouring what the application explicitly
// asked for is the safer of the two readings.
if (task->in.requestedXfbVaryings.empty()) {
for (const ShaderStage captureStage:
{ShaderStage::Geometry, ShaderStage::TessEval, ShaderStage::Vertex}) {
Bool stagePresent = false;
for (const auto& shader : m_shaders) {
if (!shader || shader->GetShaderStage() != captureStage) continue;
stagePresent = true;
if (shader->GetSpirvXfbVaryings().empty()) continue;
task->in.requestedXfbVaryings = shader->GetSpirvXfbVaryings();
task->in.requestedXfbBufferMode = shader->GetSpirvXfbBufferMode();
break;
}
if (stagePresent) break;
}
}
task->in.maxFragmentOutputColorNumber = m_maxFragmentOutputColorNumber;
Vector<SharedPtr<ShaderCompileTask>> deps;
@@ -1536,14 +1536,14 @@ namespace MobileGL::MG_State::GLState {
m_requestedXfbVaryings = Move(names);
m_requestedXfbBufferMode = bufferMode;
}
// The REQUEST, not the linked result: what glTransformFeedbackVaryings last recorded,
// which the next link will try to resolve. A program pipeline's draw composite reads it
// off the capturing stage program and re-issues it on itself, because the composite is
// built from the stage programs' SHADERS and would otherwise inherit no capture list at
// all - which made glBeginTransformFeedback reject every separable-program capture
// (glcSeparableProgramsTransformFeedbackTests).
const Vector<String>& GetRequestedTransformFeedbackVaryings() const { return m_requestedXfbVaryings; }
GLenum GetRequestedTransformFeedbackBufferMode() const { return m_requestedXfbBufferMode; }
// NO ACCESSOR FOR THE PENDING REQUEST, deliberately. A program pipeline's draw composite
// needs the capture list of the stage program it flattens, and the obvious source - what
// glTransformFeedbackVaryings last recorded - is the wrong one: that request does not take
// effect until the stage program's next link, and it bumps no version, so reading it makes
// the composite's capture list depend on when the composite cache happened to be
// invalidated. GetTransformFeedbackInterfaceNames() below is the source that is correct
// AND cache-safe, because linked state only moves at a link and the composite signature
// already keys on the link version. See GLContext::GetProgramForDraw.
GLenum GetTransformFeedbackBufferMode() const { return Artifacts().xfbBufferMode; }
SizeT GetTransformFeedbackVaryingCount() const { return Artifacts().xfbVaryings.size(); }
const XfbVarying* GetTransformFeedbackVarying(SizeT index) const {
@@ -21,16 +21,31 @@ namespace MobileGL::MG_State::GLState {
ReleaseCompileNode();
m_spirvBinary = Move(binary);
m_hasSpirvBinary = true;
m_specialized = false;
m_specializationFailed = false;
m_specializationInfoLog.clear();
m_spirvXfbVaryings.clear();
m_spirvXfbBufferMode = GL_INTERLEAVED_ATTRIBS;
m_source = MakeShared<const String>(String{});
InvalidateCompiledState();
}
void ShaderObject::SpecializeFromSpirv(String&& glsl) {
const String& ShaderObject::GetApplicationShaderSource() const {
static const String kNoSource;
// Both the unspecialized and the specialized windows answer empty: in the first m_source
// already is empty, in the second it holds generated GLSL that the application never wrote.
return m_hasSpirvBinary ? kNoSource : *m_source;
}
void ShaderObject::SpecializeFromSpirv(String&& glsl, Vector<String>&& xfbVaryings, GLenum xfbBufferMode) {
ReleaseCompileNode();
// The latch goes up HERE and nowhere else - this is the one path that actually specialized
// the shader.
m_specialized = true;
m_specializationFailed = false;
m_specializationInfoLog.clear();
m_spirvXfbVaryings = Move(xfbVaryings);
m_spirvXfbBufferMode = xfbBufferMode;
// The GLSL the module specializes to enters the ORDINARY pipeline from here: preprocess,
// glslang parse, reflection, transpile, both backends. Nothing downstream needs to know
// the source was not written by the application - which is the whole reason this hop
@@ -61,8 +76,11 @@ namespace MobileGL::MG_State::GLState {
m_hasSpirvBinary = false;
m_spirvBinary.clear();
m_spirvBinary.shrink_to_fit();
m_specialized = false;
m_specializationFailed = false;
m_specializationInfoLog.clear();
m_spirvXfbVaryings.clear();
m_spirvXfbBufferMode = GL_INTERLEAVED_ATTRIBS;
ReleaseCompileNode();
m_source = MakeShared<const String>(source);
InvalidateCompiledState();
@@ -78,10 +78,33 @@ namespace MobileGL {
// checks explicitly.
void SetSpirvBinary(Vector<Uint32>&& binary);
Bool HasSpirvBinary() const { return m_hasSpirvBinary; }
// ARB_gl_spirv: "Once specialized, a shader may not be re-specialized without first
// re-associating the original SPIR-V module with it, through ShaderBinary." A second
// glSpecializeShader is GL_INVALID_OPERATION, and this latch is what answers that.
//
// Set ONLY on the success path. A specialization that FAILED did not specialize the
// shader, and the conformance suite relies on that distinction: it deliberately fails
// specialization (a bad entry point, then an unknown constant id) on one shader object
// and then requires the next, well-formed call on that same object to be accepted.
Bool HasBeenSpecialized() const { return m_specialized; }
const Vector<Uint32>& GetSpirvBinary() const { return m_spirvBinary; }
// glSpecializeShader's half: hand the object the GLSL its module specializes to and
// let the ordinary pipeline compile it.
void SpecializeFromSpirv(String&& glsl);
void SpecializeFromSpirv(String&& glsl, Vector<String>&& xfbVaryings, GLenum xfbBufferMode);
// The capture the object's SPIR-V module DECLARED, as the equivalent
// glTransformFeedbackVaryings request. Empty for a GLSL shader and for a SPIR-V module
// that declares no transform feedback. ProgramObject::Link picks this up from the
// program's last vertex-processing stage, because ARB_gl_spirv makes decorations the
// only declaration form for a SPIR-V program and glTransformFeedbackVaryings has no
// effect on one.
const Vector<String>& GetSpirvXfbVaryings() const { return m_spirvXfbVaryings; }
GLenum GetSpirvXfbBufferMode() const { return m_spirvXfbBufferMode; }
// What glGetShaderSource / GL_SHADER_SOURCE_LENGTH must answer. A shader created from
// glShaderBinary never had glShaderSource called on it, so GL 4.6 core 7.1 makes its
// source the empty string - even after glSpecializeShader, when m_source holds the
// SPIRV-Cross GLSL the module was translated into. That text is MobileGL's, not the
// application's, and handing it back invites an application to cache and re-submit it.
const String& GetApplicationShaderSource() const;
// The other half: specialization itself failed (a bad entry point, a constant id the
// module does not declare, a module spirv-val rejects). There is nothing to compile,
// so the verdict is recorded directly - COMPILE_STATUS false with this log - and both
@@ -278,6 +301,13 @@ namespace MobileGL {
// the ORIGINAL words rather than the ones the first call folded.
Vector<Uint32> m_spirvBinary;
Bool m_hasSpirvBinary = false;
// "This shader has been specialized"; see HasBeenSpecialized. Cleared by anything that
// re-associates a module (SetSpirvBinary) or turns the object back into a GLSL shader
// (either SetShaderSource overload) - which is exactly the re-association ARB_gl_spirv
// names as the way to make a second specialization legal again.
Bool m_specialized = false;
Vector<String> m_spirvXfbVaryings;
GLenum m_spirvXfbBufferMode = GL_INTERLEAVED_ATTRIBS;
// A specialization that failed before any compile could start. Kept beside the
// compile artifacts rather than inside them because there is no compile job to hang
// it on - see RecordSpecializationFailure. Cleared by anything that gives the object
@@ -248,11 +248,12 @@ namespace MobileGL {
}
void RenderState::SetPolygonOffset(Float factor, Float units) {
if (m_parameters.PolygonOffsetFactor == factor && m_parameters.PolygonOffsetUnits == units) return;
m_parameters.PolygonOffsetFactor = factor;
m_parameters.PolygonOffsetUnits = units;
++m_version;
// GL 4.6 core 14.6.5 defines PolygonOffset(factor, units) as EQUIVALENT to
// PolygonOffsetClamp(factor, units, 0) - the equivalence is total, so the clamp is
// written too, not merely left alone. Leaving it meant a glPolygonOffsetClamp(1, 1,
// 0.5) followed by a plain glPolygonOffset(3, 4) still reported a clamp of 0.5, and
// the early-out below could even skip the version bump while doing it.
SetPolygonOffsetClamped(factor, units, 0.0f);
}
Float RenderState::GetPolygonOffsetFactor() const {
+10 -7
View File
@@ -3072,14 +3072,19 @@ TEST(GetterSanity, CombinedUniformComponentsSaturateInsteadOfOverflowing) {
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
// GL_MAX_COMBINED_COMPUTE_UNIFORM_COMPONENTS (0x8266), NOT the per-stage
// GL_MAX_COMPUTE_UNIFORM_COMPONENTS (0x8263) this list used to name. The per-stage token is
// answered by a frontend constant and never reaches GetMaxCombinedUniformComponents at all, so
// both assertions on it were vacuous - and it displaced the ONE reader whose block count comes
// from the backend (ClampUniformBlockCount(dynamicParameters.MaxComputeUniformBlocks)) rather
// than from a frontend constant, i.e. the only call site where the saturation actually depends
// on data a driver supplies.
static constexpr GLenum kCombinedPnames[] = {
GL_MAX_COMBINED_VERTEX_UNIFORM_COMPONENTS, GL_MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS,
GL_MAX_COMBINED_GEOMETRY_UNIFORM_COMPONENTS, GL_MAX_COMBINED_TESS_CONTROL_UNIFORM_COMPONENTS,
GL_MAX_COMBINED_TESS_EVALUATION_UNIFORM_COMPONENTS, GL_MAX_COMPUTE_UNIFORM_COMPONENTS,
GL_MAX_COMBINED_TESS_EVALUATION_UNIFORM_COMPONENTS, GL_MAX_COMBINED_COMPUTE_UNIFORM_COMPONENTS,
};
// The GL 4.6 core table 23.64 floors for the five combined pnames above; compute's per-stage
// GL_MAX_COMPUTE_UNIFORM_COMPONENTS is not a combined limit and carries its own, much smaller
// floor, so only the sign of its answer is asserted.
// The GL 4.6 core table 23.64 floor, which all six combined pnames carry.
static constexpr GLint kCombinedFloor = 58368;
{
@@ -3091,9 +3096,7 @@ TEST(GetterSanity, CombinedUniformComponentsSaturateInsteadOfOverflowing) {
GLint reported = 0;
MG_Impl::GLImpl::GetIntegerv(pname, &reported);
EXPECT_GT(reported, 0) << "pname 0x" << pname << " wrapped to a negative combined component count";
if (pname != GL_MAX_COMPUTE_UNIFORM_COMPONENTS) {
EXPECT_GE(reported, kCombinedFloor) << "pname 0x" << pname << " fell under the GL 4.6 floor";
}
EXPECT_GE(reported, kCombinedFloor) << "pname 0x" << pname << " fell under the GL 4.6 floor";
}
MG_Backend::pActiveBackendObject.reset();
}
+14 -4
View File
@@ -980,15 +980,25 @@ TEST_F(RenderStateTest, PolygonOffsetClampStoresTheClampAndTheFactorUnitsPair) {
EXPECT_NEAR(asDouble, 0.5, 1e-6);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// glPolygonOffset is the clamp = 0 case of the same state, but it must not DISTURB the clamp
// it does not take - GL 4.6 core 14.6.5 defines it as PolygonOffsetClamp(factor, units, 0)
// only in the sense that the clamp it leaves is whatever glPolygonOffset itself sets, which
// for MobileGL is "unchanged". Assert the factor/units half instead, which is unambiguous.
// GL 4.6 core 14.6.5 defines glPolygonOffset(factor, units) as EQUIVALENT to
// glPolygonOffsetClamp(factor, units, 0) - totally, not "except for the clamp". So it writes
// all three, and a clamp left over from an earlier glPolygonOffsetClamp must be gone.
MG_Impl::GLImpl::PolygonOffset(3.0f, 4.0f);
MG_Impl::GLImpl::GetFloatv(GL_POLYGON_OFFSET_FACTOR, &factor);
MG_Impl::GLImpl::GetFloatv(GL_POLYGON_OFFSET_UNITS, &units);
MG_Impl::GLImpl::GetFloatv(GL_POLYGON_OFFSET_CLAMP, &clamp);
EXPECT_FLOAT_EQ(factor, 3.0f);
EXPECT_FLOAT_EQ(units, 4.0f);
EXPECT_FLOAT_EQ(clamp, 0.0f) << "glPolygonOffset IS PolygonOffsetClamp(factor, units, 0)";
// The same rule when factor and units do NOT change: the clamp still has to be cleared, which
// an early-out keyed on the factor/units pair alone would skip.
MG_Impl::GLImpl::PolygonOffsetClamp(3.0f, 4.0f, 0.75f);
MG_Impl::GLImpl::GetFloatv(GL_POLYGON_OFFSET_CLAMP, &clamp);
ASSERT_FLOAT_EQ(clamp, 0.75f);
MG_Impl::GLImpl::PolygonOffset(3.0f, 4.0f);
MG_Impl::GLImpl::GetFloatv(GL_POLYGON_OFFSET_CLAMP, &clamp);
EXPECT_FLOAT_EQ(clamp, 0.0f) << "a no-op factor/units write must still clear the clamp";
MG_Impl::GLImpl::PolygonOffsetClamp(0.0f, 0.0f, 0.0f);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
+229
View File
@@ -5775,3 +5775,232 @@ TEST_F(TextureTest, TextureBufferKeepsInvalidOperationForANonBufferTexture) {
MG_Impl::GLImpl::DeleteBuffers(1, &buffer);
DrainPendingGlErrors();
}
// ---------------------------------------------------------------------------------------------
// The destination box of a copy is bounded by the LEVEL it writes, not by level 0.
//
// glCopyTexSubImage3D/1D validated through ValidateTextureSubImageOffsets, whose bound is
// ITextureObject::GetBaseSize() - hardcoded to level 0 - while CopyReadFramebufferIntoMipmapRegion
// indexes GetMipmapTexelSize(uploadTarget, level) and memcpys into the exact-sized allocation
// MipmapStorage made for that level. A box legal at level 0 and out of range at level N wrote past
// the end of the heap buffer. Both entry points were `// TODO: implement` no-ops before this
// branch, so implementing them is what opened the path.
//
// The region check runs BEFORE the read-framebuffer check on purpose, which is what lets this
// GPU-free binary assert it: no complete read FBO is needed to prove the box was rejected.
// ---------------------------------------------------------------------------------------------
TEST_F(TextureTest, CopyTexSubImage3DBoundsTheDestinationByTheRequestedLevelNotLevelZero) {
GLuint texture = 0;
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D_ARRAY, 1, &texture);
// 4 levels of an 8x8x4 array: level 0 is 8x8, level 1 4x4, level 2 2x2; the layer count stays
// 4 at every level.
MG_Impl::GLImpl::TextureStorage3D(texture, 4, GL_RGBA8, 8, 8, 4);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_ARRAY, texture);
DrainPendingGlErrors();
// THE OVERFLOW: 4+4 <= 8 and 4+4 <= 8 against level 0, but level 2 is only 2x2. This used to
// pass validation and write 24 bytes past a 64-byte allocation.
MG_Impl::GLImpl::CopyTexSubImage3D(GL_TEXTURE_2D_ARRAY, 2, 4, 4, 0, 0, 0, 4, 4);
ExpectSingleGlError(GL_INVALID_VALUE);
// The same box one axis at a time, so a check that only looked at x or only at y cannot pass.
MG_Impl::GLImpl::CopyTexSubImage3D(GL_TEXTURE_2D_ARRAY, 1, 3, 0, 0, 0, 0, 2, 2);
ExpectSingleGlError(GL_INVALID_VALUE);
MG_Impl::GLImpl::CopyTexSubImage3D(GL_TEXTURE_2D_ARRAY, 1, 0, 3, 0, 0, 0, 2, 2);
ExpectSingleGlError(GL_INVALID_VALUE);
// A box that IS inside level 1 (4x4) must get past the region check. It cannot complete here -
// this binary has no complete read framebuffer - but it must not be the box that is refused.
MG_Impl::GLImpl::CopyTexSubImage3D(GL_TEXTURE_2D_ARRAY, 1, 2, 2, 3, 0, 0, 2, 2);
EXPECT_NE(MG_Impl::GLImpl::GetError(), GL_INVALID_VALUE)
<< "an in-range level-1 box must reach the framebuffer check, not be rejected as out of range";
DrainPendingGlErrors();
// The layer axis is bounded by the level's layer count, which does NOT shrink down the chain.
MG_Impl::GLImpl::CopyTexSubImage3D(GL_TEXTURE_2D_ARRAY, 1, 0, 0, 4, 0, 0, 2, 2);
ExpectSingleGlError(GL_INVALID_VALUE);
// A level the texture never had is INVALID_OPERATION, not a write into an empty allocation.
MG_Impl::GLImpl::CopyTexSubImage3D(GL_TEXTURE_2D_ARRAY, 5, 0, 0, 0, 0, 0, 1, 1);
ExpectSingleGlError(GL_INVALID_OPERATION);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_ARRAY, 0);
DrainPendingGlErrors();
}
TEST_F(TextureTest, CopyTexSubImage1DBoundsTheDestinationByTheRequestedLevel) {
GLuint texture = 0;
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_1D, 1, &texture);
MG_Impl::GLImpl::TextureStorage1D(texture, 4, GL_RGBA8, 8);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_1D, texture);
DrainPendingGlErrors();
// Level 2 is two texels, i.e. eight bytes; this used to write sixteen bytes starting sixteen
// bytes in - entirely outside the allocation.
MG_Impl::GLImpl::CopyTexSubImage1D(GL_TEXTURE_1D, 2, 4, 0, 0, 4);
ExpectSingleGlError(GL_INVALID_VALUE);
// In range at level 1 (four texels).
MG_Impl::GLImpl::CopyTexSubImage1D(GL_TEXTURE_1D, 1, 2, 0, 0, 2);
EXPECT_NE(MG_Impl::GLImpl::GetError(), GL_INVALID_VALUE);
DrainPendingGlErrors();
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_1D, 0);
DrainPendingGlErrors();
}
// glCopyTextureSubImage3D documents zoffset as the cube-map FACE selector, but the face mapping
// ran AFTER a z-bounds check taken from GetBaseSize().z(), which for a cube map is one face's
// depth - i.e. 1. Every zoffset in 1..5 was rejected with GL_INVALID_VALUE, so five of the six
// faces were unreachable. The mapping now runs first and is bounded by the face count.
TEST_F(TextureTest, CopyTextureSubImage3DCanAddressEveryCubeMapFace) {
GLuint texture = 0;
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_CUBE_MAP, 1, &texture);
MG_Impl::GLImpl::TextureStorage2D(texture, 2, GL_RGBA8, 4, 4);
DrainPendingGlErrors();
for (GLint face = 0; face < 6; ++face) {
MG_Impl::GLImpl::CopyTextureSubImage3D(texture, 0, 0, 0, face, 0, 0, 4, 4);
EXPECT_NE(MG_Impl::GLImpl::GetError(), GL_INVALID_VALUE)
<< "face " << face << " must be reachable; the z bound is the face count, not a face's depth";
DrainPendingGlErrors();
}
// Past the last face is still GL_INVALID_VALUE.
MG_Impl::GLImpl::CopyTextureSubImage3D(texture, 0, 0, 0, 6, 0, 0, 4, 4);
ExpectSingleGlError(GL_INVALID_VALUE);
MG_Impl::GLImpl::CopyTextureSubImage3D(texture, 0, 0, 0, -1, 0, 0, 4, 4);
ExpectSingleGlError(GL_INVALID_VALUE);
// And the per-FACE extent still bounds x/y at the requested level: level 1 of a 4x4 cube is
// 2x2, so a 4x4 box into face 3 is out of range even though it fits level 0.
MG_Impl::GLImpl::CopyTextureSubImage3D(texture, 1, 0, 0, 3, 0, 0, 4, 4);
ExpectSingleGlError(GL_INVALID_VALUE);
DrainPendingGlErrors();
}
// ---------------------------------------------------------------------------------------------
// glGenerateMipmap allocates the chain in the FRONTEND before it dispatches to the backend, and
// that allocator used a two-way "does depth mip?" flag which had no way to say that a 1D array's
// HEIGHT is its layer count. It therefore both counted the layer axis into the chain length and
// halved it per level. The backend allocator cannot repair that - it only ever GROWS a chain, and
// the frontend's (wrong) count is always the longer one - so the layer-shrinking chain survived on
// both backends, and ComputeMipmapCompleteForFilter (which knows height is not a dimension for
// this target) then judged the texture mipmap-INCOMPLETE, i.e. sampling returns (0,0,0,1).
// ---------------------------------------------------------------------------------------------
namespace {
// glGenerateMipmap dispatches to the backend after the frontend allocation; this binary has no
// GL context, so the hook is stubbed for the duration of the case. What is under test is the
// frontend allocation the stub cannot influence.
struct ScopedNoOpGenerateMipmap {
ScopedNoOpGenerateMipmap(): m_snapshot(MobileGL::MG_Backend::gBackendFunctionsTable) {
MobileGL::MG_Backend::gBackendFunctionsTable.GL.GenerateMipmap = [](GLenum) {};
}
~ScopedNoOpGenerateMipmap() { MobileGL::MG_Backend::gBackendFunctionsTable = m_snapshot; }
ScopedNoOpGenerateMipmap(const ScopedNoOpGenerateMipmap&) = delete;
ScopedNoOpGenerateMipmap& operator=(const ScopedNoOpGenerateMipmap&) = delete;
private:
MobileGL::MG_Backend::GlobalBackendFunctionsTable m_snapshot;
};
GLint LevelParam(GLenum target, GLint level, GLenum pname) {
GLint value = -1;
MG_Impl::GLImpl::GetTexLevelParameteriv(target, level, pname, &value);
return value;
}
} // namespace
TEST_F(TextureTest, GenerateMipmapKeepsA1DArrayLayerCountAtEveryLevel) {
ScopedNoOpGenerateMipmap noOpBackend;
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_1D_ARRAY, texture);
// Width 8, FOUR layers. The layer count is carried in `height` for this target.
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_1D_ARRAY, 0, GL_RGBA8, 8, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
DrainPendingGlErrors();
MG_Impl::GLImpl::GenerateMipmap(GL_TEXTURE_1D_ARRAY);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// The chain length comes from the WIDTH alone: 8 -> 4 -> 2 -> 1 is four levels. Counting the
// layer axis too would give the same four here, so the width is chosen larger than the layer
// count on purpose and the layer assertions below are what actually discriminate.
for (GLint level = 0; level < 4; ++level) {
EXPECT_EQ(LevelParam(GL_TEXTURE_1D_ARRAY, level, GL_TEXTURE_WIDTH), std::max(8 >> level, 1))
<< "level " << level << " width";
EXPECT_EQ(LevelParam(GL_TEXTURE_1D_ARRAY, level, GL_TEXTURE_HEIGHT), 4)
<< "level " << level << " must keep all four layers; height is the layer count for a 1D array";
}
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_1D_ARRAY, 0);
DrainPendingGlErrors();
}
// The mirror case: more layers than texels. The chain must be as long as the WIDTH admits, not as
// long as the layer count admits - a chain sized off the layers would allocate levels whose width
// has already bottomed out at 1 while the layer count kept halving.
TEST_F(TextureTest, GenerateMipmapSizesA1DArrayChainFromWidthAloneEvenWithMoreLayersThanTexels) {
ScopedNoOpGenerateMipmap noOpBackend;
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_1D_ARRAY, texture);
// Width 2, sixteen layers: counting the layer axis would ask for five levels, the width for two.
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_1D_ARRAY, 0, GL_RGBA8, 2, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
DrainPendingGlErrors();
MG_Impl::GLImpl::GenerateMipmap(GL_TEXTURE_1D_ARRAY);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
EXPECT_EQ(LevelParam(GL_TEXTURE_1D_ARRAY, 1, GL_TEXTURE_WIDTH), 1);
EXPECT_EQ(LevelParam(GL_TEXTURE_1D_ARRAY, 1, GL_TEXTURE_HEIGHT), 16);
// Level 2 must not exist: the chain ends where the width does.
EXPECT_EQ(LevelParam(GL_TEXTURE_1D_ARRAY, 2, GL_TEXTURE_WIDTH), 0);
DrainPendingGlErrors();
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_1D_ARRAY, 0);
DrainPendingGlErrors();
}
// The 2D-array/cube-array side of the same rule, so a fix that swung the other way (making depth
// mip-able again) cannot pass. Depth is the layer count for these; only width and height reduce.
TEST_F(TextureTest, GenerateMipmapKeepsA2DArrayLayerCountAtEveryLevel) {
ScopedNoOpGenerateMipmap noOpBackend;
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_ARRAY, texture);
MG_Impl::GLImpl::TexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA8, 8, 8, 3, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
DrainPendingGlErrors();
MG_Impl::GLImpl::GenerateMipmap(GL_TEXTURE_2D_ARRAY);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
for (GLint level = 0; level < 4; ++level) {
EXPECT_EQ(LevelParam(GL_TEXTURE_2D_ARRAY, level, GL_TEXTURE_WIDTH), std::max(8 >> level, 1));
EXPECT_EQ(LevelParam(GL_TEXTURE_2D_ARRAY, level, GL_TEXTURE_HEIGHT), std::max(8 >> level, 1));
EXPECT_EQ(LevelParam(GL_TEXTURE_2D_ARRAY, level, GL_TEXTURE_DEPTH), 3)
<< "level " << level << " must keep all three layers";
}
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// And a true 3D texture still halves all three, which is the case the layer rule must not eat.
GLuint volume = 0;
MG_Impl::GLImpl::GenTextures(1, &volume);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_3D, volume);
MG_Impl::GLImpl::TexImage3D(GL_TEXTURE_3D, 0, GL_RGBA8, 8, 8, 8, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
DrainPendingGlErrors();
MG_Impl::GLImpl::GenerateMipmap(GL_TEXTURE_3D);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
EXPECT_EQ(LevelParam(GL_TEXTURE_3D, 1, GL_TEXTURE_DEPTH), 4);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_3D, 0);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_ARRAY, 0);
DrainPendingGlErrors();
}
@@ -1702,6 +1702,54 @@ namespace MobileGL {
return SpvExecutionModelFragment;
}
}
// The decorated capture layout, as the equivalent glTransformFeedbackVaryings
// request. GL 4.6 core 11.1.2.1 / ARB_transform_feedback3 give the name list two
// pseudo-varyings that are exactly what a decoration layout needs: gl_NextBuffer
// moves to the next capture buffer, and gl_SkipComponentsN (N in 1..4) advances the
// cursor without capturing. Together they can express any offset/stride layout
// whose offsets are component-aligned, which SPIR-V's are (Offset is in bytes and
// xfb offsets are four-byte aligned by rule).
Vector<String> BuildXfbVaryingRequest(const Vector<SpirvXfbCapture>& captures) {
Vector<String> names;
if (captures.empty()) return names;
auto emitSkip = [&names](Uint32 components) {
while (components > 0) {
const Uint32 step = std::min<Uint32>(components, 4);
names.push_back("gl_SkipComponents" + std::to_string(step));
components -= step;
}
};
Uint32 currentBuffer = captures.front().buffer;
Uint32 cursorComponents = 0;
Uint32 currentStride = 0;
// Buffers below the first captured one still have to be stepped over, so the
// Nth gl_NextBuffer really does land on buffer N.
for (Uint32 buffer = 0; buffer < currentBuffer; ++buffer) {
names.push_back("gl_NextBuffer");
}
for (const SpirvXfbCapture& capture : captures) {
if (capture.buffer != currentBuffer) {
// Pad the buffer being left out to its declared stride, so the record
// size the module asked for survives.
if (currentStride / 4 > cursorComponents) emitSkip(currentStride / 4 - cursorComponents);
for (Uint32 buffer = currentBuffer; buffer < capture.buffer; ++buffer) {
names.push_back("gl_NextBuffer");
}
currentBuffer = capture.buffer;
cursorComponents = 0;
currentStride = 0;
}
const Uint32 offsetComponents = capture.offset / 4;
if (offsetComponents > cursorComponents) emitSkip(offsetComponents - cursorComponents);
names.push_back(capture.name);
cursorComponents = offsetComponents + capture.componentCount;
currentStride = std::max(currentStride, capture.stride);
}
if (currentStride / 4 > cursorComponents) emitSkip(currentStride / 4 - cursorComponents);
return names;
}
} // namespace
Result<void> ShaderCompiler::ValidateSpirvModule(const Vector<Uint32>& spirv) {
@@ -1734,15 +1782,29 @@ namespace MobileGL {
return {};
}
Result<String> ShaderCompiler::SpecializeAndDecompileSpirvModule(const Vector<Uint32>& spirv,
GLenum shaderType,
const String& entryPoint,
const Vector<Uint32>& constantIds,
const Vector<Uint32>& constantValues) {
Result<ShaderCompiler::SpecializedModule> ShaderCompiler::SpecializeAndDecompileSpirvModule(
const Vector<Uint32>& spirv, GLenum shaderType, const String& entryPoint,
const Vector<Uint32>& constantIds, const Vector<Uint32>& constantValues,
SpecializationFailure& outFailure) {
outFailure = SpecializationFailure::None;
SpvcSession session(spirv, SessionUsageBit::Transpile);
if (!session.IsTranspileReady()) {
// SPIRV-Cross could not parse the module. glShaderBinary's spirv-val pass is a
// validity check, not a parseability one, so this is reachable with a module
// that validates - hence a diagnosis rather than the null dereference the
// unchecked constructor used to walk into.
outFailure = SpecializationFailure::ModuleRejected;
ResultInfo r;
r.errc = -11;
r.log = "Error: [ARB_gl_spirv] the module could not be parsed:\n" +
String(session.GetLastErrorString());
return std::unexpected(r);
}
Uint32 unknownConstantId = 0;
if (!session.SetSpecializationConstants(constantIds, constantValues, unknownConstantId)) {
outFailure = SpecializationFailure::UnknownConstantId;
ResultInfo r;
r.errc = -7;
r.log = "Error: [ARB_gl_spirv] constant index " + std::to_string(unknownConstantId) +
@@ -1750,19 +1812,32 @@ namespace MobileGL {
return std::unexpected(r);
}
if (!entryPoint.empty()) {
if (session.SetEntryPoint(entryPoint.c_str(), ExecutionModelForShaderType(shaderType)) !=
SPVC_SUCCESS) {
ResultInfo r;
r.errc = -8;
r.log = "Error: [ARB_gl_spirv] the module has no entry point named '" + entryPoint +
"' for this shader stage:\n" + String(session.GetLastErrorString());
return std::unexpected(r);
}
// No `if (!entryPoint.empty())` guard any more. ARB_gl_spirv makes pEntryPoint the
// name of the entry point to specialize, and no module carries one named ""; the
// guard turned an empty name into "whichever entry point happens to be default",
// which is neither what the application asked for nor an error it was told about.
if (session.SetEntryPoint(entryPoint.c_str(), ExecutionModelForShaderType(shaderType)) !=
SPVC_SUCCESS) {
outFailure = SpecializationFailure::UnknownEntryPoint;
ResultInfo r;
r.errc = -8;
r.log = "Error: [ARB_gl_spirv] the module has no entry point named '" + entryPoint +
"' for this shader stage:\n" + String(session.GetLastErrorString());
return std::unexpected(r);
}
// Read the declared capture layout, then REMOVE the decorations that describe it.
// Both halves matter: without the read a SPIR-V program captures nothing, and
// without the strip the decorations round-trip through the emitted GLSL back into
// the regenerated SPIR-V, where DirectGLES's ESSL hop refuses them outright and
// loses the stage. See SpvcSession::StripTransformFeedbackDecorations.
SpecializedModule specialized;
specialized.xfbVaryings = BuildXfbVaryingRequest(session.ReflectTransformFeedbackCaptures());
session.StripTransformFeedbackDecorations();
spvc_compiler_options options;
if (session.CreateOptions(&options) != SPVC_SUCCESS) {
outFailure = SpecializationFailure::ModuleRejected;
ResultInfo r;
r.errc = -9;
r.log = "Error: [ARB_gl_spirv] could not create SPIRV-Cross options for the module.";
@@ -1786,13 +1861,15 @@ namespace MobileGL {
const char* emitted = nullptr;
session.Compile(&emitted);
if (!emitted) {
outFailure = SpecializationFailure::ModuleRejected;
ResultInfo r;
r.errc = -10;
r.log = "Error: [ARB_gl_spirv] could not translate the module to GLSL:\n" +
String(session.GetLastErrorString());
return std::unexpected(r);
}
return String(emitted);
specialized.glsl = String(emitted);
return specialized;
}
Result<String> ShaderCompiler::DecompileShader(SpvcSession& session) {
@@ -434,10 +434,43 @@ namespace MobileGL {
// `constantIds` and `constantValues` are the parallel arrays the entry point
// takes. A constant id the module does not declare is GL_INVALID_VALUE per the
// extension; it is reported through the error log rather than silently ignored.
static Result<String> SpecializeAndDecompileSpirvModule(const Vector<Uint32>& spirv,
GLenum shaderType, const String& entryPoint,
const Vector<Uint32>& constantIds,
const Vector<Uint32>& constantValues);
// Why the caller needs a REASON and not just a failure: ARB_gl_spirv splits the
// ways specialization can fail into two groups with different GL surfaces. A bad
// entry-point name and a constant id the module does not declare are enumerated
// errors - GL_INVALID_VALUE, and, being errors, they must leave the shader object
// exactly as it was. Everything else (a module SPIRV-Cross cannot translate) is a
// COMPILE failure, reported through COMPILE_STATUS and the info log like any other
// glCompileShader outcome. Returning one undifferentiated error is what made both
// groups look like the second.
enum class SpecializationFailure {
None,
UnknownConstantId, // GL_INVALID_VALUE
UnknownEntryPoint, // GL_INVALID_VALUE
ModuleRejected, // COMPILE_STATUS false + info log
};
// What a specialized module turns into: the GLSL the ordinary pipeline compiles,
// plus the transform-feedback capture the module DECLARED, re-expressed as the
// glTransformFeedbackVaryings request that produces the same layout.
//
// The re-expression is the whole design. ARB_gl_spirv makes XfbBuffer/XfbStride/
// Offset decorations the only way a SPIR-V program declares capture, and MobileGL's
// capture machinery - the frontend packer, DirectGLES's forwarding to the ES
// driver, DirectVulkan's XfbCaptureDecoratePass - is driven entirely by a name
// list. Translating the decorations into the equivalent name list (with
// ARB_transform_feedback3's gl_NextBuffer / gl_SkipComponentsN spelling carrying
// the buffer breaks and the gaps) hands a SPIR-V program to the machinery that
// already exists, instead of teaching every consumer a second declaration form.
struct SpecializedModule {
String glsl;
Vector<String> xfbVaryings;
GLenum xfbBufferMode = GL_INTERLEAVED_ATTRIBS;
};
static Result<SpecializedModule> SpecializeAndDecompileSpirvModule(
const Vector<Uint32>& spirv, GLenum shaderType, const String& entryPoint,
const Vector<Uint32>& constantIds, const Vector<Uint32>& constantValues,
SpecializationFailure& outFailure);
// spirv-val over an application-supplied module, against the environment MobileGL
// parses and emits under. glShaderBinary is where a malformed module has to be
@@ -8,6 +8,8 @@
#include "SpvcSession.h"
#include <algorithm>
namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
@@ -184,11 +186,29 @@ namespace MobileGL {
const SpvId* p_spirv = spirv.data();
size_t word_count = spirv.size();
spvc_context_create(&context);
spvc_context_parse_spirv(context, p_spirv, word_count, &ir);
spvc_context_create_compiler(context, SPVC_BACKEND_GLSL, ir, SPVC_CAPTURE_MODE_TAKE_OWNERSHIP,
&compiler);
spvc_compiler_create_shader_resources(compiler, &resources);
// Every step is checked, and each guards the next: the C API writes its
// out-param only on success, so passing a failed step's null handle to the
// step after it is a raw dereference (spvc_context_create_compiler does
// `parsed_ir->parsed`, spvc_compiler_create_shader_resources does
// `compiler->context`). IsTranspileReady() is how a caller asks whether this
// sequence got all the way through.
if (spvc_context_create(&context) != SPVC_SUCCESS) {
context = nullptr;
return;
}
if (spvc_context_parse_spirv(context, p_spirv, word_count, &ir) != SPVC_SUCCESS) {
ir = nullptr;
return;
}
if (spvc_context_create_compiler(context, SPVC_BACKEND_GLSL, ir,
SPVC_CAPTURE_MODE_TAKE_OWNERSHIP, &compiler) != SPVC_SUCCESS) {
compiler = nullptr;
return;
}
if (spvc_compiler_create_shader_resources(compiler, &resources) != SPVC_SUCCESS) {
resources = nullptr;
return;
}
} else if (usage & SessionUsageBit::Reflection) {
SpvReflectResult result = spvReflectCreateShaderModule(
spirv.size() * sizeof(uint32_t), spirv.data(), &reflectModule);
@@ -496,8 +516,144 @@ namespace MobileGL {
SPVC_CHK_RETURN
}
namespace {
// How many 32-bit components a captured variable occupies, which is what the
// gl_SkipComponentsN padding below is counted in. Matrices and arrays multiply.
Uint32 XfbComponentCount(spvc_compiler compiler, spvc_type_id typeId) {
const spvc_type type = spvc_compiler_get_type_handle(compiler, typeId);
if (type == nullptr) return 0;
Uint32 components = spvc_type_get_vector_size(type) * spvc_type_get_columns(type);
const unsigned dimensions = spvc_type_get_num_array_dimensions(type);
for (unsigned d = 0; d < dimensions; ++d) {
const unsigned length = spvc_type_get_array_dimension(type, d);
if (length != 0) components *= length;
}
// A double occupies two component slots per scalar (GL 4.6 core 11.1.2.1).
const spvc_basetype base = spvc_type_get_basetype(type);
if (base == SPVC_BASETYPE_FP64 || base == SPVC_BASETYPE_INT64 ||
base == SPVC_BASETYPE_UINT64) {
components *= 2;
}
return components;
}
} // namespace
Vector<SpirvXfbCapture> SpvcSession::ReflectTransformFeedbackCaptures() const {
Vector<SpirvXfbCapture> captures;
if (compiler == nullptr || resources == nullptr) return captures;
const spvc_reflected_resource* outputs = nullptr;
SizeT outputCount = 0;
if (spvc_resources_get_resource_list_for_type(resources, SPVC_RESOURCE_TYPE_STAGE_OUTPUT, &outputs,
&outputCount) != SPVC_SUCCESS) {
return captures;
}
for (SizeT i = 0; i < outputCount; ++i) {
const spvc_reflected_resource& output = outputs[i];
// XfbBuffer/XfbStride sit on the VARIABLE; Offset sits on the variable for a
// plain output and on each MEMBER for a block (which is how a redeclared
// gl_PerVertex carries it).
const Bool hasBuffer =
spvc_compiler_has_decoration(compiler, output.id, SpvDecorationXfbBuffer) == SPVC_TRUE;
const Uint32 buffer =
hasBuffer ? spvc_compiler_get_decoration(compiler, output.id, SpvDecorationXfbBuffer) : 0u;
const Uint32 stride =
spvc_compiler_has_decoration(compiler, output.id, SpvDecorationXfbStride) == SPVC_TRUE
? spvc_compiler_get_decoration(compiler, output.id, SpvDecorationXfbStride)
: 0u;
const spvc_type type = spvc_compiler_get_type_handle(compiler, output.base_type_id);
const unsigned memberCount =
type != nullptr && spvc_type_get_basetype(type) == SPVC_BASETYPE_STRUCT
? spvc_type_get_num_member_types(type)
: 0u;
if (memberCount == 0) {
if (spvc_compiler_has_decoration(compiler, output.id, SpvDecorationOffset) != SPVC_TRUE) {
continue;
}
SpirvXfbCapture capture;
capture.name = output.name ? output.name : "";
capture.buffer = buffer;
capture.stride = stride;
capture.offset = spvc_compiler_get_decoration(compiler, output.id, SpvDecorationOffset);
capture.componentCount = XfbComponentCount(compiler, output.type_id);
if (!capture.name.empty()) captures.push_back(Move(capture));
continue;
}
for (unsigned member = 0; member < memberCount; ++member) {
if (spvc_compiler_has_member_decoration(compiler, output.base_type_id, member,
SpvDecorationOffset) != SPVC_TRUE) {
continue;
}
const char* memberName =
spvc_compiler_get_member_name(compiler, output.base_type_id, member);
if (memberName == nullptr || *memberName == '\0') continue;
SpirvXfbCapture capture;
// A redeclared built-in block contributes its members by their own names
// ("gl_Position"), which is how GL's capture interface spells them; an
// application block spells them "Block.member".
const String blockName = output.name ? String(output.name) : String{};
const Bool isBuiltInBlock = blockName.compare(0, 3, "gl_") == 0;
capture.name = isBuiltInBlock || blockName.empty()
? String(memberName)
: blockName + "." + String(memberName);
capture.buffer = buffer;
capture.stride = stride;
capture.offset = spvc_compiler_get_member_decoration(compiler, output.base_type_id, member,
SpvDecorationOffset);
capture.componentCount = XfbComponentCount(
compiler, spvc_type_get_member_type(type, member));
captures.push_back(Move(capture));
}
}
// Capture order IS buffer-then-offset order: that is the order the equivalent
// glTransformFeedbackVaryings request has to name them in for the frontend's
// packer to reproduce the declared layout.
std::stable_sort(captures.begin(), captures.end(),
[](const SpirvXfbCapture& a, const SpirvXfbCapture& b) {
if (a.buffer != b.buffer) return a.buffer < b.buffer;
return a.offset < b.offset;
});
return captures;
}
void SpvcSession::StripTransformFeedbackDecorations() {
if (compiler == nullptr || resources == nullptr) return;
const spvc_reflected_resource* outputs = nullptr;
SizeT outputCount = 0;
if (spvc_resources_get_resource_list_for_type(resources, SPVC_RESOURCE_TYPE_STAGE_OUTPUT, &outputs,
&outputCount) != SPVC_SUCCESS) {
return;
}
for (SizeT i = 0; i < outputCount; ++i) {
const spvc_reflected_resource& output = outputs[i];
spvc_compiler_unset_decoration(compiler, output.id, SpvDecorationXfbBuffer);
spvc_compiler_unset_decoration(compiler, output.id, SpvDecorationXfbStride);
spvc_compiler_unset_decoration(compiler, output.id, SpvDecorationOffset);
const spvc_type type = spvc_compiler_get_type_handle(compiler, output.base_type_id);
if (type == nullptr || spvc_type_get_basetype(type) != SPVC_BASETYPE_STRUCT) continue;
const unsigned memberCount = spvc_type_get_num_member_types(type);
for (unsigned member = 0; member < memberCount; ++member) {
spvc_compiler_unset_member_decoration(compiler, output.base_type_id, member,
SpvDecorationOffset);
spvc_compiler_unset_member_decoration(compiler, output.base_type_id, member,
SpvDecorationXfbBuffer);
spvc_compiler_unset_member_decoration(compiler, output.base_type_id, member,
SpvDecorationXfbStride);
}
}
}
spvc_result SpvcSession::SetEntryPoint(const char* name, SpvExecutionModel model) {
if (compiler == nullptr || name == nullptr || *name == '\0') return SPVC_SUCCESS;
// A null compiler or a null/empty name is a FAILURE, not a silent success: the
// caller is asking for a specific entry point and there is none to give it.
if (compiler == nullptr || name == nullptr || *name == '\0') return SPVC_ERROR_INVALID_ARGUMENT;
return spvc_compiler_set_entry_point(compiler, name, model);
}
@@ -56,6 +56,19 @@ namespace MobileGL {
}
};
// One output a SPIR-V module asked to have captured, as its Xfb decorations describe
// it. ARB_gl_spirv makes these decorations the ONLY way a SPIR-V program declares
// transform feedback - glTransformFeedbackVaryings has no effect on such a program -
// so a module that carries them and an implementation that ignores them capture
// nothing at all.
struct SpirvXfbCapture {
String name; // the GL interface name: "gl_Position", or "Block.member"
Uint32 buffer = 0; // XfbBuffer on the declaring variable
Uint32 offset = 0; // Offset on the variable or on the member
Uint32 stride = 0; // XfbStride on the declaring variable
Uint32 componentCount = 0; // how many 32-bit components the capture occupies
};
enum class SessionUsageBit {
Reflection = 1 << 0,
Transpile = 1 << 1,
@@ -164,6 +177,28 @@ namespace MobileGL {
// Select which OpEntryPoint of `model` this session compiles. A module may carry
// several of the same execution model, and glSpecializeShader names the one the
// shader object stands for.
// Whether the transpile constructor actually built a compiler. Every SPIRV-Cross
// handle below is default-null and the C API leaves its out-params untouched on
// failure, so a module SPIRV-Cross cannot parse used to leave `ir` null and then
// have spvc_context_create_compiler dereference it - a raw null read that
// SPVC_BEGIN_SAFE_SCOPE cannot catch. Only glShaderBinary feeds this class bytes
// MobileGL did not generate itself, which is why the check earns its keep now.
Bool IsTranspileReady() const { return compiler != nullptr && resources != nullptr; }
// Read the module's transform-feedback layout out of its Xfb decorations, in
// (buffer, offset) order. Empty when the module declares no capture.
Vector<SpirvXfbCapture> ReflectTransformFeedbackCaptures() const;
// Remove every Xfb decoration the reflection above just read.
//
// This is not tidying: the decorations must not survive into the GLSL this session
// emits. SPIRV-Cross re-emits them as `layout(xfb_buffer = N, xfb_stride = M) out
// gl_PerVertex { layout(xfb_offset = K) ... }`, glslang re-encodes that into the
// regenerated SPIR-V, and the DirectGLES leg then transpiles THAT to ESSL - where
// the same SPIRV-Cross throws "Need GL_ARB_enhanced_layouts for xfb_stride or
// xfb_buffer" and the stage silently fails to build, leaving a program that links
// clean and draws nothing. Stripping them and re-declaring the capture through
// MobileGL's ordinary capture machinery (which both backends already implement)
// routes a SPIR-V program down exactly the path a GLSL program takes.
void StripTransformFeedbackDecorations();
spvc_result SetEntryPoint(const char* name, SpvExecutionModel model);
// Bake glSpecializeShader's values into the module's specialization constants.
// Every value is a GLuint on the GL side and is reinterpreted according to the
@@ -176,11 +176,17 @@ namespace MobileGL {
// memo keys. The conformance suite accepts a link-time rejection: its predicate is
// compiledAndLinked(), which is the AND of the two.
//
// ONE enforcement point for all five kinds, on purpose. Before this, exactly one kind -
// shader-storage blocks - was checked, by a bespoke lexical scan of the shader source, which
// is why the storage sub-family was the one that passed while sampler, image, uniform-block
// and atomic-counter bindings sailed past every ceiling. That scanner is retired; a second
// enforcement point is a second thing to drift.
// FIVE KINDS HERE, AND ONE OF THEM IS ALSO CHECKED EARLIER. Before this, exactly one kind -
// shader-storage blocks - was checked at all, by a bespoke lexical scan of the shader source,
// which is why the storage sub-family was the one that passed while sampler, image,
// uniform-block and atomic-counter bindings sailed past every ceiling.
//
// That scan is deliberately KEPT (ShaderCompileTask.cpp's MaxShaderStorageBufferBindings
// explains why: GLSL makes an over-range binding a COMPILE-time error, and the relaxed Vulkan
// parse leaves the scan as the only place MobileGL can raise one). So the storage arm has two
// enforcement points and the other four have this one. What keeps them from drifting is not
// that there is only one site but that both read the SAME numbers - ResolveResourceBindingLimits
// is the single derivation, and neither site computes a ceiling of its own.
void TMglGlslIoResolver::CheckDeclaredBindingRange(const glslang::TType& type, const glslang::TString& name) {
if (m_bindingLimits == nullptr || m_bindingViolation == nullptr) return;
if (!m_bindingViolation->empty()) return; // first violation wins; the link is already lost
@@ -229,10 +235,17 @@ namespace MobileGL {
// The ARRAYED-INSTANCE rule: an array of N takes bindings base .. base + N - 1, and every
// one of them has to fit. getCumulativeArraySize() folds a multi-dimensional array into
// the count of leaf elements, which is exactly how many consecutive bindings GL hands out.
// An unsized or implicitly-sized array reports 0; treat it as one binding rather than
// guess, since it cannot be the shape the rule is about.
//
// isSizedArray() is MANDATORY, not defensive. glslang's TArraySizes::getCumulativeSize()
// asserts `sizes.getDimSize(d) != UnsizedArraySize` ("this only makes sense in paths that
// have a known array size"), so calling it on a run-time-sized array - the ordinary shape
// of a storage block's trailing member, and legal on the block instance itself - aborts
// the process inside mapIO's collect callback in any build with assertions live. The
// repo defines no NDEBUG of its own, so a CMake Debug build is exactly such a build; the
// "reports 0" behaviour the previous comment relied on is only what NDEBUG happens to do.
// An unsized array occupies one binding here, which is also what GL means by it.
long long elementCount = 1;
if (type.isArray()) {
if (type.isArray() && type.isSizedArray()) {
const int cumulative = static_cast<int>(type.getCumulativeArraySize());
if (cumulative > 1) elementCount = cumulative;
}