[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) { static bool CheckShaderNameValidity(Uint shader) {
if (shader == 0 || !MG_State::pGLContext->ValidateShaderName(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( MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue, error,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, 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 false;
} }
return true; return true;
@@ -451,6 +463,25 @@ namespace MobileGL::MG_Impl::GLImpl {
" has no SPIR-V binary; call glShaderBinary first.")); " has no SPIR-V binary; call glShaderBinary first."));
return; 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)) { if (numSpecializationConstants > 0 && (pConstantIndex == nullptr || pConstantValue == nullptr)) {
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue, 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()); 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( auto specialized = MG_Util::ShaderTranspiler::ShaderCompiler::SpecializeAndDecompileSpirvModule(
shaderObject->GetSpirvBinary(), shaderType, entryPoint, constantIds, constantValues); shaderObject->GetSpirvBinary(), shaderType, entryPoint, constantIds, constantValues, failure);
if (!specialized) { 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, MGLOG_D("%s: specialization failed for shader %u: %s", __func__, shader,
specialized.error().log.c_str()); 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)); shaderObject->RecordSpecializationFailure(String(specialized.error().log));
return; return;
} }
shaderObject->SpecializeFromSpirv(Move(specialized.value())); shaderObject->SpecializeFromSpirv(Move(specialized.value().glsl), Move(specialized.value().xfbVaryings),
specialized.value().xfbBufferMode);
} }
// glMaxShaderCompilerThreadsKHR / glMaxShaderCompilerThreadsARB - one implementation, // glMaxShaderCompilerThreadsKHR / glMaxShaderCompilerThreadsARB - one implementation,
@@ -1057,9 +1100,13 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
*params = shaderObject->GetInfoLog().empty() ? 0 : (GLint)shaderObject->GetInfoLog().length() + 1; *params = shaderObject->GetInfoLog().empty() ? 0 : (GLint)shaderObject->GetInfoLog().length() + 1;
break; break;
case GL_SHADER_SOURCE_LENGTH: case GL_SHADER_SOURCE_LENGTH: {
*params = shaderObject->GetShaderSource().empty() ? 0 : (GLint)shaderObject->GetShaderSource().length() + 1; // 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; break;
}
// GL_ARB_gl_spirv. GL_SPIR_V_BINARY and GL_SPIR_V_BINARY_ARB are the same token: TRUE // 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 // 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 // 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); auto& shaderObject = TryToGetShaderObject(shader);
if (!shaderObject) return; if (!shaderObject) return;
auto& src = shaderObject->GetShaderSource(); auto& src = shaderObject->GetApplicationShaderSource();
CopyStr(bufSize, length, source, src.c_str(), (GLsizei)src.length()); 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, void ProgramUniformMatrix2fv_State(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLfloat* value) { const GLfloat* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program); auto& programObject = TryToGetProgramObject(program);
if (!programObject) return; if (!programObject) return;
@@ -1981,14 +2026,14 @@ namespace MobileGL::MG_Impl::GLImpl {
return; return;
} }
if (location == -1) return;
UniformMatrixfv_Object(*programObject, __func__, location, count, transpose, value, 2, 2, UniformMatrixfv_Object(*programObject, __func__, location, count, transpose, value, 2, 2,
"program " + std::to_string(program)); "program " + std::to_string(program));
} }
void ProgramUniformMatrix3fv_State(GLuint program, GLint location, GLsizei count, GLboolean transpose, void ProgramUniformMatrix3fv_State(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLfloat* value) { const GLfloat* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program); auto& programObject = TryToGetProgramObject(program);
if (!programObject) return; if (!programObject) return;
@@ -2000,6 +2045,8 @@ namespace MobileGL::MG_Impl::GLImpl {
return; return;
} }
if (location == -1) return;
for (GLint i = 0; i < count; i++) { for (GLint i = 0; i < count; i++) {
if (i > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + i)) { if (i > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + i)) {
// Values for elements beyond the end of the uniform array are ignored. // 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, void ProgramUniformMatrix4fv_State(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLfloat* value) { const GLfloat* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program); auto& programObject = TryToGetProgramObject(program);
if (!programObject) return; if (!programObject) return;
@@ -2038,6 +2083,8 @@ namespace MobileGL::MG_Impl::GLImpl {
return; return;
} }
if (location == -1) return;
for (GLint i = 0; i < count; i++) { for (GLint i = 0; i < count; i++) {
if (i > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + i)) { if (i > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + i)) {
// Values for elements beyond the end of the uniform array are ignored. // 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, void ProgramUniformMatrixNonSquarefv_State(const char* caller, GLuint program, GLint location, GLsizei count,
GLboolean transpose, const GLfloat* value, Int columns, Int rows) { GLboolean transpose, const GLfloat* value, Int columns, Int rows) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program); auto& programObject = TryToGetProgramObject(program);
if (!programObject) return; if (!programObject) return;
@@ -2072,6 +2117,8 @@ namespace MobileGL::MG_Impl::GLImpl {
return; return;
} }
if (location == -1) return;
UniformMatrixfv_Object(*programObject, caller, location, count, transpose, value, columns, rows, UniformMatrixfv_Object(*programObject, caller, location, count, transpose, value, columns, rows,
"program " + std::to_string(program)); "program " + std::to_string(program));
} }
@@ -2621,7 +2668,6 @@ namespace MobileGL::MG_Impl::GLImpl {
void ProgramUniformMatrix2dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, void ProgramUniformMatrix2dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLdouble* value) { const GLdouble* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program); auto& programObject = TryToGetProgramObject(program);
if (!programObject) return; if (!programObject) return;
if (!programObject->GetLinkStatus()) { if (!programObject->GetLinkStatus()) {
@@ -2631,6 +2677,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"program " + std::to_string(program) + " is not linked.")); "program " + std::to_string(program) + " is not linked."));
return; return;
} }
if (location == -1) return;
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 2, 2); UniformMatrixdv_Object(*programObject, location, count, transpose, value, 2, 2);
} }
void UniformMatrix3dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) { 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, void ProgramUniformMatrix3dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLdouble* value) { const GLdouble* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program); auto& programObject = TryToGetProgramObject(program);
if (!programObject) return; if (!programObject) return;
if (!programObject->GetLinkStatus()) { if (!programObject->GetLinkStatus()) {
@@ -2657,6 +2703,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"program " + std::to_string(program) + " is not linked.")); "program " + std::to_string(program) + " is not linked."));
return; return;
} }
if (location == -1) return;
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 3, 3); UniformMatrixdv_Object(*programObject, location, count, transpose, value, 3, 3);
} }
void UniformMatrix4dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) { 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, void ProgramUniformMatrix4dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLdouble* value) { const GLdouble* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program); auto& programObject = TryToGetProgramObject(program);
if (!programObject) return; if (!programObject) return;
if (!programObject->GetLinkStatus()) { if (!programObject->GetLinkStatus()) {
@@ -2683,6 +2729,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"program " + std::to_string(program) + " is not linked.")); "program " + std::to_string(program) + " is not linked."));
return; return;
} }
if (location == -1) return;
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 4, 4); UniformMatrixdv_Object(*programObject, location, count, transpose, value, 4, 4);
} }
void UniformMatrix2x3dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) { 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, void ProgramUniformMatrix2x3dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLdouble* value) { const GLdouble* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program); auto& programObject = TryToGetProgramObject(program);
if (!programObject) return; if (!programObject) return;
if (!programObject->GetLinkStatus()) { if (!programObject->GetLinkStatus()) {
@@ -2709,6 +2755,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"program " + std::to_string(program) + " is not linked.")); "program " + std::to_string(program) + " is not linked."));
return; return;
} }
if (location == -1) return;
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 2, 3); UniformMatrixdv_Object(*programObject, location, count, transpose, value, 2, 3);
} }
void UniformMatrix2x4dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) { 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, void ProgramUniformMatrix2x4dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLdouble* value) { const GLdouble* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program); auto& programObject = TryToGetProgramObject(program);
if (!programObject) return; if (!programObject) return;
if (!programObject->GetLinkStatus()) { if (!programObject->GetLinkStatus()) {
@@ -2735,6 +2781,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"program " + std::to_string(program) + " is not linked.")); "program " + std::to_string(program) + " is not linked."));
return; return;
} }
if (location == -1) return;
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 2, 4); UniformMatrixdv_Object(*programObject, location, count, transpose, value, 2, 4);
} }
void UniformMatrix3x2dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) { 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, void ProgramUniformMatrix3x2dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLdouble* value) { const GLdouble* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program); auto& programObject = TryToGetProgramObject(program);
if (!programObject) return; if (!programObject) return;
if (!programObject->GetLinkStatus()) { if (!programObject->GetLinkStatus()) {
@@ -2761,6 +2807,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"program " + std::to_string(program) + " is not linked.")); "program " + std::to_string(program) + " is not linked."));
return; return;
} }
if (location == -1) return;
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 3, 2); UniformMatrixdv_Object(*programObject, location, count, transpose, value, 3, 2);
} }
void UniformMatrix3x4dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) { 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, void ProgramUniformMatrix3x4dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLdouble* value) { const GLdouble* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program); auto& programObject = TryToGetProgramObject(program);
if (!programObject) return; if (!programObject) return;
if (!programObject->GetLinkStatus()) { if (!programObject->GetLinkStatus()) {
@@ -2787,6 +2833,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"program " + std::to_string(program) + " is not linked.")); "program " + std::to_string(program) + " is not linked."));
return; return;
} }
if (location == -1) return;
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 3, 4); UniformMatrixdv_Object(*programObject, location, count, transpose, value, 3, 4);
} }
void UniformMatrix4x2dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) { 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, void ProgramUniformMatrix4x2dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLdouble* value) { const GLdouble* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program); auto& programObject = TryToGetProgramObject(program);
if (!programObject) return; if (!programObject) return;
if (!programObject->GetLinkStatus()) { if (!programObject->GetLinkStatus()) {
@@ -2813,6 +2859,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"program " + std::to_string(program) + " is not linked.")); "program " + std::to_string(program) + " is not linked."));
return; return;
} }
if (location == -1) return;
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 4, 2); UniformMatrixdv_Object(*programObject, location, count, transpose, value, 4, 2);
} }
void UniformMatrix4x3dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) { 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, void ProgramUniformMatrix4x3dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLdouble* value) { const GLdouble* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program); auto& programObject = TryToGetProgramObject(program);
if (!programObject) return; if (!programObject) return;
if (!programObject->GetLinkStatus()) { if (!programObject->GetLinkStatus()) {
@@ -2839,6 +2885,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"program " + std::to_string(program) + " is not linked.")); "program " + std::to_string(program) + " is not linked."));
return; return;
} }
if (location == -1) return;
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 4, 3); UniformMatrixdv_Object(*programObject, location, count, transpose, value, 4, 3);
} }
void GetUniformdv(GLuint program, GLint location, GLdouble* params) { 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)); MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS));
} }
// Array targets store their layer count in z; layers never participate in mip // How many components of a GL-space texel size actually halve down the mip chain.
// reduction (GL 3.3 §3.8.14), only true 3D textures halve their depth per level. //
// 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) { 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 // 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 // The longest mip chain the level-0 size admits, over the axes that actually reduce.
// height, so unlike a 2D texture its height takes no part in the reduction. Uint ComputeFullMipmapLevelCount(const IntVec3& baseTexelSize, Int shrinkingAxes);
Uint ComputeFullMipmapLevelCount(const IntVec3& baseTexelSize, Bool depthMips);
Uint MaxTextureStorageLevels(TextureTarget target, GLsizei width, GLsizei height, GLsizei depth) { Uint MaxTextureStorageLevels(TextureTarget target, GLsizei width, GLsizei height, GLsizei depth) {
const Int mipHeight = (target == TextureTarget::Texture1DArray) ? 1 : std::max<Int>(height, 1); return ComputeFullMipmapLevelCount(
return ComputeFullMipmapLevelCount({std::max<Int>(width, 1), mipHeight, std::max<Int>(depth, 1)}, {std::max<Int>(width, 1), std::max<Int>(height, 1), std::max<Int>(depth, 1)},
DepthParticipatesInMipmapping(target)); MipShrinkingAxisCount(target));
} }
Uint ComputeFullMipmapLevelCount(const IntVec3& baseTexelSize, Bool depthMips) { Uint ComputeFullMipmapLevelCount(const IntVec3& baseTexelSize, Int shrinkingAxes) {
Int maxDimension = std::max<Int>( Int maxDimension = 1;
baseTexelSize.x(), for (Int axis = 0; axis < shrinkingAxes && axis < 3; ++axis) {
std::max<Int>(baseTexelSize.y(), depthMips ? std::max<Int>(baseTexelSize.z(), 1) : 1)); maxDimension = std::max<Int>(maxDimension, baseTexelSize[axis]);
}
Uint mipLevelCount = 1; Uint mipLevelCount = 1;
while (maxDimension > 1) { while (maxDimension > 1) {
maxDimension = std::max<Int>(maxDimension / 2, 1); maxDimension = std::max<Int>(maxDimension / 2, 1);
@@ -449,13 +485,13 @@ namespace MobileGL::MG_Impl::GLImpl {
return mipLevelCount; return mipLevelCount;
} }
IntVec3 ComputeMipmapTexelSize(const IntVec3& baseTexelSize, Uint relativeLevel, Bool depthMips) { IntVec3 ComputeMipmapTexelSize(const IntVec3& baseTexelSize, Uint relativeLevel, Int shrinkingAxes) {
return { IntVec3 size = {std::max<Int>(baseTexelSize.x(), 1), std::max<Int>(baseTexelSize.y(), 1),
std::max<Int>(baseTexelSize.x() >> static_cast<Int>(relativeLevel), 1), std::max<Int>(baseTexelSize.z(), 1)};
std::max<Int>(baseTexelSize.y() >> static_cast<Int>(relativeLevel), 1), for (Int axis = 0; axis < shrinkingAxes && axis < 3; ++axis) {
depthMips ? std::max<Int>(baseTexelSize.z() >> static_cast<Int>(relativeLevel), 1) size[axis] = std::max<Int>(size[axis] >> static_cast<Int>(relativeLevel), 1);
: std::max<Int>(baseTexelSize.z(), 1), }
}; return size;
} }
Bool EnsureGeneratedMipmapStorageAllocated( Bool EnsureGeneratedMipmapStorageAllocated(
@@ -477,10 +513,10 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
const SizeT bytesPerTexel = baseByteSize / baseTexelCount; const SizeT bytesPerTexel = baseByteSize / baseTexelCount;
const Bool depthMips = DepthParticipatesInMipmapping(texture.GetTarget()); const Int shrinkingAxes = MipShrinkingAxisCount(texture.GetTarget());
const Uint requiredLevelCount = ComputeFullMipmapLevelCount(baseTexelSize, depthMips); const Uint requiredLevelCount = ComputeFullMipmapLevelCount(baseTexelSize, shrinkingAxes);
for (Uint level = 1; level < requiredLevelCount; ++level) { 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()) * const SizeT levelByteSize = bytesPerTexel * static_cast<SizeT>(levelTexelSize.x()) *
static_cast<SizeT>(levelTexelSize.y()) * static_cast<SizeT>(levelTexelSize.y()) *
static_cast<SizeT>(levelTexelSize.z()); static_cast<SizeT>(levelTexelSize.z());
@@ -3686,6 +3722,59 @@ namespace MobileGL::MG_Impl::GLImpl {
GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height,
GLsizei depth, const char* caller); 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 // The shared body of glCopyTexSubImage3D and glCopyTextureSubImage3D once the caller has
// resolved the destination texture. `allowCubeFaceFromZOffset` is the ONE difference between // 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 // 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 level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x,
GLint y, GLsizei width, GLsizei height, Bool allowCubeFaceFromZOffset, GLint y, GLsizei width, GLsizei height, Bool allowCubeFaceFromZOffset,
const char* caller) { 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; 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); TextureUploadTarget uploadTarget = GetPrimaryUploadTarget(textureObject);
GLint sliceOffset = zoffset; GLint sliceOffset = zoffset;
if (allowCubeFaceFromZOffset && textureObject->GetTarget() == TextureTarget::TextureCubeMap) { 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>( uploadTarget = static_cast<TextureUploadTarget>(
static_cast<SizeT>(TextureUploadTarget::CubeMapPositiveX) + static_cast<SizeT>(zoffset)); static_cast<SizeT>(TextureUploadTarget::CubeMapPositiveX) + static_cast<SizeT>(zoffset));
sliceOffset = 0; 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, CopyReadFramebufferIntoMipmapRegion(textureObject, uploadTarget, level, xoffset, yoffset, sliceOffset, x, y,
width, height, caller); 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, void CopyTexSubImage3D_State(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x,
GLint y, GLsizei width, GLsizei height) { GLint y, GLsizei width, GLsizei height) {
// GL 4.6 core 8.6 table: the three-dimensional form of the bound-texture copy accepts // 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); const auto textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget); auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
if (!textureObject) return; if (!textureObject) return;
if (!ValidateCopyTextureSubImage(textureObject, level, xoffset, 0, 0, width, 1, 1, __func__)) return; CopyTextureSubImage1DResolved(textureObject, level, xoffset, x, y, width, __func__);
CopyReadFramebufferIntoMipmapRegion(textureObject, GetPrimaryUploadTarget(textureObject), level, xoffset,
/*yoffset=*/0, /*zoffset=*/0, x, y, width, /*height=*/1, __func__);
} }
Bool CopyTexImage2D_State(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width, 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.")); "CopyTextureSubImage1D requires a 1D texture."));
return; return;
} }
if (!ValidateCopyTextureSubImage(textureObject, level, xoffset, 0, 0, width, 1, 1, __func__)) return; CopyTextureSubImage1DResolved(textureObject, level, xoffset, x, y, width, __func__);
CopyReadFramebufferIntoMipmapRegion(textureObject, GetPrimaryUploadTarget(textureObject), level, xoffset,
/*yoffset=*/0, /*zoffset=*/0, x, y, width, /*height=*/1, __func__);
} }
void CopyTextureSubImage3D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x, 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 // a graphics program carrying a compute module, which Adreno 830 does not reject
// from vkCreateGraphicsPipelines - it SIGSEGVs inside it. // from vkCreateGraphicsPipelines - it SIGSEGVs inside it.
Bool anyStage = false; 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) { for (SizeT stage = 0; stage < ProgramPipelineObject::kGraphicsStageCount; ++stage) {
const auto& stageProgram = pipeline->GetStageProgram(static_cast<ShaderStage>(stage)); const auto& stageProgram = pipeline->GetStageProgram(static_cast<ShaderStage>(stage));
if (!stageProgram) continue; if (!stageProgram) continue;
@@ -665,6 +671,7 @@ namespace MobileGL::MG_State {
if (!ref.shader || static_cast<SizeT>(ref.shader->GetShaderStage()) != stage) continue; if (!ref.shader || static_cast<SizeT>(ref.shader->GetShaderStage()) != stage) continue;
composite->AttachShaderWithPinnedLinkInput(ref); composite->AttachShaderWithPinnedLinkInput(ref);
anyStage = true; anyStage = true;
compositeHasStage[stage] = true;
} }
} }
if (!anyStage) return nullProgram; 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 // (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 // 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 // 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 // GL-thread-owned state, so without this it links with an empty capture list and
// capture list and glBeginTransformFeedback rejects the draw with INVALID_OPERATION // glBeginTransformFeedback rejects the draw with INVALID_OPERATION ("the program has
// ("the program has no transform feedback varyings") even though // no transform feedback varyings") even though glValidateProgramPipeline had passed.
// glValidateProgramPipeline had just passed. Same resolution order as //
// ProgramLinkTask::ResolveTransformFeedbackVaryings: geometry, else tessellation // TWO RULES, both easy to get subtly wrong and both load-bearing:
// evaluation, else vertex. //
// (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: for (const ShaderStage captureStage:
{ShaderStage::Geometry, ShaderStage::TessEval, ShaderStage::Vertex}) { {ShaderStage::Geometry, ShaderStage::TessEval, ShaderStage::Vertex}) {
if (!compositeHasStage[static_cast<SizeT>(captureStage)]) continue;
const auto& captureProgram = pipeline->GetStageProgram(captureStage); const auto& captureProgram = pipeline->GetStageProgram(captureStage);
if (!captureProgram) continue; if (!captureProgram) continue;
const auto& requested = captureProgram->GetRequestedTransformFeedbackVaryings(); const auto& linkedNames = captureProgram->GetTransformFeedbackInterfaceNames();
if (requested.empty()) continue; if (!linkedNames.empty()) {
composite->SetTransformFeedbackVaryings(Vector<String>(requested), composite->SetTransformFeedbackVaryings(Vector<String>(linkedNames),
captureProgram->GetRequestedTransformFeedbackBufferMode()); captureProgram->GetTransformFeedbackBufferMode());
}
break; break;
} }
// A pipeline with no fragment stage still rasterises, so the default fragment // 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.explicitFragDataIndex = m_explicitFragDataIndex;
task->in.requestedXfbVaryings = m_requestedXfbVaryings; task->in.requestedXfbVaryings = m_requestedXfbVaryings;
task->in.requestedXfbBufferMode = m_requestedXfbBufferMode; 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; task->in.maxFragmentOutputColorNumber = m_maxFragmentOutputColorNumber;
Vector<SharedPtr<ShaderCompileTask>> deps; Vector<SharedPtr<ShaderCompileTask>> deps;
@@ -1536,14 +1536,14 @@ namespace MobileGL::MG_State::GLState {
m_requestedXfbVaryings = Move(names); m_requestedXfbVaryings = Move(names);
m_requestedXfbBufferMode = bufferMode; m_requestedXfbBufferMode = bufferMode;
} }
// The REQUEST, not the linked result: what glTransformFeedbackVaryings last recorded, // NO ACCESSOR FOR THE PENDING REQUEST, deliberately. A program pipeline's draw composite
// which the next link will try to resolve. A program pipeline's draw composite reads it // needs the capture list of the stage program it flattens, and the obvious source - what
// off the capturing stage program and re-issues it on itself, because the composite is // glTransformFeedbackVaryings last recorded - is the wrong one: that request does not take
// built from the stage programs' SHADERS and would otherwise inherit no capture list at // effect until the stage program's next link, and it bumps no version, so reading it makes
// all - which made glBeginTransformFeedback reject every separable-program capture // the composite's capture list depend on when the composite cache happened to be
// (glcSeparableProgramsTransformFeedbackTests). // invalidated. GetTransformFeedbackInterfaceNames() below is the source that is correct
const Vector<String>& GetRequestedTransformFeedbackVaryings() const { return m_requestedXfbVaryings; } // AND cache-safe, because linked state only moves at a link and the composite signature
GLenum GetRequestedTransformFeedbackBufferMode() const { return m_requestedXfbBufferMode; } // already keys on the link version. See GLContext::GetProgramForDraw.
GLenum GetTransformFeedbackBufferMode() const { return Artifacts().xfbBufferMode; } GLenum GetTransformFeedbackBufferMode() const { return Artifacts().xfbBufferMode; }
SizeT GetTransformFeedbackVaryingCount() const { return Artifacts().xfbVaryings.size(); } SizeT GetTransformFeedbackVaryingCount() const { return Artifacts().xfbVaryings.size(); }
const XfbVarying* GetTransformFeedbackVarying(SizeT index) const { const XfbVarying* GetTransformFeedbackVarying(SizeT index) const {
@@ -21,16 +21,31 @@ namespace MobileGL::MG_State::GLState {
ReleaseCompileNode(); ReleaseCompileNode();
m_spirvBinary = Move(binary); m_spirvBinary = Move(binary);
m_hasSpirvBinary = true; m_hasSpirvBinary = true;
m_specialized = false;
m_specializationFailed = false; m_specializationFailed = false;
m_specializationInfoLog.clear(); m_specializationInfoLog.clear();
m_spirvXfbVaryings.clear();
m_spirvXfbBufferMode = GL_INTERLEAVED_ATTRIBS;
m_source = MakeShared<const String>(String{}); m_source = MakeShared<const String>(String{});
InvalidateCompiledState(); 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(); 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_specializationFailed = false;
m_specializationInfoLog.clear(); m_specializationInfoLog.clear();
m_spirvXfbVaryings = Move(xfbVaryings);
m_spirvXfbBufferMode = xfbBufferMode;
// The GLSL the module specializes to enters the ORDINARY pipeline from here: preprocess, // The GLSL the module specializes to enters the ORDINARY pipeline from here: preprocess,
// glslang parse, reflection, transpile, both backends. Nothing downstream needs to know // 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 // 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_hasSpirvBinary = false;
m_spirvBinary.clear(); m_spirvBinary.clear();
m_spirvBinary.shrink_to_fit(); m_spirvBinary.shrink_to_fit();
m_specialized = false;
m_specializationFailed = false; m_specializationFailed = false;
m_specializationInfoLog.clear(); m_specializationInfoLog.clear();
m_spirvXfbVaryings.clear();
m_spirvXfbBufferMode = GL_INTERLEAVED_ATTRIBS;
ReleaseCompileNode(); ReleaseCompileNode();
m_source = MakeShared<const String>(source); m_source = MakeShared<const String>(source);
InvalidateCompiledState(); InvalidateCompiledState();
@@ -78,10 +78,33 @@ namespace MobileGL {
// checks explicitly. // checks explicitly.
void SetSpirvBinary(Vector<Uint32>&& binary); void SetSpirvBinary(Vector<Uint32>&& binary);
Bool HasSpirvBinary() const { return m_hasSpirvBinary; } 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; } const Vector<Uint32>& GetSpirvBinary() const { return m_spirvBinary; }
// glSpecializeShader's half: hand the object the GLSL its module specializes to and // glSpecializeShader's half: hand the object the GLSL its module specializes to and
// let the ordinary pipeline compile it. // 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 // 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, // 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 // 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. // the ORIGINAL words rather than the ones the first call folded.
Vector<Uint32> m_spirvBinary; Vector<Uint32> m_spirvBinary;
Bool m_hasSpirvBinary = false; 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 // 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 // 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 // it on - see RecordSpecializationFailure. Cleared by anything that gives the object
@@ -248,11 +248,12 @@ namespace MobileGL {
} }
void RenderState::SetPolygonOffset(Float factor, Float units) { void RenderState::SetPolygonOffset(Float factor, Float units) {
if (m_parameters.PolygonOffsetFactor == factor && m_parameters.PolygonOffsetUnits == units) return; // 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
m_parameters.PolygonOffsetFactor = factor; // written too, not merely left alone. Leaving it meant a glPolygonOffsetClamp(1, 1,
m_parameters.PolygonOffsetUnits = units; // 0.5) followed by a plain glPolygonOffset(3, 4) still reported a clamp of 0.5, and
++m_version; // the early-out below could even skip the version bump while doing it.
SetPolygonOffsetClamped(factor, units, 0.0f);
} }
Float RenderState::GetPolygonOffsetFactor() const { Float RenderState::GetPolygonOffsetFactor() const {
+10 -7
View File
@@ -3072,14 +3072,19 @@ TEST(GetterSanity, CombinedUniformComponentsSaturateInsteadOfOverflowing) {
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>(); 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[] = { static constexpr GLenum kCombinedPnames[] = {
GL_MAX_COMBINED_VERTEX_UNIFORM_COMPONENTS, GL_MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS, 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_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 // The GL 4.6 core table 23.64 floor, which all six combined pnames carry.
// 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.
static constexpr GLint kCombinedFloor = 58368; static constexpr GLint kCombinedFloor = 58368;
{ {
@@ -3091,9 +3096,7 @@ TEST(GetterSanity, CombinedUniformComponentsSaturateInsteadOfOverflowing) {
GLint reported = 0; GLint reported = 0;
MG_Impl::GLImpl::GetIntegerv(pname, &reported); MG_Impl::GLImpl::GetIntegerv(pname, &reported);
EXPECT_GT(reported, 0) << "pname 0x" << pname << " wrapped to a negative combined component count"; 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(); MG_Backend::pActiveBackendObject.reset();
} }
+14 -4
View File
@@ -980,15 +980,25 @@ TEST_F(RenderStateTest, PolygonOffsetClampStoresTheClampAndTheFactorUnitsPair) {
EXPECT_NEAR(asDouble, 0.5, 1e-6); EXPECT_NEAR(asDouble, 0.5, 1e-6);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); 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 // GL 4.6 core 14.6.5 defines glPolygonOffset(factor, units) as EQUIVALENT to
// it does not take - GL 4.6 core 14.6.5 defines it as PolygonOffsetClamp(factor, units, 0) // glPolygonOffsetClamp(factor, units, 0) - totally, not "except for the clamp". So it writes
// only in the sense that the clamp it leaves is whatever glPolygonOffset itself sets, which // all three, and a clamp left over from an earlier glPolygonOffsetClamp must be gone.
// for MobileGL is "unchanged". Assert the factor/units half instead, which is unambiguous.
MG_Impl::GLImpl::PolygonOffset(3.0f, 4.0f); MG_Impl::GLImpl::PolygonOffset(3.0f, 4.0f);
MG_Impl::GLImpl::GetFloatv(GL_POLYGON_OFFSET_FACTOR, &factor); 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_UNITS, &units);
MG_Impl::GLImpl::GetFloatv(GL_POLYGON_OFFSET_CLAMP, &clamp);
EXPECT_FLOAT_EQ(factor, 3.0f); EXPECT_FLOAT_EQ(factor, 3.0f);
EXPECT_FLOAT_EQ(units, 4.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); MG_Impl::GLImpl::PolygonOffsetClamp(0.0f, 0.0f, 0.0f);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); 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); MG_Impl::GLImpl::DeleteBuffers(1, &buffer);
DrainPendingGlErrors(); 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; 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 } // namespace
Result<void> ShaderCompiler::ValidateSpirvModule(const Vector<Uint32>& spirv) { Result<void> ShaderCompiler::ValidateSpirvModule(const Vector<Uint32>& spirv) {
@@ -1734,15 +1782,29 @@ namespace MobileGL {
return {}; return {};
} }
Result<String> ShaderCompiler::SpecializeAndDecompileSpirvModule(const Vector<Uint32>& spirv, Result<ShaderCompiler::SpecializedModule> ShaderCompiler::SpecializeAndDecompileSpirvModule(
GLenum shaderType, const Vector<Uint32>& spirv, GLenum shaderType, const String& entryPoint,
const String& entryPoint, const Vector<Uint32>& constantIds, const Vector<Uint32>& constantValues,
const Vector<Uint32>& constantIds, SpecializationFailure& outFailure) {
const Vector<Uint32>& constantValues) { outFailure = SpecializationFailure::None;
SpvcSession session(spirv, SessionUsageBit::Transpile); 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; Uint32 unknownConstantId = 0;
if (!session.SetSpecializationConstants(constantIds, constantValues, unknownConstantId)) { if (!session.SetSpecializationConstants(constantIds, constantValues, unknownConstantId)) {
outFailure = SpecializationFailure::UnknownConstantId;
ResultInfo r; ResultInfo r;
r.errc = -7; r.errc = -7;
r.log = "Error: [ARB_gl_spirv] constant index " + std::to_string(unknownConstantId) + r.log = "Error: [ARB_gl_spirv] constant index " + std::to_string(unknownConstantId) +
@@ -1750,19 +1812,32 @@ namespace MobileGL {
return std::unexpected(r); return std::unexpected(r);
} }
if (!entryPoint.empty()) { // No `if (!entryPoint.empty())` guard any more. ARB_gl_spirv makes pEntryPoint the
if (session.SetEntryPoint(entryPoint.c_str(), ExecutionModelForShaderType(shaderType)) != // name of the entry point to specialize, and no module carries one named ""; the
SPVC_SUCCESS) { // guard turned an empty name into "whichever entry point happens to be default",
ResultInfo r; // which is neither what the application asked for nor an error it was told about.
r.errc = -8; if (session.SetEntryPoint(entryPoint.c_str(), ExecutionModelForShaderType(shaderType)) !=
r.log = "Error: [ARB_gl_spirv] the module has no entry point named '" + entryPoint + SPVC_SUCCESS) {
"' for this shader stage:\n" + String(session.GetLastErrorString()); outFailure = SpecializationFailure::UnknownEntryPoint;
return std::unexpected(r); 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; spvc_compiler_options options;
if (session.CreateOptions(&options) != SPVC_SUCCESS) { if (session.CreateOptions(&options) != SPVC_SUCCESS) {
outFailure = SpecializationFailure::ModuleRejected;
ResultInfo r; ResultInfo r;
r.errc = -9; r.errc = -9;
r.log = "Error: [ARB_gl_spirv] could not create SPIRV-Cross options for the module."; 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; const char* emitted = nullptr;
session.Compile(&emitted); session.Compile(&emitted);
if (!emitted) { if (!emitted) {
outFailure = SpecializationFailure::ModuleRejected;
ResultInfo r; ResultInfo r;
r.errc = -10; r.errc = -10;
r.log = "Error: [ARB_gl_spirv] could not translate the module to GLSL:\n" + r.log = "Error: [ARB_gl_spirv] could not translate the module to GLSL:\n" +
String(session.GetLastErrorString()); String(session.GetLastErrorString());
return std::unexpected(r); return std::unexpected(r);
} }
return String(emitted); specialized.glsl = String(emitted);
return specialized;
} }
Result<String> ShaderCompiler::DecompileShader(SpvcSession& session) { Result<String> ShaderCompiler::DecompileShader(SpvcSession& session) {
@@ -434,10 +434,43 @@ namespace MobileGL {
// `constantIds` and `constantValues` are the parallel arrays the entry point // `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 // 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. // extension; it is reported through the error log rather than silently ignored.
static Result<String> SpecializeAndDecompileSpirvModule(const Vector<Uint32>& spirv, // Why the caller needs a REASON and not just a failure: ARB_gl_spirv splits the
GLenum shaderType, const String& entryPoint, // ways specialization can fail into two groups with different GL surfaces. A bad
const Vector<Uint32>& constantIds, // entry-point name and a constant id the module does not declare are enumerated
const Vector<Uint32>& constantValues); // 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 // spirv-val over an application-supplied module, against the environment MobileGL
// parses and emits under. glShaderBinary is where a malformed module has to be // parses and emits under. glShaderBinary is where a malformed module has to be
@@ -8,6 +8,8 @@
#include "SpvcSession.h" #include "SpvcSession.h"
#include <algorithm>
namespace MobileGL { namespace MobileGL {
namespace MG_Util { namespace MG_Util {
namespace ShaderTranspiler { namespace ShaderTranspiler {
@@ -184,11 +186,29 @@ namespace MobileGL {
const SpvId* p_spirv = spirv.data(); const SpvId* p_spirv = spirv.data();
size_t word_count = spirv.size(); size_t word_count = spirv.size();
spvc_context_create(&context); // Every step is checked, and each guards the next: the C API writes its
spvc_context_parse_spirv(context, p_spirv, word_count, &ir); // out-param only on success, so passing a failed step's null handle to the
spvc_context_create_compiler(context, SPVC_BACKEND_GLSL, ir, SPVC_CAPTURE_MODE_TAKE_OWNERSHIP, // step after it is a raw dereference (spvc_context_create_compiler does
&compiler); // `parsed_ir->parsed`, spvc_compiler_create_shader_resources does
spvc_compiler_create_shader_resources(compiler, &resources); // `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) { } else if (usage & SessionUsageBit::Reflection) {
SpvReflectResult result = spvReflectCreateShaderModule( SpvReflectResult result = spvReflectCreateShaderModule(
spirv.size() * sizeof(uint32_t), spirv.data(), &reflectModule); spirv.size() * sizeof(uint32_t), spirv.data(), &reflectModule);
@@ -496,8 +516,144 @@ namespace MobileGL {
SPVC_CHK_RETURN 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) { 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); 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 { enum class SessionUsageBit {
Reflection = 1 << 0, Reflection = 1 << 0,
Transpile = 1 << 1, Transpile = 1 << 1,
@@ -164,6 +177,28 @@ namespace MobileGL {
// Select which OpEntryPoint of `model` this session compiles. A module may carry // Select which OpEntryPoint of `model` this session compiles. A module may carry
// several of the same execution model, and glSpecializeShader names the one the // several of the same execution model, and glSpecializeShader names the one the
// shader object stands for. // 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); spvc_result SetEntryPoint(const char* name, SpvExecutionModel model);
// Bake glSpecializeShader's values into the module's specialization constants. // 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 // 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 // memo keys. The conformance suite accepts a link-time rejection: its predicate is
// compiledAndLinked(), which is the AND of the two. // compiledAndLinked(), which is the AND of the two.
// //
// ONE enforcement point for all five kinds, on purpose. Before this, exactly one kind - // FIVE KINDS HERE, AND ONE OF THEM IS ALSO CHECKED EARLIER. Before this, exactly one kind -
// shader-storage blocks - was checked, by a bespoke lexical scan of the shader source, which // shader-storage blocks - was checked at all, by a bespoke lexical scan of the shader source,
// is why the storage sub-family was the one that passed while sampler, image, uniform-block // which is why the storage sub-family was the one that passed while sampler, image,
// and atomic-counter bindings sailed past every ceiling. That scanner is retired; a second // uniform-block and atomic-counter bindings sailed past every ceiling.
// enforcement point is a second thing to drift. //
// 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) { void TMglGlslIoResolver::CheckDeclaredBindingRange(const glslang::TType& type, const glslang::TString& name) {
if (m_bindingLimits == nullptr || m_bindingViolation == nullptr) return; if (m_bindingLimits == nullptr || m_bindingViolation == nullptr) return;
if (!m_bindingViolation->empty()) return; // first violation wins; the link is already lost 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 // 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 // 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. // 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; long long elementCount = 1;
if (type.isArray()) { if (type.isArray() && type.isSizedArray()) {
const int cumulative = static_cast<int>(type.getCumulativeArraySize()); const int cumulative = static_cast<int>(type.getCumulativeArraySize());
if (cumulative > 1) elementCount = cumulative; if (cumulative > 1) elementCount = cumulative;
} }