Compare commits

...
Author SHA1 Message Date
swung0x48 b62d1f2078 [Fix, Test] (MG_Backend/DirectVulkan, MG_IntegrationTest): map a layered CopyImageSubData onto the axis each endpoint keeps its slices on, instead of copying slice 0 and calling it done 2026-08-13 02:49:57 -04:00
swung0x48 5e82ff968a Merge branch "feat/cts-rgb9e5-raw-transfer" into dev 2026-08-13 02:47:18 -04:00
swung0x48 6a80a82dd3 [Fix, Test] (MG_Util, MG_Backend/DirectGLES, MG_Backend/DirectVulkan, MG_Test): a packed texel whose client type already spells its storage word must cross glGetTexImage unencoded, or the RGB9_E5 shared exponent gets canonicalized 2026-08-13 02:41:37 -04:00
swung0x48 f9182a5ca3 Merge branch "fix/cts-copyimage-level-validation" into dev 2026-08-13 02:38:43 -04:00
swung0x48 f37b511fca [Fix, Test] (MG_Impl/GLImpl, MG_Backend/DirectVulkan, MG_Backend/DirectGLES, MG_Test, MG_IntegrationTest): a glCopyImageSubData to a mipmap level the texture never had is INVALID_VALUE, not a subresource handed to the driver 2026-08-13 02:28:55 -04:00
swung0x48 38027d21f8 Merge branch "feat/log-level-semantics" into dev 2026-08-13 02:19:26 -04:00
Swung0x48 dd2a62228f [Fix, Feat, Test] (MG_Util, MG_Backend, MG_Impl, MG_State): correct the log severity ordering and unwind the diagnostics it silenced 2026-08-13 02:02:49 -04:00
swung0x48 373aa44dd7 Merge branch "fix/cts-tess-ssbo-crash" into dev 2026-08-13 01:38:58 -04:00
swung0x48 96646df12e [Fix] (MG_Backend/DirectVulkan): review round - failure diagnostics take MGLOG_E, the once-per-patch-size success note takes MGLOG_D, and the per-draw refusal stays latched 2026-08-13 01:34:57 -04:00
swung0x48 f20b20e643 [Fix, Feat, Test] (MG_Backend/DirectVulkan, MG_Test): synthesize the pass-through tessellation control stage GL gives an evaluation-only program, and refuse the half-tessellated pipeline Mali dereferences null inside 2026-08-13 01:28:35 -04:00
swung0x48 2587814970 Merge branch "feat/cts-image-format-qualifier" into dev 2026-08-12 19:23:51 -04:00
swung0x48 43398e33e8 [Fix, Test] (MG_Util, MG_IntegrationTest): review round - anchor a clone only behind a real definition, decline the atomic-image shape, and say what the software lanes cannot falsify 2026-08-12 19:12:39 -04:00
swung0x48 b7557d6615 [Feat, Fix, Test] (MG_Util, MG_Backend/DirectGLES, MG_IntegrationTest): bake the bound image format into the ESSL a format-less image declaration needs 2026-08-12 18:56:31 -04:00
swung0x48 bce34d7fac Merge branch "feat/cts-followup-fp64-ds-imgbuf" into dev 2026-08-12 17:56:13 -04:00
swung0x48 f91857266f [Fix, Test] (MG_Backend/DirectGLES, MG_Test): the tessellation stages never got the flat qualifier an ES linker compares across both sides of every integer interface 2026-08-12 17:50:31 -04:00
swung0x48 49cb1be0fd [Fix, Test] (MG_Backend/DirectGLES, MG_IntegrationTest): a shader writing a buffer texture through an image unit left the CPU shadow stale, so every map and readback after it saw the old bytes 2026-08-12 17:50:31 -04:00
swung0x48 a51c68bb2c [Fix, Test] (MG_Util, MG_IntegrationTest): a 64-bit float constant was read as 32 bits, so every comparison against a round double became an epsilon test against zero 2026-08-12 17:50:25 -04:00
swung0x48 1c723a6cfc Merge branch "feat/cts-image-targets" into dev 2026-08-12 16:52:34 -04:00
swung0x48 44805bfa07 [Fix, Test] (MG_Util, MG_Backend/DirectVulkan, MG_IntegrationTest): adversarial review - the rewritten 1D-array image collided with a module's own 2D-array one and left an invalid duplicate type; pin the component order, keep the subject kinds in a truncated matrix 2026-08-12 16:49:59 -04:00
swung0x48 257fcbfd0b [Fix, Test] (MG_Util, MG_Backend/DirectVulkan, MG_IntegrationTest): review round - one module parse for the shaders with no 1D-array image, per-kind values the combined case can name, and the invariants two shared buffers rest on 2026-08-12 16:43:29 -04:00
swung0x48 2b46a3db96 [Fix, Test] (MG_Util, MG_Backend/DirectGLES, MG_Backend/DirectVulkan): the two image target kinds a compute dispatch could not read - 1D-array on ES, imageBuffer on Vulkan 2026-08-12 16:11:10 -04:00
swung0x48 0b36621069 Merge branch "feat/cts-diag-wave" into dev 2026-08-12 15:33:43 -04:00
swung0x48 9ee2e0a1db [Fix] (MG_Backend/DirectGLES): re-push the texture parameters a regenerated driver texture has lost, instead of trusting caches that outlived it 2026-08-12 15:22:39 -04:00
swung0x48 6b6623ae72 [Fix, Test] (MG_Backend, MG_Impl, MG_State): advertise the uniform-block and stencil-texturing strings a 4.0 context hides behind, forward DEPTH_STENCIL_TEXTURE_MODE to both backends, and stop a reserved transform feedback name passing for an object 2026-08-12 15:12:01 -04:00
swung0x48 a6e029734b [Test, Doc] (MG_IntegrationTest, MG_Backend/DirectGLES): review wave - stop the skip paths leaking their GL objects, normalise the clip enables a neighbouring scenario leaves behind, and state what the software lanes cannot falsify 2026-08-12 13:57:32 -04:00
swung0x48 e7d6bfddac [Fix, Test] (MG_Backend/DirectGLES, MG_State, MG_Util): publish the real default framebuffer depth/stencil format, forward the clip distance enables, and stop trusting a stale format probe for non-2D depth attachments 2026-08-12 13:34:39 -04:00
swung0x48 f2c879528f Merge branch "feat/es-ds-readback" into dev 2026-08-12 12:12:49 -04:00
Swung0x48 eaba4ac1dc [Fix, Docs] (MG_Backend/DirectGLES): drain the attachment-format probe, and document the depth/stencil readback emulation switch 2026-08-12 12:09:45 -04:00
Swung0x48 ed6578954e [Feat, Fix, Test] (MG_Backend/DirectGLES, MG_IntegrationTest): emulate ES depth/stencil readback by shader sampling, and stop the replicate blit leaking its borrowed texture unit 2026-08-12 11:38:34 -04:00
swung0x48 e005c8b6cb [Test] (tools/trace_replay): drop the minecraft-26.2-main-menu fixture 2026-08-12 11:07:38 -04:00
swung0x48 535b5e3095 Merge branch "feat/cts-xfb-respec-compressed" into dev 2026-08-12 10:35:09 -04:00
swung0x48 1b05a84928 [Fix, Test] (MG_State, MG_Backend, MG_Impl, MG_Test): review wave - hand the mapping back instead of renewing it, retire an immutable ES store on respecify, and tag glTexStorage2D levels too 2026-08-12 10:32:39 -04:00
swung0x48 7ccb762936 [Fix, Test] (MG_Impl, MG_Test): a persistently mapped unpack buffer is a legal source for the compressed uploads too 2026-08-12 09:24:57 -04:00
swung0x48 442e7eec1c [Docs] (MG_IntegrationTest): the capture-buffer note described a bug that is fixed and pinned elsewhere now 2026-08-12 09:22:17 -04:00
swung0x48 2787d15706 [Fix] (MG_Impl): the compressed sub-image bounds check added two application-supplied ints 2026-08-12 09:19:50 -04:00
swung0x48 74ce58a6c7 [Fix] (MG_Backend/DirectGLES): a declined map renewal left the respecify pointing at immutable storage 2026-08-12 09:17:54 -04:00
swung0x48 bb122ebd4f [Test] (MG_IntegrationTest): the DSA enable-disable-attributes case, reassembled where it can be stepped through 2026-08-12 08:56:13 -04:00
swung0x48 9b0ed5b3af [Fix, Feat, Test] (MG_Impl, MG_Test): a specific compressed internalformat now tags the level it defines, and the compressed sub-image entry points stopped being stubs 2026-08-12 08:52:01 -04:00
swung0x48 0e31c1481b [Fix, Test] (MG_State, MG_Backend/DirectVulkan, MG_Backend/DirectGLES, MG_IntegrationTest): a respecified capture buffer left the transform feedback writing one store and the readback reading another 2026-08-12 08:42:45 -04:00
swung0x48 811f32760e [CI] (test): gate the retrace pipeline on the integration lane - build-retrace and trace-cases now need it 2026-08-12 08:41:19 -04:00
swung0x48 01381a0404 Merge branch "feat/cts-baseinstance-dsa-bufstorage" into dev 2026-08-12 08:23:57 -04:00
swung0x48 0f02b0fdb1 [Fix] (MG_Util): the POST base-instance row described an emulation that no longer exists 2026-08-12 08:22:26 -04:00
swung0x48 3be02abf47 [Fix] (MG_Impl): multisample renderbuffers ignored the integer formats own sample limit 2026-08-12 08:17:11 -04:00
swung0x48 f91d6b676c [Fix] (MG_Impl): the per-stage atomic-counter-buffer limits answered GL_INVALID_ENUM instead of their zero 2026-08-12 08:12:32 -04:00
swung0x48 1ebf191f94 [Fix, Test] (MG_Backend/DirectGLES, MG_Util): baseInstance reached the shader but never the vertex fetch 2026-08-12 08:12:27 -04:00
swung0x48 ccad803023 Merge branch "feat/cts-vab-current-attrib" into dev 2026-08-12 07:40:47 -04:00
swung0x48 21159caf31 [Fix, Test] (MG_Util, MG_Backend/DirectGLES): transform feedback captured nothing at all from an interface block 2026-08-12 07:38:50 -04:00
swung0x48 ea4819a21d [Fix, Test] (MG_State, MG_IntegrationTest): an array vertex input occupies one location per element, not one location in total 2026-08-12 07:36:12 -04:00
swung0x48 6b882b3ccf Merge branch "feat/fp64-demote-to-fp32" into dev 2026-08-12 06:42:23 -04:00
swung0x48 96bd36c50b [Docs] (README): the MOBILEGL_ADVERTISE_FP64 switch 2026-08-12 06:41:54 -04:00
swung0x48 46fbd837b3 [Fix, Test] (MG_State, MG_Impl, MG_IntegrationTest): a double uniform initializer no longer reads zero 2026-08-12 06:41:54 -04:00
swung0x48 796a57a115 [Feat, Test] (MG_Backend, MG_Util, MG_Impl): report the fp64 tier at startup, decline 64-bit vertex formats everywhere, advertise GL_ARB_gpu_shader_fp64 only on request 2026-08-12 06:25:35 -04:00
swung0x48 62a2dae5ba [Fix, Test] (MG_Impl, MG_State, MG_IntegrationTest): glUniform*d stores what the demoted shader reads 2026-08-12 06:20:11 -04:00
swung0x48 532836c058 [Feat, Test] (MG_Util): demote every 64-bit float in a shader to 32 bits, with the block layout re-derived 2026-08-12 06:11:50 -04:00
swung0x48 2fced2241b [Fix, Test] (MG_Util, MG_State, MG_Backend): the GL 4.3 vertex binding model - array vertex inputs, zero binding strides, instance divisors, and formats ES refuses 2026-08-12 05:36:00 -04:00
swung0x48 21b5fc2d92 Merge branch 'feat/cts-draw-parameters' into dev 2026-08-12 04:24:54 -04:00
swung0x48 1f753ab5fa [Test] (MG_IntegrationTest): the draw-parameter builtins, read back out of the shader the draw produced 2026-08-12 04:18:08 -04:00
swung0x48 3ed9501be5 [Fix, Feat, Test] (MG_Backend/DirectGLES, MG_Test): the draw-parameter builtins never reached the draws that carry them, and glMultiDrawArraysIndirectCount had no backend at all 2026-08-12 04:18:08 -04:00
swung0x48 7311251f30 [Fix] (MG_Util, MG_Backend/DirectVulkan): GL reads gl_BaseVertex as zero on a non-indexed draw where Vulkan's builtin hands over firstVertex 2026-08-12 04:18:07 -04:00
swung0x48 7625cf450d [Fix, Test] (MG_Impl, MG_IntegrationTest): a block count may not exceed its binding points, and an atomic-counter buffer size must survive division by them 2026-08-12 02:38:04 -04:00
swung0x48 450eb209b6 [Fix, Test] (MG_State, MG_Impl): the program interface of a separable program is its own first and last stage, not vertex and fragment 2026-08-12 02:38:04 -04:00
swung0x48 8c5c39b3c3 [Feat, Test] (MG_Impl): glBindTextures and glBindImageTextures were no-op stubs while GL_ARB_multi_bind was advertised 2026-08-12 02:38:03 -04:00
134 changed files with 17639 additions and 1233 deletions
+2
View File
@@ -344,6 +344,7 @@ jobs:
- build-linux
- test
- benchmark
- integration
permissions:
actions: write
contents: read
@@ -488,6 +489,7 @@ jobs:
needs:
- test
- benchmark
- integration
outputs:
names: ${{ steps.trace-cases.outputs.names }}
steps:
+6
View File
@@ -276,10 +276,16 @@ set(SOURCE_FILES
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RenameBuiltinShadowingFunctionsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecomposeWorkgroupVec3Pass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecoratePositionInvariantPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DemoteFloat64Pass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenXfbInterfaceBlocksPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/SplitArrayVertexInputsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroBaseVertexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/Lower1DArrayImagesPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/BakeImageFormatsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PrivateToEntryLocalPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUniformLocationsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp
+15
View File
@@ -82,6 +82,14 @@ namespace MobileGL::MG_Config {
#endif
// MOBILEGL_DISABLE_SUBGROUP: force-disable Vulkan shader subgroup support.
Bool DisableSubgroup = false;
// MOBILEGL_ADVERTISE_FP64: add GL_ARB_gpu_shader_fp64 to the advertised extension
// string. `double` in a shader always WORKS - it is narrowed to 32 bits before any
// module reaches a backend (ShaderTranspiler::DemoteFloat64Pass) - but the extension
// promises 64-bit precision, and that is the one thing the narrowing cannot deliver.
// Off by default so an application that checks the string before using doubles keeps
// its float path; on for measuring what the conformance suite makes of the demoted
// precision. See the DemoteFloat64Pass header and the "fp64" POST row.
Bool AdvertiseFp64 = false;
// MOBILEGL_MAGMA_R11G11B10F_FALLBACK: use fallback format for R11G11B10F on Vulkan.
Bool MagmaR11G11B10FFallback = false;
// MOBILEGL_MAGMA_FRAMESINFLIGHT: requested Magma frames in flight, defaulting to 3.
@@ -109,6 +117,13 @@ namespace MobileGL::MG_Config {
// per-draw glBufferSubData path instead of the persistent-mapped ring allocator
// (negative control / driver-bug escape hatch).
Bool DisableUboRing = false;
// MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION: make DirectGLES skip the native ES
// depth/stencil reads and always go through the shader-sampling emulation. Core GL
// ES has no depth or stencil readback, but some drivers accept it anyway (Mesa does,
// Adreno does not), which means the emulation is dead code on exactly the stack the
// headless suite runs on. This forces it live so the scenarios and the CTS can
// exercise the path, and gives the device an A/B lever over the same choice.
Bool EsprytForceDepthStencilReadbackEmulation = false;
// MOBILEGL_RELAXED_SEMANTICS: relax strict core-profile rules (e.g. VAO-0 draws,
// texture-name reuse after delete) even on contexts that explicitly requested a core
// profile. Without it, relaxed semantics still apply to every context that did not
+3
View File
@@ -167,6 +167,7 @@ namespace MobileGL::MG_ConfigLoader {
QueryEnvVariable("MOBILEGL_TRACE_ANGLE_VARIANT", features.TraceAngleVariant, "");
#endif
features.DisableSubgroup = QueryEnvFlag("MOBILEGL_DISABLE_SUBGROUP");
features.AdvertiseFp64 = QueryEnvFlag("MOBILEGL_ADVERTISE_FP64");
features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK");
features.MagmaFramesInFlight = QueryEnvUint32("MOBILEGL_MAGMA_FRAMESINFLIGHT", 3, 1, 64);
features.AvoidSamplerMipmapMinFilter =
@@ -175,6 +176,8 @@ namespace MobileGL::MG_ConfigLoader {
features.CoherentAsFlush = QueryEnvFlag("MOBILEGL_COHERENT_AS_FLUSH");
features.TraceSkipAutodestroy = QueryEnvFlag("MOBILEGL_TRACE_SKIP_AUTODESTROY");
features.DisableUboRing = QueryEnvFlag("MOBILEGL_DISABLE_UBO_RING");
features.EsprytForceDepthStencilReadbackEmulation =
QueryEnvFlag("MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION");
features.RelaxedSemantics = QueryEnvFlag("MOBILEGL_RELAXED_SEMANTICS");
features.SubgroupPrefixScanQuirk = QueryEnvQuirkOverride("MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN");
features.MagmaDisableBlendedDepthWriteQuirk =
+13 -3
View File
@@ -52,11 +52,15 @@
// that includes Defines.h without Log.h both tokens would silently evaluate to 0 in the
// preprocessor conditional - enabling the assert in exactly the INFO-level builds it is
// documented to be compiled out of. Log.h redefines them identically, which is legal.
//
// Severity order, ascending: DEBUG < INFO < WARN < ERROR < FATAL. MOBILEGL_LOG_ACTIVE_LEVEL
// names the lowest severity compiled in, so the production default INFO keeps I/W/E/F and
// drops only D. Any edit here must be mirrored in Log.h.
#ifndef MOBILEGL_LOG_LEVEL_DEBUG
#define MOBILEGL_LOG_LEVEL_DEBUG 0
#define MOBILEGL_LOG_LEVEL_WARN 1
#define MOBILEGL_LOG_LEVEL_ERROR 2
#define MOBILEGL_LOG_LEVEL_INFO 3
#define MOBILEGL_LOG_LEVEL_INFO 1
#define MOBILEGL_LOG_LEVEL_WARN 2
#define MOBILEGL_LOG_LEVEL_ERROR 3
#define MOBILEGL_LOG_LEVEL_FATAL 4
#endif
@@ -91,6 +95,12 @@
#endif
// =============================== Utils ================================ //
// Asserts are live in exactly the builds where MGLOG_D is live, i.e. DEBUG builds only;
// an INFO build (the production default) compiles them out. DEBUG is the lowest severity
// in the ordering above, so "ACTIVE <= DEBUG" is true only for ACTIVE == DEBUG - the same
// gate MGLOG_D uses in Log.h. That equivalence is what makes this gate survive the
// 2026-08-13 renumbering unchanged; the contract is and stays
// "INFO builds: asserts OFF; DEBUG builds: asserts ON".
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
#define MOBILEGL_ASSERT(condition, ...) \
do { \
@@ -940,6 +940,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
// picks a whole different shader for draw_buffers without
// explicit_attrib_location. DirectVulkan advertises both.
E_GL_ARB_explicit_attrib_location, E_GL_ARB_texture_multisample, E_GL_ARB_shader_image_size,
// Core since GL 3.1 and implemented for every version advertised here. The string
// matters because applications gate the ENTRY POINTS on it rather than on the
// version: a caller that finds the extension missing never resolves
// glGetUniformBlockIndex / glUniformBlockBinding, and one that then uses uniform
// blocks anyway calls through a null pointer.
E_GL_ARB_uniform_buffer_object,
// Sampling the stencil aspect through DEPTH_STENCIL_TEXTURE_MODE. Core from 4.3,
// so on a 4.0 context the string is the only way to reach it. The host ES driver
// has had the same texture parameter since ES 3.1, which every device MobileGL
// runs on provides.
E_GL_ARB_stencil_texturing,
// Advertised with GL_NUM_PROGRAM_BINARY_FORMATS = 0, which the
// extension explicitly permits. It is also the only thing that
// exposes glProgramParameteri before GL 4.1.
@@ -960,6 +971,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (MG_Util::Async::AsyncShaderCompileEnabled()) {
extensions.push_back(E_GL_KHR_parallel_shader_compile);
}
// GL_ARB_gpu_shader_fp64 is opt-in (MOBILEGL_ADVERTISE_FP64). Every `double` in a
// shader compiles and runs already - it is narrowed to 32 bits before the module
// reaches this backend - so an application that simply uses doubles needs nothing
// advertised. What the extension additionally promises is 64-bit PRECISION, which no
// mobile GPU has and the narrowing cannot fake, so advertising it by default would
// make an application that checks the string take a path MobileGL cannot honour.
if (MG_Config::Features.AdvertiseFp64) {
extensions.push_back(E_GL_ARB_gpu_shader_fp64);
}
// Only advertised when the device driver actually has usable timer queries
// (GL_EXT_disjoint_timer_query plus its entry points) and the
// MOBILEGL_DISABLE_TIMERQUERY escape hatch is off.
@@ -1002,6 +1022,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
funcsTable.GL.MultiDrawElementsIndirect = MultiDrawElementsIndirect;
funcsTable.GL.MultiDrawElementsIndirectCount = MultiDrawElementsIndirectCount;
funcsTable.GL.MultiDrawArraysIndirect = MultiDrawArraysIndirect;
funcsTable.GL.MultiDrawArraysIndirectCount = MultiDrawArraysIndirectCount;
funcsTable.GL.DrawRangeElementsBaseVertex = DrawRangeElementsBaseVertex;
funcsTable.GL.DrawRangeElements = DrawRangeElements;
funcsTable.GL.DrawElementsInstancedBaseVertexBaseInstance = DrawElementsInstancedBaseVertexBaseInstance;
File diff suppressed because it is too large Load Diff
@@ -40,6 +40,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount,
GLsizei maxdrawcount, GLsizei stride);
void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride);
void MultiDrawArraysIndirectCount(GLenum mode, const void* indirect, GLintptr drawcount, GLsizei maxdrawcount,
GLsizei stride);
void DrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type,
const void* indices, GLint basevertex);
void DrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices);
File diff suppressed because it is too large Load Diff
+122 -2
View File
@@ -82,14 +82,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
// GLES core supports only GL_PRIMITIVE_RESTART_FIXED_INDEX. Throws when the app enabled
// the arbitrary GL_PRIMITIVE_RESTART with a non-fixed index for this index type.
void CheckPrimitiveRestartSupported(GLenum indexType);
// Feed the current program's gl_BaseInstance / gl_DrawID emulation uniforms. Both are
// no-ops when the program does not read the corresponding builtin.
// Feed the current program's gl_BaseInstance / gl_DrawID / gl_BaseVertex emulation
// uniforms. All are no-ops when the program does not read the corresponding builtin.
void SetCurrentBaseInstance(Uint32 baseInstance);
void SetCurrentDrawID(Uint32 drawId);
// GL's gl_BaseVertex is the base-vertex parameter of an indexed draw and zero for every
// command that has none - including all the DrawArrays forms - so every draw path that
// does not carry one must leave this at zero rather than inherit the last draw's value.
void SetCurrentBaseVertex(Int32 baseVertex);
// True when the current program actually reads gl_DrawID, i.e. when a batched
// (single driver call) multi-draw tier would have to feed it one value for the whole
// batch and would therefore be wrong.
Bool CurrentProgramReadsDrawID();
// Same question for gl_BaseVertex: a batched multi-draw tier cannot give each sub-draw
// its own base vertex through a uniform either.
Bool CurrentProgramReadsBaseVertex();
// Both of the above, conservatively, for a caller that must decide BEFORE PrepareForDraw
// has synced the program - where "does not read it" is indistinguishable from "cannot be
// asked yet". Answers true whenever the backend twin is missing or predates the current
// link.
Bool CurrentProgramMayNeedPerSubDrawBuiltins(Bool batchCarriesBaseVertices);
template <typename StateObject, typename BackendObject>
class StateBackendObjectRegistry {
@@ -274,6 +286,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
// context loss.
Bool persistentMapped = false;
void* persistentPtr = nullptr;
// The GL store behind `id` was created with glBufferStorageEXT and is
// therefore IMMUTABLE - glBufferData cannot respecify it and it must never be
// recycled through the size-keyed buffer pool. Tracked separately from
// persistentMapped because the two come apart: a glMapBufferRange that fails
// after its glBufferStorageEXT succeeded leaves immutable storage behind with
// no map, and a respecification then has to retire the id rather than hand it
// to glBufferData, which the driver would silently refuse.
Bool immutableStorage = false;
};
// Registered as the frontend's BufferBackendOps at backend init and on
@@ -454,6 +474,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
Uint32 m_syncedConfigVersion = 0;
Array<MG_State::GLState::VertexAttributeVersion, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS>
m_syncedAttributeVersions;
// Byte shift currently baked into the instanced arrays' offsets by the baseInstance
// emulation (see SetPendingFetchBaseInstance). It is draw state, not VAO state, so it
// is deliberately NOT covered by the config version: the frontend never bumps for it.
// Kept here because it describes what was last EMITTED, which is what the next sync
// has to correct.
Uint32 m_syncedFetchBaseInstance = 0;
};
extern StateBackendObjectRegistry<MG_State::GLState::VertexArrayObject, BackendVertexArrayObject>
@@ -467,6 +493,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
void InvalidateVAOBindingCache();
// ES resets the binding to 0 when the currently bound VAO is deleted.
void NoteVAOIdDeleted(Uint id);
// baseInstance emulation for drivers without GL_EXT_base_instance. GL fetches an
// instanced array at element "floor(instance / divisor) + baseInstance", and ES has no
// way to say the "+ baseInstance" part - so it is folded into the attribute's own byte
// offset (baseInstance * stride) for every divisor'd array, which is exactly equivalent.
// Must be set BEFORE PrepareForDraw so the VAO sync sees it, and cleared after the draw
// so the next one refetches from element 0; ScopedFetchBaseInstance does both.
void SetPendingFetchBaseInstance(Uint32 baseInstance);
Uint32 GetPendingFetchBaseInstance();
class ScopedFetchBaseInstance {
public:
explicit ScopedFetchBaseInstance(Uint32 baseInstance) { SetPendingFetchBaseInstance(baseInstance); }
~ScopedFetchBaseInstance() { SetPendingFetchBaseInstance(0); }
ScopedFetchBaseInstance(const ScopedFetchBaseInstance&) = delete;
ScopedFetchBaseInstance& operator=(const ScopedFetchBaseInstance&) = delete;
};
} // namespace VertexArrayImpl
namespace TextureImpl {
@@ -656,8 +699,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
FloatVec4 m_cacheBorderColor = {0.0f, 0.0f, 0.0f, 0.0f};
Vec4<TextureSwizzleParam> m_cacheSwizzleParams = {TextureSwizzleParam::Red, TextureSwizzleParam::Green,
TextureSwizzleParam::Blue, TextureSwizzleParam::Alpha};
// GL_DEPTH_STENCIL_TEXTURE_MODE. GL_DEPTH_COMPONENT is the GL and ES default, so a
// texture that never asks for the stencil aspect never emits the call. The
// depth/stencil readback and replicate-blit emulations also write this parameter
// raw, but only ever on their own scratch textures (never on an application
// texture), so they cannot desynchronise this cache.
GLenum m_cacheDepthStencilTextureMode = GL_DEPTH_COMPONENT;
Uint16 m_syncedSamplerVersion = 0;
Uint16 m_syncedTextureParamsVersion = 0;
// Set when the driver texture underneath was regenerated and has therefore lost every
// parameter already pushed onto it: the params-version early-out has to be overridden
// once, or an unchanged version would skip the re-push forever.
Bool m_forceTextureParamsResync = false;
};
void ActivateTextureUnit(Uint unit);
@@ -1025,9 +1078,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
void SetBaseInstance(Uint32 baseInstance) const;
void SetBaseInstanceWordIndex(Int32 wordIndex) const;
void SetDrawID(Uint32 drawId) const;
void SetBaseVertex(Int32 baseVertex) const;
// True when the transpiled program kept a gl_DrawID uniform, i.e. SetDrawID
// actually reaches a shader read rather than being discarded.
Bool ReadsDrawID() const { return m_drawIdUniformLocation >= 0; }
// Same for gl_BaseVertex: only a program that reads it pays for the per-draw
// uniform write, and only such a program needs the reset after one.
Bool ReadsBaseVertex() const { return m_baseVertexUniformLocation >= 0; }
Int GetIndirectParamsBinding() const { return m_indirectParamsBinding; }
Uint GetBackendProgramId() const { return m_backendProgramId; }
// False when the last SyncToBackend could not produce a usable program (a
@@ -1065,6 +1122,28 @@ namespace MobileGL::MG_Backend::DirectGLES {
// stale as one built before a relink - while the sampler half, which really is
// re-issued per draw, needs nothing of the sort.
Uint32 GetSyncedImageUnitVersion() const { return m_syncedImageUnitVersion; }
// Whether the (unit, bound format) pairs this program's FORMAT-LESS image uniforms
// resolve to are still the ones its ESSL was generated against.
//
// A fourth condition of the same family as the three above, and the only one that
// reads live state rather than a program-side counter, because that is where the
// dependency actually is. GLSL ES requires a format layout qualifier on every image
// where desktop GLSL lets a writeonly declaration omit one, and the only correct
// qualifier is whatever glBindImageTexture named - so a declaration with no format
// is compiled against the BINDING, and a rebind to a different format makes the
// built program wrong. Keyed on the units the program's own images address (cached
// at sync, since a unit can only move by glUniform1i, which bumps the image-unit
// version above and forces a re-sync anyway), so the cost on a program with no
// format-less image - which is all but a handful - is one empty-vector test.
//
// Deliberately NOT reached from glBindImageTexture: that entry point must never
// trigger a build (same constraint as glShaderStorageBlockBinding). It moves the
// state and this comparison notices at the next Prepare, which is also what makes
// an image first bound AFTER link work.
Bool ImageUnitFormatsStillMatch() const;
// The value ImageUnitFormatsStillMatch() compares against, recomputed from live
// image-unit state. 0 when the program has no format-less image uniform.
Uint64 ComputeImageUnitFormatSignature() const;
private:
void CacheResourceLocations(const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject);
@@ -1077,6 +1156,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
Uint m_backendGlobalUBOId = 0;
Int m_baseInstanceUniformLocation = -1;
Int m_drawIdUniformLocation = -1;
Int m_baseVertexUniformLocation = -1;
Int m_baseInstanceWordIndexUniformLocation = -1;
Int m_indirectParamsBinding = -1;
Uint32 m_snormFallbackClampOutputMask = 0;
@@ -1097,6 +1177,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
BufferImpl::UboRingAllocation m_globalUboRingAllocation;
Uint32 m_syncedLinkVersion = ~0u;
Uint32 m_syncedImageUnitVersion = ~0u;
// Image units addressed by the program's FORMAT-LESS image uniforms, and the digest
// of the (unit, format) pairs the generated ESSL baked. Empty/0 for every program
// that declares a format on all of its images, which is the overwhelming majority -
// and what keeps the per-draw comparison free for them.
Vector<Int> m_formatlessImageUnits;
Uint64 m_imageUnitFormatSignature = 0;
SamplerPassMemo m_samplerPassMemo;
};
@@ -1140,6 +1226,40 @@ namespace MobileGL::MG_Backend::DirectGLES {
// already has costs nothing. 0 when nothing was ever rebound.
Uint64 ComputeShaderStorageBlockBindingSignature(
const MG_State::GLState::ProgramObject& stateProgramObject);
// Everything the image-format bake needs from one walk of a program's uniform
// reflection. GLSL ES requires a format layout qualifier on every image uniform;
// desktop GLSL lets a writeonly (or readonly) declaration omit one, and the only
// format that is CORRECT to substitute is whatever glBindImageTexture named for the
// unit that uniform addresses - so the transpile bakes it in and the build is keyed
// on it.
struct ImageFormatBakeInputs {
// Uniform name (SPIR-V spelling, i.e. an array named once, unsubscripted) to the GL
// internal format to bake. Holds only uniforms that DECLARED no format; a declared
// one is authoritative and is never overridden.
UnorderedMap<String, Uint> glFormatByUniformName;
// The same uniforms whose format SPIRV-Cross REFUSES to print for ESSL (it throws on
// its desktop-only set, which loses the stage), paired with the ESSL spelling to
// write into the emitted declaration instead. Disjoint from the map above by
// construction: a format is baked into the module or completed in the text, never
// both. r8ui - the stencil half of the packed_depth_stencil case - lands here.
UnorderedMap<String, String> esslFormatQualifierByUniformName;
// Units those uniforms address, kept so the draw path can re-read their formats
// without walking the reflection again.
Vector<Int> units;
// Digest of the (unit, format) pairs above. 0 when the program has no format-less
// image uniform, which is all but a handful.
Uint64 signature = 0;
// Array uniforms whose elements resolved to units holding DIFFERENT formats: one
// declaration carries one qualifier, so there is nothing correct to bake and they
// are dropped from the map above. Kept for diagnostics.
Vector<String> conflictedNames;
// Some format in play - declared or baked - is outside the GLSL ES core image
// format set, so the emitted ESSL needs the GL_NV_image_formats directive.
Bool needsExtendedImageFormats = false;
};
ImageFormatBakeInputs CollectImageFormatBakeInputs(
const MG_State::GLState::ProgramObject& stateProgramObject);
} // namespace PrgramImpl
namespace SamplerImpl {
+57 -23
View File
@@ -252,7 +252,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
g_resolvedTier =
ResolveTier(g_GLESCapabilities, g_GLESFuncs, MG_Config::Features.EsprytMultiDrawMode,
&g_tierResolution);
MGLOG_I("DirectGLES multi-draw: %s", g_tierResolution.c_str());
MGLOG_D("DirectGLES multi-draw: %s", g_tierResolution.c_str());
}
// Which tiers have already announced themselves, one bit per GLESMultiDrawMode.
@@ -267,24 +267,29 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
const Uint32 bit = 1u << static_cast<Uint32>(tier);
if (g_announcedTiers & bit) return;
g_announcedTiers |= bit;
MGLOG_I("DirectGLES multi-draw: first batch executed via tier \"%s\"", TierName(tier));
MGLOG_D("DirectGLES multi-draw: first batch executed via tier \"%s\"", TierName(tier));
}
// The tier this particular batch can actually take. A tier is demoted here when
// the batch's own shape - not the driver - rules it out; the compute tier keeps
// its remaining feasibility checks inside its implementation, where the data it
// has to walk is already in hand.
GLESMultiDrawMode ResolveTierForBatch(Bool programReadsDrawID, Bool hasIndexBuffer) {
GLESMultiDrawMode ResolveTierForBatch(Bool programReadsDrawID, Bool perSubDrawBaseVertex,
Bool hasIndexBuffer) {
ResolveTierOnce();
GLESMultiDrawMode tier = g_resolvedTier;
// Batched tiers issue one driver entry for the whole batch, so the emulated
// gl_DrawID uniform can only hold one value across every sub-draw. A program
// that reads gl_DrawID gets an unrolled tier, which feeds each sub-draw its
// own index (the spec's value); nothing else observes the difference.
// own index (the spec's value); nothing else observes the difference. The
// emulated gl_BaseVertex is one uniform for the same reason, so a batch whose
// sub-draws carry their own base vertices unrolls too - even the Ext tier,
// which hands the driver the whole basevertex array, can only leave ONE value
// in the uniform the shader reads.
const Bool batched = tier == GLESMultiDrawMode::Ext || tier == GLESMultiDrawMode::MultiIndirect ||
tier == GLESMultiDrawMode::Compute;
if (batched && programReadsDrawID) {
if (batched && (programReadsDrawID || perSubDrawBaseVertex)) {
tier = SupportsTier(GLESMultiDrawMode::BaseVertex) ? GLESMultiDrawMode::BaseVertex
: GLESMultiDrawMode::DrawElements;
}
@@ -371,7 +376,8 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
// ---------------------------------------------------------------------------
Bool RunIndirect(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount, const GLint* basevertex, Bool batched, Bool feedDrawID) {
GLsizei drawcount, const GLint* basevertex, Bool batched, Bool feedDrawID,
Bool feedBaseVertex) {
if (!SupportsTier(batched ? GLESMultiDrawMode::MultiIndirect : GLESMultiDrawMode::Indirect)) return false;
const SizeT indexSize = IndexTypeSize(type);
if (indexSize == 0) return false;
@@ -413,10 +419,12 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
} else {
for (GLsizei i = 0; i < drawcount; ++i) {
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
if (feedBaseVertex) SetCurrentBaseVertex(basevertex ? basevertex[i] : 0);
const SizeT commandOffset = commandBase + static_cast<SizeT>(i) * sizeof(DrawElementsIndirectCommand);
g_GLESFuncs.glDrawElementsIndirect(mode, type, reinterpret_cast<const void*>(commandOffset));
}
if (feedDrawID) SetCurrentDrawID(0);
if (feedBaseVertex) SetCurrentBaseVertex(0);
}
BufferImpl::BindBufferId(GL_DRAW_INDIRECT_BUFFER, previousIndirectBinding);
NoteTierExecuted(batched ? GLESMultiDrawMode::MultiIndirect : GLESMultiDrawMode::Indirect);
@@ -428,15 +436,17 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
// ---------------------------------------------------------------------------
Bool RunBaseVertexLoop(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount, const GLint* basevertex, Bool feedDrawID) {
GLsizei drawcount, const GLint* basevertex, Bool feedDrawID, Bool feedBaseVertex) {
if (!SupportsTier(GLESMultiDrawMode::BaseVertex)) return false;
for (GLsizei i = 0; i < drawcount; ++i) {
if (count[i] <= 0) continue;
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
if (feedBaseVertex) SetCurrentBaseVertex(basevertex ? basevertex[i] : 0);
g_GLESFuncs.glDrawElementsBaseVertex(mode, count[i], type, indices[i],
basevertex ? basevertex[i] : 0);
}
if (feedDrawID) SetCurrentDrawID(0);
if (feedBaseVertex) SetCurrentBaseVertex(0);
NoteTierExecuted(GLESMultiDrawMode::BaseVertex);
return true;
}
@@ -446,7 +456,8 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
// ---------------------------------------------------------------------------
Bool RunRebasedDrawElements(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount, const GLint* basevertex, Bool feedDrawID) {
GLsizei drawcount, const GLint* basevertex, Bool feedDrawID,
Bool feedBaseVertex) {
const SizeT indexSize = IndexTypeSize(type);
if (indexSize == 0) return false;
@@ -479,7 +490,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
const Uint8* source = ResolveSubDrawIndices(indexBuffer, indexBufferBytes, indexBufferSize, indices[i],
subDrawCount, indexSize);
if (!source) {
MGLOG_E("DirectGLES multi-draw (drawelements tier): sub-draw %d reads outside the bound index "
MGLOG_E_ONCE("DirectGLES multi-draw (drawelements tier): sub-draw %d reads outside the bound index "
"buffer; skipping the batch",
i);
return false;
@@ -500,11 +511,16 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
for (GLsizei i = 0; i < drawcount; ++i) {
if (count[i] <= 0) continue;
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
// The base vertex is folded into the rewritten index stream here, so the
// driver sees none - but gl_BaseVertex still has to report the value the
// application passed for this sub-draw.
if (feedBaseVertex) SetCurrentBaseVertex(basevertex ? basevertex[i] : 0);
g_GLESFuncs.glDrawElements(mode, count[i], GL_UNSIGNED_INT,
reinterpret_cast<const void*>(indexBase + cursor * sizeof(Uint32)));
cursor += static_cast<SizeT>(count[i]);
}
if (feedDrawID) SetCurrentDrawID(0);
if (feedBaseVertex) SetCurrentBaseVertex(0);
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, previousIndexBinding);
NoteTierExecuted(GLESMultiDrawMode::DrawElements);
return true;
@@ -580,7 +596,7 @@ void main() {
const GLuint shader = g_GLESFuncs.glCreateShader(GL_COMPUTE_SHADER);
if (shader == 0) {
MGLOG_E("DirectGLES multi-draw (compute tier): glCreateShader(GL_COMPUTE_SHADER) failed");
MGLOG_E_ONCE("DirectGLES multi-draw (compute tier): glCreateShader(GL_COMPUTE_SHADER) failed");
return false;
}
const char* source = kFlattenComputeSource;
@@ -591,14 +607,14 @@ void main() {
if (status != GL_TRUE) {
char log[1024] = {};
g_GLESFuncs.glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
MGLOG_E("DirectGLES multi-draw (compute tier): index-flattening shader failed to compile: %s", log);
MGLOG_E_ONCE("DirectGLES multi-draw (compute tier): index-flattening shader failed to compile: %s", log);
g_GLESFuncs.glDeleteShader(shader);
return false;
}
const GLuint program = g_GLESFuncs.glCreateProgram();
if (program == 0) {
MGLOG_E("DirectGLES multi-draw (compute tier): glCreateProgram failed");
MGLOG_E_ONCE("DirectGLES multi-draw (compute tier): glCreateProgram failed");
g_GLESFuncs.glDeleteShader(shader);
return false;
}
@@ -609,7 +625,7 @@ void main() {
if (status != GL_TRUE) {
char log[1024] = {};
g_GLESFuncs.glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
MGLOG_E("DirectGLES multi-draw (compute tier): index-flattening program failed to link: %s", log);
MGLOG_E_ONCE("DirectGLES multi-draw (compute tier): index-flattening program failed to link: %s", log);
g_GLESFuncs.glDeleteProgram(program);
return false;
}
@@ -619,7 +635,7 @@ void main() {
g_uDrawCount = g_GLESFuncs.glGetUniformLocation(program, "uDrawCount");
g_uTotalIndices = g_GLESFuncs.glGetUniformLocation(program, "uTotalIndices");
g_computeProgramFailed = false;
MGLOG_I("DirectGLES multi-draw: index-flattening compute program ready (id %u)", program);
MGLOG_D("DirectGLES multi-draw: index-flattening compute program ready (id %u)", program);
return true;
}
@@ -837,8 +853,15 @@ void main() {
// afterwards would mean unpicking the program, SSBO and index bindings
// PrepareForDraw just made, and a dispatch inside an open transform feedback
// span is not legal at all. On success it hands back a flattened index stream.
// A batch whose sub-draws carry their own base vertices cannot be flattened either
// when the program reads gl_BaseVertex: one draw call leaves one uniform value.
// Asked conservatively because this decision precedes PrepareForDraw - see
// CurrentProgramMayNeedPerSubDrawBuiltins. Flattening is the irreversible half:
// once the batch is one draw the values are gone, whereas declining to flatten only
// costs the unrolled tier.
FlattenedStream flattened;
if (ResolvedTier() == GLESMultiDrawMode::Compute && !CurrentProgramReadsDrawID()) {
if (ResolvedTier() == GLESMultiDrawMode::Compute &&
!CurrentProgramMayNeedPerSubDrawBuiltins(basevertex != nullptr)) {
FlattenWithCompute(mode, count, type, indices, drawcount, basevertex, flattened);
}
@@ -852,8 +875,11 @@ void main() {
return;
}
// Now that PrepareForDraw has synced the program, both questions have real answers;
// the tier choice and the per-sub-draw feeds use those, not the guess above.
const Bool feedDrawID = CurrentProgramReadsDrawID();
const GLESMultiDrawMode tier = ResolveTierForBatch(feedDrawID, hasIndexBuffer);
const Bool feedBaseVertex = basevertex != nullptr && CurrentProgramReadsBaseVertex();
const GLESMultiDrawMode tier = ResolveTierForBatch(feedDrawID, feedBaseVertex, hasIndexBuffer);
Bool drawn = false;
switch (tier) {
@@ -861,16 +887,19 @@ void main() {
drawn = RunExt(mode, count, type, indices, drawcount, basevertex);
break;
case GLESMultiDrawMode::MultiIndirect:
drawn = RunIndirect(mode, count, type, indices, drawcount, basevertex, /*batched=*/true, feedDrawID);
drawn = RunIndirect(mode, count, type, indices, drawcount, basevertex, /*batched=*/true, feedDrawID,
feedBaseVertex);
break;
case GLESMultiDrawMode::Indirect:
drawn = RunIndirect(mode, count, type, indices, drawcount, basevertex, /*batched=*/false, feedDrawID);
drawn = RunIndirect(mode, count, type, indices, drawcount, basevertex, /*batched=*/false, feedDrawID,
feedBaseVertex);
break;
case GLESMultiDrawMode::BaseVertex:
drawn = RunBaseVertexLoop(mode, count, type, indices, drawcount, basevertex, feedDrawID);
drawn = RunBaseVertexLoop(mode, count, type, indices, drawcount, basevertex, feedDrawID, feedBaseVertex);
break;
case GLESMultiDrawMode::DrawElements:
drawn = RunRebasedDrawElements(mode, count, type, indices, drawcount, basevertex, feedDrawID);
drawn = RunRebasedDrawElements(mode, count, type, indices, drawcount, basevertex, feedDrawID,
feedBaseVertex);
break;
case GLESMultiDrawMode::Compute:
// Its pre-pass ran above; reaching here means it declined this batch's shape.
@@ -883,10 +912,15 @@ void main() {
// below are the floor: a base-vertex replay where the driver has one, and the
// rewritten index stream where it does not. Both are safe for any batch these
// entry points can receive.
if (!drawn) drawn = RunBaseVertexLoop(mode, count, type, indices, drawcount, basevertex, feedDrawID);
if (!drawn) drawn = RunRebasedDrawElements(mode, count, type, indices, drawcount, basevertex, feedDrawID);
if (!drawn) {
MGLOG_E("DirectGLES multi-draw: no usable tier for a %d sub-draw batch (mode 0x%x, type 0x%x); "
drawn = RunBaseVertexLoop(mode, count, type, indices, drawcount, basevertex, feedDrawID, feedBaseVertex);
}
if (!drawn) {
drawn = RunRebasedDrawElements(mode, count, type, indices, drawcount, basevertex, feedDrawID,
feedBaseVertex);
}
if (!drawn) {
MGLOG_E_ONCE("DirectGLES multi-draw: no usable tier for a %d sub-draw batch (mode 0x%x, type 0x%x); "
"the batch was dropped",
drawcount, mode, type);
}
+200 -67
View File
@@ -417,10 +417,27 @@ namespace MobileGL::MG_Backend::DirectGLES {
result = std::regex_replace(result, pattern, "$1flat $2");
};
// Every stage that has an integer interface at all, on BOTH sides. Interpolation is
// only ever consumed at a fragment input, so the qualifier is semantically inert on
// a tessellation or geometry interface - but an ES linker still compares the two
// sides of every interface and rejects a program whose producer says `flat` and
// whose consumer does not. Covering only the stages that "need" it left exactly two
// holes, and a program that used tessellation fell into both:
// vertex `flat out uint` -> tess-control `in uint` (producer flat, consumer not)
// tess-eval `out uint` -> geometry `flat in uint` (consumer flat, producer not)
// Adreno answers "output ... interpolation mismatch with other stage" and the whole
// program fails to link, which is a draw that silently paints nothing.
//
// Adding rather than stripping, because a fragment input's `flat` is load-bearing
// (ESSL forbids an interpolated integer) and would have to be put back for the last
// stage before the fragment shader anyway - so "everything integer is flat" is the
// one rule that is consistent no matter which stages a program happens to have.
switch (shaderType) {
case GL_VERTEX_SHADER:
addFlatQualifier("out");
break;
case GL_TESS_CONTROL_SHADER:
case GL_TESS_EVALUATION_SHADER:
case GL_GEOMETRY_SHADER:
addFlatQualifier("in");
addFlatQualifier("out");
@@ -518,6 +535,92 @@ namespace MobileGL::MG_Backend::DirectGLES {
return glslCode;
}
String RequestExtendedImageFormats(String glslCode, Bool needed) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
// GLSL ES core has thirteen image formats; GL has forty. SPIRV-Cross prints whatever
// format the OpTypeImage carries and asks for no extension for it, so an r8ui or
// rg16f image - declared as such, or baked from the bound one - reaches the driver as
// a format its core language does not know. GL_NV_image_formats is the only thing
// that adds them, and it has to be requested by name.
//
// The caller decides `needed`: it knows which formats are in play (from the uniform
// reflection and the image-unit bindings) and whether the driver advertises the
// extension at all - `#extension` on an unadvertised name is itself a hard error, so
// this must never be emitted speculatively.
static constexpr const char* kDirective = "#extension GL_NV_image_formats : require\n";
static constexpr const char* kExtName = "GL_NV_image_formats";
if (!needed || glslCode.find(kExtName) != String::npos) {
return glslCode;
}
// After the #version line, which must stay first. Everything else about the header is
// order-insensitive, and ForceSupporterOutput's scan for the LAST #extension
// directive still finds whichever one that is.
const SizeT versionPos = glslCode.find("#version");
if (versionPos == String::npos) {
return kDirective + glslCode;
}
const SizeT lineEnd = glslCode.find('\n', versionPos);
if (lineEnd == String::npos) {
return glslCode + "\n" + kDirective;
}
glslCode.insert(lineEnd + 1, kDirective);
return glslCode;
}
String BakeImageFormatQualifiers(String glslCode,
const UnorderedMap<String, String>& esslFormatByUniformName) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (esslFormatByUniformName.empty() || glslCode.find("image") == String::npos) {
return glslCode;
}
// Same declaration shape RebindImageUniformsToFrontendUnits matches, and for the same
// reason: one line, one image uniform, the name in group 3.
static const std::regex imageDeclRegex(
R"((layout\s*\(([^)]*)\)\s*)?uniform\s+(?:(?:readonly|writeonly|coherent|volatile|restrict|highp|mediump|lowp)\s+)*[iu]?image[A-Za-z0-9]+\s+([A-Za-z_][A-Za-z0-9_]*)\s*(\[[^\]]*\])?\s*;)");
// Every image format spelling GLSL has, so a declaration that already carries one is
// recognised whatever it says - the caller's map is consulted only for declarations
// with NO format, never to override a written one.
static const std::regex existingFormatRegex(
R"(\b(rgba32f|rgba16f|rg32f|rg16f|r11f_g11f_b10f|r32f|r16f|rgba16|rgb10_a2|rg16|rg8|r16|r8|rgba16_snorm|rgba8_snorm|rg16_snorm|rg8_snorm|r16_snorm|r8_snorm|rgba32i|rgba16i|rgba8i|rg32i|rg16i|rg8i|r32i|r16i|r8i|rgba32ui|rgba16ui|rgba8ui|rgb10_a2ui|rg32ui|rg16ui|rg8ui|r32ui|r16ui|r8ui)\b)");
String result;
result.reserve(glslCode.size());
SizeT lineStart = 0;
while (lineStart <= glslCode.size()) {
const SizeT lineEnd = glslCode.find('\n', lineStart);
const Bool lastLine = lineEnd == String::npos;
String line = glslCode.substr(lineStart, lastLine ? String::npos : lineEnd - lineStart);
std::smatch match;
if (std::regex_search(line, match, imageDeclRegex)) {
const String name = match[3].str();
const auto formatIt = esslFormatByUniformName.find(name);
const String layoutContents = match[2].matched ? match[2].str() : String();
if (formatIt != esslFormatByUniformName.end() && !formatIt->second.empty() &&
!std::regex_search(layoutContents, existingFormatRegex)) {
if (match[1].matched) {
const SizeT layoutOpen = line.find('(', match.position(1));
line.insert(layoutOpen + 1, formatIt->second + ", ");
} else {
line.insert(match.position(0), "layout(" + formatIt->second + ") ");
}
}
}
result += line;
if (lastLine) {
break;
}
result += '\n';
lineStart = lineEnd + 1;
}
return result;
}
String RemoveLayoutBinding(const String& glslCode) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
@@ -1073,7 +1176,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
for (GLenum err = g_GLESFuncs.glGetError(); err != GL_NO_ERROR; err = g_GLESFuncs.glGetError()) {
MGLOG_E("-> GLES Error: %s", MG_Util::ConvertGLEnumToString(err).c_str());
MGLOG_D("-> GLES Error: %s", MG_Util::ConvertGLEnumToString(err).c_str());
}
}
@@ -1493,88 +1596,71 @@ namespace MobileGL::MG_Backend::DirectGLES {
return (rowBytes + align - 1) / align * align;
}
// Repacks wide RGBA(_INTEGER) rows into the client's (format, type) layout, honoring the
// client-side PACK parameters and the bound pixel-pack buffer. `wide` holds
// `sliceHeight * sliceCount` rows of `width` texels (slice-major, tightly stacked),
// 4 components x GetReadbackComponentSize(wideType) bytes each.
// Walks the client-side destination the PACK parameters describe and hands each row to
// `fillRow(slice, row, dstRow)`, which writes width * dstPixelBytes bytes of finished client
// texels. Shared by the converting and the raw-word stores so both address the destination -
// and feed the bound pixel-pack buffer - identically.
// applyPackImageParams: GL_PACK_IMAGE_HEIGHT / GL_PACK_SKIP_IMAGES apply only to GetTexImage
// of 3D/array images; ReadPixels and 2D GetTexImage ignore them (GL 3.3 sections 4.3.1, 6.1.4).
// Per the GL addressing rules, slice k row j lands at
// SKIP_IMAGES*imageStride + SKIP_ROWS*rowStride + SKIP_PIXELS*pixelBytes
// + k*imageStride + j*rowStride, with imageStride = max(IMAGE_HEIGHT, sliceHeight)*rowStride.
Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei sliceHeight,
GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type,
void* pixels, Bool applyPackImageParams) {
const SizeT dstPixelBytes = GetReadbackDstPixelSize(mapping, type);
if (dstPixelBytes == 0) {
return false;
}
PackedReadbackLayout packedLayout{};
const Bool isPackedType = GetPackedReadbackLayout(type, packedLayout);
const SizeT dstComponentSize = GetReadbackComponentSize(type);
template <typename FillRow>
static Bool StoreClientRows(SizeT dstPixelBytes, SizeT swapGroupSize, GLsizei width, GLsizei sliceHeight,
GLsizei sliceCount, void* pixels, Bool applyPackImageParams, FillRow&& fillRow) {
const auto& pixelPackBufferObject =
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
const auto& pixelPackBufferObject =
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
// Destination layout is computed from the client-side PACK parameters; only the actual pixel
// rows are written so skip regions of the destination stay untouched.
const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);
const SizeT rowPixels = static_cast<SizeT>(packParams.RowLength > 0 ? packParams.RowLength : width);
const SizeT dstRowStride = AlignReadbackRow(rowPixels * dstPixelBytes, packParams.Alignment);
const SizeT imageRows =
applyPackImageParams && packParams.ImageHeight > 0
? static_cast<SizeT>(packParams.ImageHeight)
: static_cast<SizeT>(sliceHeight);
const SizeT dstImageStride = imageRows * dstRowStride;
const SizeT skipImages =
applyPackImageParams ? static_cast<SizeT>(std::max(packParams.SkipImages, 0)) : SizeT{0};
const SizeT dstSkipOffset = skipImages * dstImageStride +
static_cast<SizeT>(std::max(packParams.SkipRows, 0)) * dstRowStride +
static_cast<SizeT>(std::max(packParams.SkipPixels, 0)) * dstPixelBytes;
const SizeT dstRowBytes = static_cast<SizeT>(width) * dstPixelBytes;
// Destination layout is computed from the client-side PACK parameters; only the actual pixel
// rows are written so skip regions of the destination stay untouched.
const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);
const SizeT rowPixels = static_cast<SizeT>(packParams.RowLength > 0 ? packParams.RowLength : width);
const SizeT dstRowStride = AlignReadbackRow(rowPixels * dstPixelBytes, packParams.Alignment);
const SizeT imageRows =
applyPackImageParams && packParams.ImageHeight > 0
? static_cast<SizeT>(packParams.ImageHeight)
: static_cast<SizeT>(sliceHeight);
const SizeT dstImageStride = imageRows * dstRowStride;
const SizeT skipImages =
applyPackImageParams ? static_cast<SizeT>(std::max(packParams.SkipImages, 0)) : SizeT{0};
const SizeT dstSkipOffset = skipImages * dstImageStride +
static_cast<SizeT>(std::max(packParams.SkipRows, 0)) * dstRowStride +
static_cast<SizeT>(std::max(packParams.SkipPixels, 0)) * dstPixelBytes;
const SizeT dstRowBytes = static_cast<SizeT>(width) * dstPixelBytes;
const SizeT pboBaseOffset = reinterpret_cast<SizeT>(pixels); // with a PBO, `pixels` is an offset
if (pixelPackBufferObject) {
const SizeT requiredSize = pboBaseOffset + dstSkipOffset +
static_cast<SizeT>(sliceCount - 1) * dstImageStride +
static_cast<SizeT>(sliceHeight - 1) * dstRowStride + dstRowBytes;
if (requiredSize > pixelPackBufferObject->GetSize()) {
MGLOG_E("Readback conversion: pixel pack buffer is too small");
return true;
const SizeT pboBaseOffset = reinterpret_cast<SizeT>(pixels); // with a PBO, `pixels` is an offset
if (pixelPackBufferObject) {
const SizeT requiredSize = pboBaseOffset + dstSkipOffset +
static_cast<SizeT>(sliceCount - 1) * dstImageStride +
static_cast<SizeT>(sliceHeight - 1) * dstRowStride + dstRowBytes;
if (requiredSize > pixelPackBufferObject->GetSize()) {
MGLOG_E_ONCE("Readback conversion: pixel pack buffer is too small");
return true;
}
}
}
const SizeT srcComponentSize = GetReadbackComponentSize(wideType);
const SizeT srcPixelBytes = 4 * srcComponentSize;
Vector<Uint8> convertedRow(dstRowBytes);
Vector<Uint8> convertedRow(dstRowBytes);
for (GLsizei slice = 0; slice < sliceCount; ++slice) {
for (GLsizei row = 0; row < sliceHeight; ++row) {
const SizeT flatRow = static_cast<SizeT>(slice) * static_cast<SizeT>(sliceHeight) +
static_cast<SizeT>(row);
const Uint8* srcRow = wide + flatRow * static_cast<SizeT>(width) * srcPixelBytes;
ConvertWideReadbackRow(srcRow, convertedRow.data(), static_cast<SizeT>(width), wideType,
mapping, type);
for (GLsizei slice = 0; slice < sliceCount; ++slice) {
for (GLsizei row = 0; row < sliceHeight; ++row) {
fillRow(slice, row, convertedRow.data());
if (packParams.SwapBytes) {
const SizeT groupSize = isPackedType ? packedLayout.byteSize : dstComponentSize;
if (groupSize > 1) {
for (SizeT offset = 0; offset + groupSize <= dstRowBytes; offset += groupSize) {
std::reverse(convertedRow.data() + offset, convertedRow.data() + offset + groupSize);
if (packParams.SwapBytes && swapGroupSize > 1) {
for (SizeT offset = 0; offset + swapGroupSize <= dstRowBytes; offset += swapGroupSize) {
std::reverse(convertedRow.data() + offset, convertedRow.data() + offset + swapGroupSize);
}
}
}
const SizeT dstOffset = dstSkipOffset + static_cast<SizeT>(slice) * dstImageStride +
static_cast<SizeT>(row) * dstRowStride;
if (pixelPackBufferObject) {
pixelPackBufferObject->WritebackFromBackend({convertedRow.data(), dstRowBytes},
pboBaseOffset + dstOffset);
} else {
Memcpy(static_cast<Uint8*>(pixels) + dstOffset, convertedRow.data(), dstRowBytes);
const SizeT dstOffset = dstSkipOffset + static_cast<SizeT>(slice) * dstImageStride +
static_cast<SizeT>(row) * dstRowStride;
if (pixelPackBufferObject) {
pixelPackBufferObject->WritebackFromBackend({convertedRow.data(), dstRowBytes},
pboBaseOffset + dstOffset);
} else {
Memcpy(static_cast<Uint8*>(pixels) + dstOffset, convertedRow.data(), dstRowBytes);
}
}
}
}
if (pixelPackBufferObject) {
// WritebackFromBackend bumps change serials with no backend op; re-open
// the buffer draw-clean memos (once for the whole row loop).
@@ -1582,5 +1668,52 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
return true;
}
// Repacks wide RGBA(_INTEGER) rows into the client's (format, type) layout, honoring the
// client-side PACK parameters and the bound pixel-pack buffer. `wide` holds
// `sliceHeight * sliceCount` rows of `width` texels (slice-major, tightly stacked),
// 4 components x GetReadbackComponentSize(wideType) bytes each.
Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei sliceHeight,
GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type,
void* pixels, Bool applyPackImageParams) {
const SizeT dstPixelBytes = GetReadbackDstPixelSize(mapping, type);
if (dstPixelBytes == 0) {
return false;
}
PackedReadbackLayout packedLayout{};
const Bool isPackedType = GetPackedReadbackLayout(type, packedLayout);
const SizeT swapGroupSize = isPackedType ? packedLayout.byteSize : GetReadbackComponentSize(type);
const SizeT srcPixelBytes = 4 * GetReadbackComponentSize(wideType);
return StoreClientRows(dstPixelBytes, swapGroupSize, width, sliceHeight, sliceCount, pixels,
applyPackImageParams,
[&](GLsizei slice, GLsizei row, Uint8* dstRow) {
const SizeT flatRow = static_cast<SizeT>(slice) *
static_cast<SizeT>(sliceHeight) +
static_cast<SizeT>(row);
const Uint8* srcRow =
wide + flatRow * static_cast<SizeT>(width) * srcPixelBytes;
ConvertWideReadbackRow(srcRow, dstRow, static_cast<SizeT>(width), wideType,
mapping, type);
});
}
Bool StorePackedWordsToClient(const Uint8* srcWords, GLsizei width, GLsizei sliceHeight, GLsizei sliceCount,
GLenum type, void* pixels, Bool applyPackImageParams) {
PackedReadbackLayout packedLayout{};
if (!GetPackedReadbackLayout(type, packedLayout) || packedLayout.byteSize != 4) {
return false;
}
const SizeT srcRowBytes = static_cast<SizeT>(width) * 4;
return StoreClientRows(4, packedLayout.byteSize, width, sliceHeight, sliceCount, pixels,
applyPackImageParams,
[&](GLsizei slice, GLsizei row, Uint8* dstRow) {
const SizeT flatRow = static_cast<SizeT>(slice) *
static_cast<SizeT>(sliceHeight) +
static_cast<SizeT>(row);
Memcpy(dstRow, srcWords + flatRow * srcRowBytes, srcRowBytes);
});
}
} // namespace ReadbackImpl
} // namespace MobileGL::MG_Backend::DirectGLES
+30
View File
@@ -115,6 +115,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei sliceHeight,
GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type,
void* pixels, Bool applyPackImageParams);
// Stores packed 32-bit source words verbatim, with the same destination addressing, PACK
// parameters and pixel-pack-buffer handling as StoreWideRowsToClient. For the sources whose
// storage word already IS the client word (MG_Util::IsRawPackedPixelTransfer): routing those
// through the wide float intermediate re-encodes them, and the RGB9_E5 encoder canonicalizes
// the shared exponent, so glGetTexImage would answer with different bits than were stored.
// `srcWords` holds sliceHeight * sliceCount tightly stacked rows of `width` 32-bit words.
// False when `type` is not a 4-byte packed type.
Bool StorePackedWordsToClient(const Uint8* srcWords, GLsizei width, GLsizei sliceHeight, GLsizei sliceCount,
GLenum type, void* pixels, Bool applyPackImageParams);
} // namespace ReadbackImpl
namespace PrgramImpl {
@@ -137,6 +147,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
// ES 3.2 needs no directive at all and an EXT driver already has the right one.
String RetargetTextureBufferExtension(String glslCode,
MG_External::GLESCapabilities::TextureBufferTier tier);
// Adds `#extension GL_NV_image_formats : require` when the shader carries an image
// format qualifier GLSL ES has no core spelling for. SPIRV-Cross prints the format and
// asks for nothing, so the request has to be made here. `needed` is the caller's answer,
// because only it knows which formats are in play AND whether the driver advertises the
// extension - requesting an unadvertised extension is itself a compile error, so this is
// never emitted speculatively. A no-op when not needed or already present.
String RequestExtendedImageFormats(String glslCode, Bool needed);
// Writes a format layout qualifier into the image declarations named in
// `esslFormatByUniformName` that still have none. The completion half of the image-format
// bake, and ONLY that: the SPIR-V pass (BakeImageFormatsPass) is what normally puts the
// format in, but SPIRV-Cross throws rather than printing the formats it calls
// desktop-only when it targets ESSL - r8ui among them, which is what the stencil half of
// KHR-GL4x.packed_depth_stencil.stencil_texturing binds - and a throw loses the whole
// stage. So those formats stay out of the module and are spelled here instead, on the
// emitted text, where nothing can refuse them.
//
// Declarations that already carry a format are left exactly as they are, whoever wrote
// it. Must run before RemoveLayoutBinding, which is where an image's layout qualifier
// stops being safe to edit by hand.
String BakeImageFormatQualifiers(String glslCode, const UnorderedMap<String, String>& esslFormatByUniformName);
String RemoveLayoutBinding(const String& glslCode);
// Prefix of the writeonly half a read+write image uniform is split into (see
// SplitReadWriteImageUniforms); the suffix is the image's own name.
@@ -517,6 +517,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
E_GL_ARB_gpu_shader_int64, E_GL_KHR_debug, E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind,
E_GL_ARB_shading_language_420pack, E_GL_ARB_vertex_attrib_binding, E_GL_ARB_shader_image_size,
E_GL_ARB_explicit_attrib_location,
// Core since GL 3.1 and implemented for every version advertised here. The string
// matters because applications gate the ENTRY POINTS on it rather than on the
// version: a caller that finds the extension missing never resolves
// glGetUniformBlockIndex / glUniformBlockBinding, and one that then uses uniform
// blocks anyway calls through a null pointer.
E_GL_ARB_uniform_buffer_object,
// Sampling the stencil aspect through DEPTH_STENCIL_TEXTURE_MODE. Core from 4.3,
// so on a 4.0 context the string is the only way to reach it.
E_GL_ARB_stencil_texturing,
// Advertised with GL_NUM_PROGRAM_BINARY_FORMATS = 0, which the
// extension explicitly permits. It is also the only thing that
// exposes glProgramParameteri before GL 4.1.
@@ -539,6 +548,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (MG_Util::Async::AsyncShaderCompileEnabled()) {
extensions.push_back(E_GL_KHR_parallel_shader_compile);
}
// GL_ARB_gpu_shader_fp64 is opt-in (MOBILEGL_ADVERTISE_FP64). Every `double` in a
// shader compiles and runs already - it is narrowed to 32 bits before the module
// reaches this backend - so an application that simply uses doubles needs nothing
// advertised. What the extension additionally promises is 64-bit PRECISION, which no
// mobile GPU has and the narrowing cannot fake, so advertising it by default would
// make an application that checks the string take a path MobileGL cannot honour.
if (MG_Config::Features.AdvertiseFp64) {
extensions.push_back(E_GL_ARB_gpu_shader_fp64);
}
// GL_ARB_timer_query gates MC's F3 GPU% (LWJGL checks the extension string);
// only advertised when the device actually supports timestamp queries and the
// MOBILEGL_DISABLE_TIMERQUERY escape hatch is off.
@@ -877,7 +895,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::TextureCubeMapArray);
}
}
m_dynamicParameters.SupportsFloat64VertexAttributes = m_vulkanCaps.SupportsShaderFloat64;
// Never, on any device, and no longer for the reason it used to be. It used to track
// shaderFloat64 because a `dvec3` input needed the Float64 capability to exist in the
// module at all; a 64-bit vertex FETCH was already impossible (VK_FORMAT_R64*_SFLOAT is
// optional and lavapipe reports zero bufferFeatures for all four), so the attribute
// arrived as its 32-bit word pair and PackDoubleVertexInputsPass bitcast it back.
//
// The shader half of that is gone: every 64-bit float is narrowed before any module
// reaches a backend (ShaderTranspiler::DemoteFloat64Pass), so there is no `double` input
// left to bitcast INTO, and feeding a UINT-formatted attribute to what is now a `float`
// input would be silent garbage. Reconstructing the value would mean decoding the
// IEEE-754 double bit pattern in the shader - software fp64, which is precisely what the
// demotion exists to avoid - and on Espryt it would additionally need the ES driver to
// fetch 2N uint components where the application declared N doubles, which a dvec3 or
// dvec4 cannot even express within one attribute location.
//
// So glVertexAttribLFormat / glVertexAttribLPointer are declined here exactly as they
// already were on Espryt and on every real mobile device (Adreno and Mali both report
// shaderFloat64 == VK_FALSE), and for the same visible reason. A `dvec3` INPUT still
// compiles and draws - it is a `vec3` after demotion - as long as the application feeds
// it with glVertexAttribPointer(GL_FLOAT) rather than 64-bit data.
m_dynamicParameters.SupportsFloat64VertexAttributes = false;
m_dynamicParameters.MaxShaderStorageBlockSize =
std::min(m_vulkanCaps.MaxShaderStorageBlockSize, kMaxAdvertisedShaderStorageBlockSize);
if (m_vulkanCaps.SupportsShaderSubgroup) {
@@ -269,14 +269,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
drawBuffer->SyncPersistentMappedRange();
const SizeT commandOffset = reinterpret_cast<SizeT>(indirect);
if (drawBuffer->MappedData() == nullptr || commandOffset + requiredBytes > drawBuffer->GetSize()) {
MGLOG_E("%s skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range", label);
MGLOG_E_ONCE("%s skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range", label);
return nullptr;
}
return drawBuffer->MappedData() + commandOffset;
}
if (!indirect) {
MGLOG_E("%s skipped: indirect pointer is null", label);
MGLOG_E_ONCE("%s skipped: indirect pointer is null", label);
return nullptr;
}
@@ -398,7 +398,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
stride = sizeof(DrawArraysIndirectCommand);
}
if (stride < static_cast<GLsizei>(sizeof(DrawArraysIndirectCommand))) {
MGLOG_E("MultiDrawArraysIndirect skipped: stride %d is smaller than command size %zu",
MGLOG_E_ONCE("MultiDrawArraysIndirect skipped: stride %d is smaller than command size %zu",
stride, sizeof(DrawArraysIndirectCommand));
return;
}
@@ -446,20 +446,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
stride = sizeof(DrawArraysIndirectCommand);
}
if (stride < static_cast<GLsizei>(sizeof(DrawArraysIndirectCommand))) {
MGLOG_E("MultiDrawArraysIndirectCount skipped: stride %d is smaller than command size %zu",
MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: stride %d is smaller than command size %zu",
stride, sizeof(DrawArraysIndirectCommand));
return;
}
auto parameterBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();
if (!parameterBuffer || drawcount < 0 || static_cast<SizeT>(drawcount) + sizeof(Uint32) > parameterBuffer->GetSize()) {
MGLOG_E("MultiDrawArraysIndirectCount skipped: invalid GL_PARAMETER_BUFFER binding or range");
MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: invalid GL_PARAMETER_BUFFER binding or range");
return;
}
parameterBuffer->SyncPersistentMappedRange();
if (parameterBuffer->MappedData() == nullptr) {
MGLOG_E("MultiDrawArraysIndirectCount skipped: CPU fallback cannot read parameter buffer");
MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: CPU fallback cannot read parameter buffer");
return;
}
@@ -513,7 +513,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
if (indexSize == 0) {
MGLOG_E("DrawElementsIndirect skipped: unsupported index type 0x%x", type);
MGLOG_E_ONCE("DrawElementsIndirect skipped: unsupported index type 0x%x", type);
return;
}
@@ -1009,7 +1009,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// shift - the hardware divide was the hottest instruction of this loop.
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
if (indexSize == 0) {
MGLOG_E("MultiDrawElements skipped: unsupported index type 0x%x", type);
MGLOG_E_ONCE("MultiDrawElements skipped: unsupported index type 0x%x", type);
return;
}
const Uint32 indexSizeShift = static_cast<Uint32>(std::countr_zero(indexSize));
@@ -205,7 +205,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// commands away. The device is gone on that path anyway - stay silent-safe
// rather than trade a lost device for a barrier into a closed buffer.
if (frame.hasCommandBufferRecorded) {
MGLOG_E("TransitionToPresent: command buffer already closed; skipping the present barrier");
MGLOG_E_ONCE("TransitionToPresent: command buffer already closed; skipping the present barrier");
return false;
}
@@ -259,7 +259,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// is the correct price for a broken pipeline and is bounded by the draw itself being
// skipped.
if (pipeline == VK_NULL_HANDLE) {
MGLOG_I("PipelineFactory::GetOrCreatePipeline: creation failed for hash=0x%llx "
// Unlatched, like the CreatePipeline report it accompanies: a pipeline MobileGL
// assembled and the driver refused is a broken invariant, not an expected failure,
// so it stays loud for as long as it is reachable. Raised from MGLOG_I once the
// Log.h ordering fix made MGLOG_E live in INFO builds.
MGLOG_E("PipelineFactory::GetOrCreatePipeline: creation failed for hash=0x%llx "
"programHash=0x%llx; not caching the failure",
static_cast<unsigned long long>(hash),
static_cast<unsigned long long>(payload.programHash));
@@ -471,9 +475,56 @@ namespace MobileGL::MG_Backend::DirectVulkan {
blend.attachmentCount = payload.colorAttachmentCount;
blend.pAttachments = colorAttachments.empty() ? nullptr : colorAttachments.data();
// A GL program may have a tessellation EVALUATION stage and no CONTROL stage: GL 4.6 core
// 11.2.2 gives it a fixed-function pass-through instead. Vulkan has no such stage, and
// VUID-VkGraphicsPipelineCreateInfo-pStages-00730 requires both tessellation stages or
// neither - so the renderer synthesizes the pass-through GL describes and hands it in
// here (see ProgramFactory::GetOrCreatePassthroughTessControlStage).
//
// The refusal below is what keeps the half-tessellated shape away from the driver when
// there is no synthesized stage to add - because Mali does not reject it, it dereferences
// null INSIDE vkCreateGraphicsPipelines and takes the process down (SIGSEGV, fault addr
// 0x34, on Mali-G715/r54p2 and Mali-G925/r49p1 alike; Adreno and lavapipe merely render
// wrong). Returning VK_NULL_HANDLE routes this through the same path a driver rejection
// takes: the draw is skipped, nothing is memoised, and the process survives.
const Vector<VkPipelineShaderStageCreateInfo>* effectiveStages = payload.stages;
Vector<VkPipelineShaderStageCreateInfo> stagesWithPassthrough;
if (payload.passthroughTessControlStage.module != VK_NULL_HANDLE) {
stagesWithPassthrough = *payload.stages;
stagesWithPassthrough.push_back(payload.passthroughTessControlStage);
effectiveStages = &stagesWithPassthrough;
}
{
VkShaderStageFlags stagesPresent = 0;
for (const auto& stageInfo : *effectiveStages) {
stagesPresent |= stageInfo.stage;
}
const Bool hasTessControl = (stagesPresent & VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT) != 0;
const Bool hasTessEval = (stagesPresent & VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT) != 0;
if (hasTessControl != hasTessEval) {
// Latched, and the latch is the point: a failed creation is deliberately never
// memoised (see GetOrCreatePipeline), so a program in this state re-enters here
// once per draw, every frame - and a refusal diagnostic that repeats per draw is
// noise, not a diagnostic. One line names the program; the draws it explains are
// all the same draw.
static Bool s_warnedHalfTessellatedPipeline = false;
if (!s_warnedHalfTessellatedPipeline) {
s_warnedHalfTessellatedPipeline = true;
MGLOG_E_ONCE("PipelineFactory::CreatePipeline: refusing a pipeline with %s tessellation stage and "
"no %s stage (VUID-VkGraphicsPipelineCreateInfo-pStages-00730). programHash=0x%llx "
"patchControlPoints=%u. Its draws are skipped; logged once.",
hasTessEval ? "an evaluation" : "a control",
hasTessEval ? "control" : "evaluation",
static_cast<unsigned long long>(payload.programHash),
payload.patchControlPoints);
}
return VK_NULL_HANDLE;
}
}
VkGraphicsPipelineCreateInfo gpi{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO};
gpi.stageCount = static_cast<Uint32>(payload.stages->size());
gpi.pStages = payload.stages->data();
gpi.stageCount = static_cast<Uint32>(effectiveStages->size());
gpi.pStages = effectiveStages->data();
gpi.pVertexInputState = payload.vertexInputState;
gpi.pInputAssemblyState = &ia;
gpi.pTessellationState =
@@ -490,6 +541,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkPipeline pipeline = VK_NULL_HANDLE;
const VkResult result = vkCreateGraphicsPipelines(m_device, m_pipelineCache, 1, &gpi, nullptr, &pipeline);
// Loud, at MGLOG_F, and deliberately NOT latched. vkCreateGraphicsPipelines refusing a
// pipeline MobileGL assembled is a should-never-happen state, and the driver's own
// answer is VK_ERROR_UNKNOWN - no information at all - so this dump is the entire
// diagnosis. It is not an expected failure mode, so the one-shot rule that quiets W/E
// does not apply: while this is reachable it should keep saying so on every draw.
// GetOrCreatePipeline deliberately does not cache the failure, which is what makes that
// repetition happen; if the repetition ever needs to stop, fix the pipeline, not the log.
if (result != VK_SUCCESS) {
MGLOG_F("PipelineFactory::CreatePipeline failed: result=%s (%d) programHash=0x%llx vertexInputHash=0x%llx stageCount=%u topology=%s(%d) colorAttachmentCount=%u samples=%s(%d) subpass=%u",
VkResultToString(result),
@@ -522,8 +580,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
payload.vertexInputState->vertexAttributeDescriptionCount);
// The driver's own answer is VK_ERROR_UNKNOWN, i.e. no information at all, so the only
// way to work out WHICH shader it choked on (the open sampler-array-in-struct
// investigation) is to name the modules. MGLOG_I, not _D/_E: this must survive in the
// INFO-level builds that CTS actually runs against.
// investigation) is to name the modules. MGLOG_I, not _D: this is part of a
// should-never-happen report and must survive in the INFO-level builds that CTS
// actually runs against, alongside the MGLOG_F lines above.
if (payload.stageSpirvDigests) {
for (SizeT i = 0; i < payload.stageSpirvDigests->size(); ++i) {
const auto& digest = (*payload.stageSpirvDigests)[i];
@@ -71,6 +71,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool fragmentReplacesDepth = false;
Array<VkPipelineColorBlendAttachmentState, kMaxColorAttachments> colorBlendAttachments{};
const Vector<VkPipelineShaderStageCreateInfo>* stages = nullptr;
// The tessellation control stage this renderer synthesized for a program that has
// an evaluation stage and none of its own (GL 4.6 core 11.2.2 gives such a program a
// fixed-function pass-through; Vulkan has no such thing and
// VUID-VkGraphicsPipelineCreateInfo-pStages-00730 forbids the half-tessellated
// pipeline outright). Appended to `stages` at creation. A null module means the
// renderer could not build one, and CreatePipeline refuses the pipeline - the same
// refusal it applies when `stages` itself is half-tessellated.
//
// NOT hashed: it is a pure function of the program and of patchControlPoints, both
// of which ComputeHash already mixes in.
VkPipelineShaderStageCreateInfo passthroughTessControlStage{};
const VkPipelineVertexInputStateCreateInfo* vertexInputState = nullptr;
// Diagnostic only; may be null. Read solely from the pipeline-creation failure path.
const Vector<ShaderStageSpirvDigest>* stageSpirvDigests = nullptr;
@@ -376,12 +376,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
spv_diagnostic diagnostic = nullptr;
const spv_result_t result = spvValidateWithOptions(context, options, &binary, &diagnostic);
if (result != SPV_SUCCESS) {
// MGLOG_I, not E: at the INFO compile level of the CI/test lanes that arm
// the validation switch, MGLOG_E is compiled out (Log.h orders
// DEBUG < WARN < ERROR < INFO) and the VUID would never reach a log. The
// latch is what a test harness asserts on.
// MGLOG_E, unlatched: reaching here already requires the validation switch to
// be armed, which bounds the volume, and each VUID names a different defect.
// (Parked at MGLOG_I until the Log.h level ordering was fixed, when E was
// compiled out of every INFO build.) The latch is what a test harness asserts on.
MG_Util::ShaderTranspiler::ShaderCompiler::NoteSpirvValidationFailure();
MGLOG_I(
MGLOG_E(
"ProgramFactory::ValidateTransformedSpirv: validation failed for stage=%d program=%u result=%d index=%zu msg=%s",
static_cast<Int>(shaderStage),
programExternalIndex,
@@ -1266,7 +1266,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
for (SizeT i = 1; i < group.offsets.size(); ++i) {
if (group.elementBytes == 0 ||
group.offsets[i] != group.offsets[i - 1] + group.elementBytes) {
MGLOG_I("XfbCaptureDecoratePass: block member %u of type %%%u is captured with a "
MGLOG_D("XfbCaptureDecoratePass: block member %u of type %%%u is captured with a "
"non-contiguous element set; the capture layout will differ from GL's",
key.second, key.first);
break;
@@ -1721,6 +1721,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return ProgramFactory::DescriptorBindingKind::CombinedImageSampler;
case SPV_REFLECT_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER:
return ProgramFactory::DescriptorBindingKind::UniformTexelBuffer;
case SPV_REFLECT_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER:
return ProgramFactory::DescriptorBindingKind::StorageTexelBuffer;
case SPV_REFLECT_DESCRIPTOR_TYPE_STORAGE_BUFFER:
return ProgramFactory::DescriptorBindingKind::StorageBuffer;
case SPV_REFLECT_DESCRIPTOR_TYPE_STORAGE_IMAGE:
@@ -1750,6 +1752,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
if (kind == ProgramFactory::DescriptorBindingKind::CombinedImageSampler ||
kind == ProgramFactory::DescriptorBindingKind::UniformTexelBuffer ||
kind == ProgramFactory::DescriptorBindingKind::StorageTexelBuffer ||
kind == ProgramFactory::DescriptorBindingKind::StorageImage) {
const auto arraySuffix = name.find("[0]");
if (arraySuffix != String::npos) {
@@ -1839,8 +1842,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// UniformManager::BindProgramUniformBuffers: UBO instance arrays
// (uniform Block {...} b[N];), storage-block instance arrays, image uniform
// arrays, and combined-image-sampler arrays (uniform sampler2D s[N];).
// Anything else - a uniform TEXEL buffer array is the one remaining kind -
// must fail program creation cleanly rather than continue with corrupt state.
// Anything else - the two TEXEL buffer kinds are what remain, samplerBuffer[N]
// and imageBuffer[N] - must fail program creation cleanly rather than continue
// with corrupt state. Their per-draw path writes pTexelBufferView as the
// address of a vector element sized for one descriptor per binding, so an
// array would not merely be unresolved, it would dangle.
//
// Getting listed here is not cosmetic: a kind that is rejected leaves
// GetOrCreateProgram's MOBILEGL_ASSERT(remapOk) as the only complaint, and
@@ -1849,16 +1855,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// unification and the set->0 normalisation this function exists to do. A
// program with an image array plus any second descriptor got aliased
// bindings out of that, and a DEBUG build trapped on the same program.
// Which is also why the message below is MGLOG_I: MGLOG_E is compiled out
// of an INFO build, so a refusal that only said MGLOG_E said nothing at all
// in the builds that ship.
// The refusal below is MGLOG_E and per-program-compile, so it reports every
// program it declines. It spent time at MGLOG_I because the old level
// ordering compiled E out of the builds that ship.
const Bool arraySupportedForKind =
kind == ProgramFactory::DescriptorBindingKind::UniformBufferDynamic ||
kind == ProgramFactory::DescriptorBindingKind::StorageBuffer ||
kind == ProgramFactory::DescriptorBindingKind::StorageImage ||
kind == ProgramFactory::DescriptorBindingKind::CombinedImageSampler;
if (binding->count != 1 && !arraySupportedForKind) {
MGLOG_I("ProgramFactory: descriptor arrays are unsupported for this descriptor "
MGLOG_E("ProgramFactory: descriptor arrays are unsupported for this descriptor "
"kind (name='%s' count=%u type=%d)",
binding->name ? binding->name : "<null>", binding->count,
static_cast<Int>(binding->descriptor_type));
@@ -1979,13 +1985,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// cannot be corrected and instanced draws with a non-zero baseInstance misrender; this
// detects the case so the user gets one warning instead of silent corruption.
Bool ProgramFactory::ReflectedReadsInstanceIndexBuiltin(const SpvReflectShaderModule& reflectModule) {
return ReflectedDeclaresInputBuiltin(reflectModule, SpvBuiltInInstanceIndex);
}
// GL's gl_BaseVertex and Vulkan's BaseVertex agree for indexed draws and disagree for every
// other command, so a program declaring the builtin needs the ZeroBaseVertex variant when a
// non-indexed draw uses it (see CompileOptionBit::ZeroBaseVertex). "Declares" rather than
// "reads" is the honest word and the useful one: the zeroing pass keeps the variable, so
// both variants of a program answer this question identically.
Bool ProgramFactory::ReflectedReadsBaseVertexBuiltin(const SpvReflectShaderModule& reflectModule) {
return ReflectedDeclaresInputBuiltin(reflectModule, SpvBuiltInBaseVertex);
}
Bool ProgramFactory::ReflectedDeclaresInputBuiltin(const SpvReflectShaderModule& reflectModule,
SpvBuiltIn builtin) {
for (Uint32 entryIndex = 0; entryIndex < reflectModule.entry_point_count; ++entryIndex) {
const SpvReflectEntryPoint& entryPoint = reflectModule.entry_points[entryIndex];
for (Uint32 variableIndex = 0; variableIndex < entryPoint.input_variable_count; ++variableIndex) {
const SpvReflectInterfaceVariable* variable = entryPoint.input_variables[variableIndex];
if (variable != nullptr &&
(variable->decoration_flags & SPV_REFLECT_DECORATION_BUILT_IN) != 0 &&
variable->built_in == SpvBuiltInInstanceIndex) {
variable->built_in == builtin) {
return true;
}
}
@@ -2244,6 +2264,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkProgramObject& entry) const {
entry.activeVertexInputLocationMask = 0;
entry.vertexInputTypes.fill(0);
entry.readsBaseVertexBuiltin = false;
for (SizeT moduleIndex = 0; moduleIndex < shaders.size() && moduleIndex < spirv.size(); ++moduleIndex) {
if (!shaders[moduleIndex] || shaders[moduleIndex]->GetShaderStage() != ShaderStage::Vertex) {
@@ -2265,6 +2286,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
continue;
}
entry.readsBaseVertexBuiltin = ReflectedReadsBaseVertexBuiltin(reflectModule);
if (!m_shaderDrawParametersEnabled && ReflectedReadsInstanceIndexBuiltin(reflectModule)) {
static Bool s_warnedInstanceIndexUnsupported = false;
if (!s_warnedInstanceIndexUnsupported) {
@@ -2445,7 +2468,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// inert; a device whose binding cap is smaller than a shader's array is not a
// configuration MobileGL can serve at all. Needs a >maxBindings-element array to
// reach (256 on desktop, ~16 on mobile).
MGLOG_I("ProgramFactory::ReflectLayout: %s array '%s' at binding %u has %u elements, past the %u "
MGLOG_D("ProgramFactory::ReflectLayout: %s array '%s' at binding %u has %u elements, past the %u "
"this device can describe - declining the program",
kindLabel, uniformName.c_str(), binding, count, maxBindings);
outDeclined = true;
@@ -2453,7 +2476,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
if (baseLocation < 0 ||
!program.UniformLocationsAliasSameUniform(baseLocation, baseLocation + static_cast<Int>(count - 1u))) {
MGLOG_I("ProgramFactory::ReflectLayout: %s array '%s' at binding %u spans %u descriptors but the "
MGLOG_D("ProgramFactory::ReflectLayout: %s array '%s' at binding %u spans %u descriptors but the "
"reflection reserved fewer uniform locations for it (base=%d) - a multi-dimensional array "
"is the usual cause, and MobileGL declines it rather than resolve elements onto a "
"neighbouring uniform",
@@ -2644,6 +2667,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const auto descriptorKind = ReflectDescriptorTypeToBindingKind(sampler->descriptor_type);
if (descriptorKind != DescriptorBindingKind::CombinedImageSampler &&
descriptorKind != DescriptorBindingKind::UniformTexelBuffer &&
descriptorKind != DescriptorBindingKind::StorageTexelBuffer &&
descriptorKind != DescriptorBindingKind::StorageImage &&
descriptorKind != DescriptorBindingKind::StorageBuffer) {
continue;
@@ -2689,7 +2713,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// a Uint16 on the way, where 65536 would silently become 0.
const Uint32 storageArrayCount = std::max<Uint32>(1u, sampler->count);
if (storageArrayCount > m_maxBindings) {
MGLOG_I("ProgramFactory::ReflectLayout: storage block array '%s' at binding %u has %u "
MGLOG_D("ProgramFactory::ReflectLayout: storage block array '%s' at binding %u has %u "
"elements, past the %u this device can describe - declining the program",
uniformName.c_str(), binding, storageArrayCount, m_maxBindings);
entry.declinedDescriptors = true;
@@ -2712,7 +2736,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// so at a level that survives a release build, because dropping the binding
// leaves the shader reading a descriptor the layout never declared.
if (sampler->count > 1) {
MGLOG_I("ProgramFactory::ReflectLayout: declining '%s' at binding %u - a %u-element "
MGLOG_E("ProgramFactory::ReflectLayout: declining '%s' at binding %u - a %u-element "
"descriptor array with no frontend uniform location (a multi-dimensional array "
"of samplers or images is the known cause)",
uniformName.c_str(), binding, sampler->count);
@@ -2773,6 +2797,29 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
if (descriptorKind == DescriptorBindingKind::StorageTexelBuffer) {
// Only the declared format is recorded, and only so the per-draw resolve can
// prefer it over the one glBindImageTexture named. Everything the StorageImage
// branch above does about ARRAYS is deliberately absent: an imageBuffer array
// is refused outright by the array gate in RemapDescriptorBindingsForVulkan,
// exactly as a samplerBuffer array is, so bindingDescriptorCounts stays at the
// default 1 and the descriptor write below may take the address of a vector
// element without reserving room for extra elements.
const VkFormat reflectedFormat =
ConvertSpirvImageFormatToVkFormat(sampler->image.image_format);
VkFormat& existingFormat = entry.storageImageFormatByBinding[binding];
MOBILEGL_ASSERT(existingFormat == VK_FORMAT_UNDEFINED ||
reflectedFormat == VK_FORMAT_UNDEFINED ||
existingFormat == reflectedFormat,
"ProgramFactory::ReflectLayout: storage texel buffer binding %u ('%s') "
"has conflicting reflected formats (%d vs %d)",
binding, uniformName.c_str(), static_cast<Int>(existingFormat),
static_cast<Int>(reflectedFormat));
if (existingFormat == VK_FORMAT_UNDEFINED) {
existingFormat = reflectedFormat;
}
}
const TextureTarget target = UniformTypeToTextureTarget(uniformType);
MOBILEGL_ASSERT(target != TextureTarget::Unknown,
"ProgramFactory::ReflectLayout: failed to resolve texture target for '%s'",
@@ -2850,6 +2897,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
entry.dynamicBindings.push_back(binding);
} else if (kind == DescriptorBindingKind::UniformTexelBuffer) {
layoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER;
} else if (kind == DescriptorBindingKind::StorageTexelBuffer) {
layoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER;
} else if (kind == DescriptorBindingKind::StorageBuffer) {
layoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
} else if (kind == DescriptorBindingKind::StorageImage) {
@@ -2909,6 +2958,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// a FragCoordYFlip variant also depends on the baked default-framebuffer height, so
// that height rides in the free high half of the key. Flags occupy the low bits, and a
// height cannot exceed the 16 bits a swapchain extent fits in.
//
// "The low bits" is load-bearing and was until now only a comment: a flag that reached
// bit 16 would alias the height and two different variants would share one memo slot.
static_assert(static_cast<Uint>(CompileOptionBit::ZeroBaseVertex) < (1u << 16),
"CompileOptionBit values must stay below bit 16: GetOrCreateProgram packs the "
"default-framebuffer height into the high half of the same memo key");
const Uint memoKey = (flags & CompileOptionBit::FragCoordYFlip)
? (flags.GetRaw() | (m_defaultFramebufferHeight << 16))
: flags.GetRaw();
@@ -3027,6 +3082,26 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
// The non-indexed variant of a vertex stage that reads gl_BaseVertex: GL wants zero
// there, Vulkan's builtin would hand it the draw's firstVertex. Requested per draw
// through CompileOptionBit::ZeroBaseVertex, so the indexed variant of the same
// program keeps the native builtin and stays correct for glDrawElementsBaseVertex
// and for the baseVertex word of an indexed indirect command.
if (shaders[i] && shaders[i]->GetShaderStage() == ShaderStage::Vertex &&
(flags & CompileOptionBit::ZeroBaseVertex)) {
Vector<Uint> zeroedSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::ZeroBaseVertexForVulkan(moduleSpirvs[i],
zeroedSpirv)) {
moduleSpirvs[i] = std::move(zeroedSpirv);
} else {
// Failing open keeps the native builtin, which is the pre-fix behavior:
// gl_BaseVertex reads firstVertex on a DrawArrays instead of zero.
MGLOG_E("ProgramFactory: failed to zero gl_BaseVertex for program %u; non-indexed "
"draws will read the draw's first vertex from it instead of zero",
program.GetExternalIndex());
}
}
// A 64-bit vertex input has to arrive as its 32-bit word pair: VK_FORMAT_R64*_SFLOAT is
// optional and lavapipe advertises none of them at all. The pass is unconditional so it
// always agrees with the Float64 case in VertexInputStateFactory::ToVkVertexFormat, and
@@ -3115,6 +3190,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
#endif
ReflectVertexInputs(shaders, moduleSpirvs, entry);
ReflectFragmentOutputs(shaders, moduleSpirvs, entry);
ReflectPassthroughTessControlNeed(shaders, moduleSpirvs, entry);
ReflectLayout(program, moduleSpirvs, entry);
// A failed remap means the modules kept glslang's per-stage auto-mapped binding numbers -
// no cross-stage unification, no set->0 normalisation - so the bindings this layout
@@ -3125,7 +3201,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// "the layout and the shader disagree", so route it through that. Set AFTER ReflectLayout,
// which clears the flag.
if (!remapOk) {
MGLOG_I("ProgramFactory::GetOrCreateProgram: declining program %u - its descriptor bindings could not "
MGLOG_E("ProgramFactory::GetOrCreateProgram: declining program %u - its descriptor bindings could not "
"be remapped, so the layout does not describe what the shader reads",
program.GetExternalIndex());
entry.declinedDescriptors = true;
@@ -3172,4 +3248,235 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
}
ProgramFactory::~ProgramFactory() {
for (auto& entry : m_passthroughTessControlStages) {
if (entry.second.module != VK_NULL_HANDLE) {
vkDestroyShaderModule(m_device, entry.second.module, nullptr);
}
}
}
String ProgramFactory::BuildPassthroughTessControlSource(Uint32 patchVertices) {
// The stage GL 4.6 core 11.2.2 describes when a program has an evaluation shader and no
// control shader: "the input patch is passed through unmodified", the output patch has
// as many vertices as the input one (PATCH_VERTICES), and the levels come from the
// PATCH_DEFAULT_OUTER_LEVEL / PATCH_DEFAULT_INNER_LEVEL state.
//
// Those two levels default to 1.0 and are baked here as literals because
// glPatchParameterfv - their only setter - is not implemented in this frontend (it is a
// stub in MG_Impl/GLImpl/Exporting/Definitions.cpp). Implementing that entry point means
// making the levels a parameter of this source AND of the cache key in
// GetOrCreatePassthroughTessControlStage; the two must move together, so they are named
// together here.
//
// gl_out carries gl_Position and nothing else on purpose. The evaluation stage that
// reads it was linked against the VERTEX stage directly, so its input gl_PerVertex holds
// exactly the built-ins that stage used, and its user-defined inputs (if any) come
// straight off the vertex stage's outputs - which a control stage sitting in between
// would leave unwritten. ReflectPassthroughTessControlNeed refuses those programs rather
// than let this write a partial interface.
//
// All four outer levels and both inner levels are written unconditionally: writing a
// level the evaluation stage's domain does not use is legal and ignored, and it saves
// this from having to know the domain.
String source = "#version 450 core\n";
source += "layout(vertices = " + std::to_string(patchVertices) + ") out;\n";
// gl_in and gl_out are redeclared to the exact gl_PerVertex the FRONTEND's linked programs
// carry - gl_Position, gl_PointSize, gl_ClipDistance[1], in that order - because Vulkan
// matches built-in interface blocks by their whole shape, and the two obvious spellings
// are both wrong:
// * narrowing the block to gl_Position alone makes the evaluation stage read a patch of
// zeroes (degenerate triangles, nothing rasterized), and
// * taking glslang's DEFAULT block for a standalone control stage yields FOUR members -
// it appends gl_CullDistance - where a linked vertex+evaluation program has three.
// PassthroughTessControlTest.MatchesTheFrontendPerVertexBlock is the latch: it links a
// vertex+evaluation program through this same compiler and fails if the two shapes ever
// stop agreeing, rather than letting the mismatch show up as a black frame.
//
// Only gl_Position is written. gl_PointSize is declared but left alone deliberately:
// writing it from a tessellation stage requires the shaderTessellationAndGeometryPointSize
// feature, which this renderer does not enable, so a program whose evaluation stage reads
// gl_in[].gl_PointSize gets an undefined point size instead of the vertex stage's - a gap
// this trades for not making every tessellated pipeline depend on an optional feature.
source += "in gl_PerVertex {\n"
" vec4 gl_Position;\n"
" float gl_PointSize;\n"
" float gl_ClipDistance[1];\n"
"} gl_in[gl_MaxPatchVertices];\n";
source += "out gl_PerVertex {\n"
" vec4 gl_Position;\n"
" float gl_PointSize;\n"
" float gl_ClipDistance[1];\n"
"} gl_out[];\n";
source += "void main() {\n";
source += " gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;\n";
source += " gl_TessLevelOuter[0] = 1.0;\n";
source += " gl_TessLevelOuter[1] = 1.0;\n";
source += " gl_TessLevelOuter[2] = 1.0;\n";
source += " gl_TessLevelOuter[3] = 1.0;\n";
source += " gl_TessLevelInner[0] = 1.0;\n";
source += " gl_TessLevelInner[1] = 1.0;\n";
source += "}\n";
return source;
}
VkPipelineShaderStageCreateInfo ProgramFactory::GetOrCreatePassthroughTessControlStage(Uint32 patchVertices) {
// A cached VK_NULL_HANDLE is a remembered failure, not a miss: returning it keeps a
// generator that cannot compile from re-running glslang on every draw.
const auto cached = m_passthroughTessControlStages.find(patchVertices);
if (cached != m_passthroughTessControlStages.end()) {
return cached->second;
}
VkPipelineShaderStageCreateInfo stage{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO};
stage.stage = VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT;
stage.module = VK_NULL_HANDLE;
stage.pName = "main";
using namespace MG_Util::ShaderTranspiler;
const String source = BuildPassthroughTessControlSource(patchVertices);
// Same compile configuration as every other stage of every other program: this runs on
// the GL thread (the draw path), so the live compile env is the right one, and flags=0
// is the Vulkan-targeting form (CompileForOpenGL is what the GLES backend adds).
const SharedPtr<const CompileEnv>& env = GetCurrentCompileEnv();
ShaderAttrib shaderAttrib{.shaderType = GL_TESS_CONTROL_SHADER,
.sourceStr = source,
.flags = 0,
.env = env.get()};
auto compiled = ShaderCompiler::CompileShader(shaderAttrib);
if (!compiled) {
MGLOG_E("ProgramFactory: could not compile the pass-through tessellation control stage for "
"patchVertices=%u; a program with an evaluation stage and no control stage cannot draw. %s",
patchVertices, compiled.error().log.c_str());
m_passthroughTessControlStages.emplace(patchVertices, stage);
return stage;
}
ProgramAttrib programAttrib{};
programAttrib.shaders.push_back(compiled.value());
auto linked = ShaderCompiler::LinkProgram(programAttrib);
if (!linked) {
MGLOG_E("ProgramFactory: could not link the pass-through tessellation control stage for "
"patchVertices=%u. %s", patchVertices, linked.error().log.c_str());
m_passthroughTessControlStages.emplace(patchVertices, stage);
return stage;
}
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_TESS_CONTROL_SHADER}, .program = *linked.value()};
auto binary = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
if (!binary || binary.value().empty() || binary.value().front().empty()) {
MGLOG_E("ProgramFactory: could not generate SPIR-V for the pass-through tessellation control stage "
"for patchVertices=%u", patchVertices);
m_passthroughTessControlStages.emplace(patchVertices, stage);
return stage;
}
const Vector<Uint>& spirv = binary.value().front();
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
ValidateTransformedSpirv(spirv, ShaderStage::TessControl, 0);
#else
if (MG_Util::ShaderTranspiler::ShaderCompiler::SpirvValidationEnabled()) {
ValidateTransformedSpirv(spirv, ShaderStage::TessControl, 0);
}
#endif
VkShaderModuleCreateInfo smci{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
smci.codeSize = spirv.size() * sizeof(Uint);
smci.pCode = spirv.data();
VkShaderModule module = VK_NULL_HANDLE;
const VkResult result = vkCreateShaderModule(m_device, &smci, nullptr, &module);
if (result != VK_SUCCESS) {
MGLOG_E("ProgramFactory: vkCreateShaderModule failed (%d) for the pass-through tessellation control "
"stage for patchVertices=%u", static_cast<Int>(result), patchVertices);
m_passthroughTessControlStages.emplace(patchVertices, stage);
return stage;
}
stage.module = module;
MGLOG_D("ProgramFactory: built the pass-through tessellation control stage for patchVertices=%u "
"(GL 4.6 11.2.2; Vulkan has no fixed-function equivalent)", patchVertices);
m_passthroughTessControlStages.emplace(patchVertices, stage);
return stage;
}
void ProgramFactory::ReflectPassthroughTessControlNeed(
const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
const Vector<Vector<Uint>>& spirv,
VkProgramObject& entry) const {
entry.needsPassthroughTessControl = false;
entry.passthroughTessControlEmulatable = false;
Bool hasTessEval = false;
Bool hasTessControl = false;
SizeT tessEvalModuleIndex = 0;
for (SizeT i = 0; i < shaders.size(); ++i) {
if (!shaders[i]) continue;
const auto stage = shaders[i]->GetShaderStage();
if (stage == ShaderStage::TessControl) hasTessControl = true;
if (stage == ShaderStage::TessEval) {
hasTessEval = true;
tessEvalModuleIndex = i;
}
}
if (!hasTessEval || hasTessControl) return;
entry.needsPassthroughTessControl = true;
if (tessEvalModuleIndex >= spirv.size() || spirv[tessEvalModuleIndex].empty()) return;
const auto& module = spirv[tessEvalModuleIndex];
SpvReflectShaderModule reflectModule{};
const SpvReflectResult createResult =
spvReflectCreateShaderModule(module.size() * sizeof(Uint), module.data(), &reflectModule);
if (createResult != SPV_REFLECT_RESULT_SUCCESS) {
MGLOG_E("ProgramFactory::ReflectPassthroughTessControlNeed: reflection failed (result=%d); the "
"evaluation stage's inputs are unknown, so the pass-through is not offered",
static_cast<Int>(createResult));
return;
}
uint32_t inputCount = 0;
SpvReflectResult reflectResult = spvReflectEnumerateInputVariables(&reflectModule, &inputCount, nullptr);
Vector<SpvReflectInterfaceVariable*> inputs(inputCount);
if (reflectResult == SPV_REFLECT_RESULT_SUCCESS && inputCount > 0) {
reflectResult = spvReflectEnumerateInputVariables(&reflectModule, &inputCount, inputs.data());
}
if (reflectResult != SPV_REFLECT_RESULT_SUCCESS) {
spvReflectDestroyShaderModule(&reflectModule);
return;
}
// The question is only ever "does this stage read anything a control stage would have to
// forward", and the answer is: does it have a LOCATION. A located input is a user-defined
// varying (or a per-patch input), which the vertex stage writes today and would stop
// reaching once a control stage sits in between - the pass-through carries gl_Position and
// nothing else, so such a program is declined instead of being handed undefined values.
// Everything without a location is a built-in: gl_in, gl_TessCoord, gl_PatchVerticesIn,
// gl_PrimitiveID, gl_TessLevel*, all either forwarded or generated for the evaluation
// stage by the tessellator itself.
//
// This deliberately does NOT judge on SpvReflectInterfaceVariable::built_in. gl_in is an
// array of interface blocks, and for those SPIRV-Reflect reports built_in == -1 on the
// block AND leaves every member's built_in at 0 - which is SpvBuiltInPosition, so a
// member walk reads "Position, Position, Position" for a {Position, PointSize,
// ClipDistance} block and would accept anything on the strength of parse garbage. The
// location, by contrast, is decorated on the OpVariable and is what SPIRV-Reflect reads
// straight through.
constexpr Uint32 kNoLocation = 0xFFFFFFFFu;
Bool emulatable = true;
for (auto* input : inputs) {
if (input == nullptr) continue;
if (input->location == kNoLocation) continue;
MGLOG_E("ProgramFactory: a tessellation evaluation stage with no control stage reads the "
"user-defined input '%s' at location=%u; a synthesized control stage cannot forward it, so "
"this program's draws are declined rather than fed an undefined varying",
input->name != nullptr ? input->name : "<null>", input->location);
emulatable = false;
break;
}
spvReflectDestroyShaderModule(&reflectModule);
entry.passthroughTessControlEmulatable = emulatable;
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -33,7 +33,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
CombinedImageSampler,
UniformTexelBuffer,
StorageBuffer,
StorageImage
StorageImage,
// GLSL `imageBuffer` - a buffer texture reached through an IMAGE unit rather than a
// texture unit. Vulkan spells it VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER, which is a
// VkBufferView like UniformTexelBuffer and not a VkImageView like StorageImage: it is
// the one image uniform whose descriptor is a buffer. Appended, never inserted -
// DescriptorKeyHash mixes the enumerator's value.
StorageTexelBuffer
};
enum class CompileOptionBit : Uint {
@@ -60,6 +66,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// PositionYFlip (the two are the same fact about the same draws) except under a
// quarter turn, which this renderer does not convert rectangles for either.
FragCoordYFlip = 1 << 7,
// Replaces the vertex stage's gl_BaseVertex reads with zero. GL defines the builtin
// as zero for every drawing command that has no baseVertex parameter - all the
// DrawArrays forms - while Vulkan's BaseVertex reports firstVertex there. Set only
// for a non-indexed draw whose program actually reads the builtin, so nothing else
// acquires a second program/pipeline variant. See ZeroBaseVertexPass.
ZeroBaseVertex = 1 << 8,
};
using CompileOptionFlags = Flags<CompileOptionBit>;
using HashType = Uint64;
@@ -97,6 +109,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Vector<Int> samplerUniformLocationByBinding;
Vector<TextureTarget> samplerTextureTargetByBinding;
Vector<SamplerNumericDomain> samplerNumericDomainByBinding;
// Shared by StorageImage and StorageTexelBuffer bindings: a binding is one kind or
// the other, never both, and both need exactly the same thing - the format the
// shader declared, so the per-draw resolve can tell a typed declaration from a
// formatless one. Kept as one pair rather than two so the move operations below
// cannot drift out of sync with a field that only one kind populates.
Vector<VkFormat> storageImageFormatByBinding;
Vector<Bool> storageImageUsesBindingFormatByBinding;
Vector<String> storageBlockNameByBinding;
@@ -129,6 +146,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// gl_FragDepth); shader-computed depth is immune to the cross-pipeline
// position-invariance quirk (see PipelineFactory::ShouldSuppressDepthWrite).
Bool fragmentReplacesDepth = false;
// The vertex module declares the BaseVertex builtin. Selects the ZeroBaseVertex
// program variant for non-indexed draws, and is deliberately a property of the
// PROGRAM rather than of the variant: the zeroed variant leaves the variable
// declared, so both variants answer the same and the draw path can ask either.
Bool readsBaseVertexBuiltin = false;
// This program has a tessellation EVALUATION stage and no tessellation CONTROL
// stage. GL allows that (4.6 core 11.2.2: with no control shader the input patch
// is passed through unmodified, the output patch size is PATCH_VERTICES, and the
// levels come from the PATCH_DEFAULT_*_LEVEL state); Vulkan does not - either both
// tessellation stages are present or neither
// (VUID-VkGraphicsPipelineCreateInfo-pStages-00730). So the draw path has to supply
// the pass-through stage GL describes; see GetOrCreatePassthroughTessControlStage.
Bool needsPassthroughTessControl = false;
// ...and the pass-through this renderer can synthesize carries gl_Position and
// nothing else, so it is only correct when the evaluation stage's inputs are
// built-ins. A user-defined varying would arrive at the evaluation stage
// UNWRITTEN once a control stage sits between it and the vertex stage, which is
// silently wrong pixels rather than a crash - so those programs are declined
// instead (PipelineFactory::CreatePipeline refuses the pipeline and the draw is
// skipped). See ReflectPassthroughTessControlNeed.
Bool passthroughTessControlEmulatable = false;
// Frame-boundary counter value of the last GetOrCreateProgram hit; drives
// cache eviction (see OnFrameBoundary). Mutable: the draw snapshot's memoised
// entry pointer re-stamps use through a const reference (StampProgramUse).
@@ -179,6 +217,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
producerOutputComponentCount = other.producerOutputComponentCount;
fragmentInputComponentCount = other.fragmentInputComponentCount;
fragmentReplacesDepth = other.fragmentReplacesDepth;
readsBaseVertexBuiltin = other.readsBaseVertexBuiltin;
needsPassthroughTessControl = other.needsPassthroughTessControl;
passthroughTessControlEmulatable = other.passthroughTessControlEmulatable;
lastUsedFrame = other.lastUsedFrame;
other.hash = 0;
other.descriptorSetLayout = VK_NULL_HANDLE;
@@ -192,6 +233,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
other.producerOutputComponentCount = 0;
other.fragmentInputComponentCount = 0;
other.fragmentReplacesDepth = false;
other.readsBaseVertexBuiltin = false;
other.needsPassthroughTessControl = false;
other.passthroughTessControlEmulatable = false;
other.lastUsedFrame = 0;
}
VkProgramObject& operator=(VkProgramObject&& other) noexcept {
@@ -231,6 +275,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
producerOutputComponentCount = other.producerOutputComponentCount;
fragmentInputComponentCount = other.fragmentInputComponentCount;
fragmentReplacesDepth = other.fragmentReplacesDepth;
readsBaseVertexBuiltin = other.readsBaseVertexBuiltin;
needsPassthroughTessControl = other.needsPassthroughTessControl;
passthroughTessControlEmulatable = other.passthroughTessControlEmulatable;
lastUsedFrame = other.lastUsedFrame;
other.hash = 0;
other.descriptorSetLayout = VK_NULL_HANDLE;
@@ -244,6 +291,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
other.producerOutputComponentCount = 0;
other.fragmentInputComponentCount = 0;
other.fragmentReplacesDepth = false;
other.readsBaseVertexBuiltin = false;
other.needsPassthroughTessControl = false;
other.passthroughTessControlEmulatable = false;
other.lastUsedFrame = 0;
return *this;
}
@@ -295,7 +345,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_unformattedFloatStorageImagesEnabled(unformattedFloatStorageImagesEnabled) {
VkProgramObject::s_device = device;
}
~ProgramFactory() = default;
// Destroys the pass-through tessellation control modules. Runs while the device is
// still alive for the same reason ~VkProgramObject's does: this factory outlives
// nothing that owns the device.
~ProgramFactory();
ProgramFactory(const ProgramFactory&) = delete;
HashType ComputeHash(const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags) const;
@@ -341,6 +394,33 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// True when an entry point reads the InstanceIndex builtin. Only gates a diagnostic:
// without shaderDrawParameters such a shader cannot have gl_InstanceID rebased.
static Bool ReflectedReadsInstanceIndexBuiltin(const SpvReflectShaderModule& reflectModule);
// True when an entry point declares the BaseVertex builtin, i.e. when a non-indexed
// draw with this program has to take the ZeroBaseVertex variant.
static Bool ReflectedReadsBaseVertexBuiltin(const SpvReflectShaderModule& reflectModule);
// Shared by the two above: does any entry point list an input variable decorated with
// this builtin?
static Bool ReflectedDeclaresInputBuiltin(const SpvReflectShaderModule& reflectModule, SpvBuiltIn builtin);
// The pass-through tessellation control stage GL 4.6 core 11.2.2 describes for a
// program that has an evaluation stage and no control stage, for an input patch of
// `patchVertices` control points. Returned BY VALUE (a stage description is a POD, and
// the cache below is a rehashing map, so a pointer into it would not survive the next
// distinct patch size). `.module == VK_NULL_HANDLE` means the stage could not be built:
// the caller then has no control stage to inject, and CreatePipeline refuses the
// pipeline rather than handing the driver a half-tessellated one.
//
// Keyed on the patch size because GL takes the output patch size from PATCH_VERTICES,
// which is draw state, not link state - the CTS case that motivated this links at the
// default 3 and draws at 4. The pipeline cache already re-keys on patchControlPoints,
// so the module a pipeline was built with is part of that pipeline's identity.
// Compiling is bounded by the number of distinct patch sizes a program draws with
// (MAX_PATCH_VERTICES = 32 in the worst case, one or two in practice) and only ever
// happens for the rare program that has no control stage at all.
VkPipelineShaderStageCreateInfo GetOrCreatePassthroughTessControlStage(Uint32 patchVertices);
// Source of the module above. Exposed for tests: the generated GLSL is the whole
// contract with the evaluation stage, so it is worth pinning independently of a device.
static String BuildPassthroughTessControlSource(Uint32 patchVertices);
private:
struct ProgramLookupCache {
@@ -359,6 +439,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkProgramObject& entry) const;
void ReflectLayout(const MG_State::GLState::ProgramObject& program, const Vector<Vector<Uint>>& spirv,
VkProgramObject& entry) const;
// Fills needsPassthroughTessControl / passthroughTessControlEmulatable off the linked
// modules. Const and reflection-only: it decides nothing about the pipeline, it only
// records what the evaluation stage's input interface is made of.
void ReflectPassthroughTessControlNeed(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
const Vector<Vector<Uint>>& spirv,
VkProgramObject& entry) const;
VkDevice m_device = VK_NULL_HANDLE;
Uint32 m_maxBindings = 0;
@@ -379,6 +465,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// See GetCacheStructureEpoch(). Starts at 1 so a zero-initialized memo can never match.
Uint64 m_cacheStructureEpoch = 1;
IEvictionObserver* m_evictionObserver = nullptr;
// Pass-through tessellation control stages by input patch size. Never evicted: at most
// MAX_PATCH_VERTICES entries exist for the lifetime of the device, and every pipeline
// ever built from one keeps referencing its module. A failed build is cached as
// VK_NULL_HANDLE so a broken generator costs one compile, not one per draw.
UnorderedMap<Uint32, VkPipelineShaderStageCreateInfo> m_passthroughTessControlStages;
static inline XXH64_state_t* m_hashState = XXH64_createState();
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -157,7 +157,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MGLOG_I("Got %d surface formats:", swapchainCapabilities.surfaceFormats.size());
for (const auto& sf : swapchainCapabilities.surfaceFormats) {
MGLOG_I(" [%s, %s]", string_VkFormat(sf.format), string_VkColorSpaceKHR(sf.colorSpace));
MGLOG_D(" [%s, %s]", string_VkFormat(sf.format), string_VkColorSpaceKHR(sf.colorSpace));
}
const auto pickedSurfaceFormat = ChooseSwapchainSurfaceFormat(swapchainCapabilities.surfaceFormats);
@@ -166,7 +166,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MGLOG_I("Got %d present modes:", swapchainCapabilities.presentModes.size());
for (const auto& pm : swapchainCapabilities.presentModes) {
MGLOG_I(" %s", string_VkPresentModeKHR(pm));
MGLOG_D(" %s", string_VkPresentModeKHR(pm));
}
const auto presentMode = ChooseSwapchainPresentMode(swapchainCapabilities.presentModes);
@@ -157,7 +157,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkDescriptorPool initialPool = VK_NULL_HANDLE;
if (!CreateDescriptorPool(m_setsPerFrame, initialPool)) {
MGLOG_E("UniformDescriptorBinder::Initialize failed: cannot create frame descriptor pool %u",
MGLOG_E_ONCE("UniformDescriptorBinder::Initialize failed: cannot create frame descriptor pool %u",
frameIndex);
Shutdown();
return false;
@@ -345,13 +345,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
fallbackHolder = GetFallbackTexture(preferredTarget);
texture = fallbackHolder.get();
if (texture == nullptr) {
MGLOG_E("ResolveSamplerDescriptor: no fallback texture available for binding=%u ('%s') "
MGLOG_E_ONCE("ResolveSamplerDescriptor: no fallback texture available for binding=%u ('%s') "
"location=%d unit=%d target=%d",
binding, programObj.samplerNameByBinding[binding].c_str(), location, unit,
static_cast<Int>(preferredTarget));
return false;
}
MGLOG_W(
MGLOG_W_ONCE(
"ResolveSamplerDescriptor: using fallback texture for unbound sampler binding=%u ('%s') location=%d unit=%d target=%d",
binding, programObj.samplerNameByBinding[binding].c_str(), location, unit,
static_cast<Int>(preferredTarget));
@@ -360,7 +360,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const MG_State::GLState::SamplerObject* samplerToUse =
samplerOverride ? samplerOverride.get() : texture->GetSamplerObject().get();
if (samplerToUse == nullptr) {
MGLOG_E(
MGLOG_E_ONCE(
"ResolveSamplerDescriptor: sampler binding %u ('%s') has no sampler object (textureId=%d location=%d unit=%d)",
binding, programObj.samplerNameByBinding[binding].c_str(), texture->GetExternalIndex(), location,
unit);
@@ -368,7 +368,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
VkTextureManager::TextureResource* resource = m_textureManager->SyncTextureAndGetDescriptor(*texture);
if (resource == nullptr) {
MGLOG_E(
MGLOG_E_ONCE(
"ResolveSamplerDescriptor: sampler binding %u ('%s') failed to create/sync texture resource (textureId=%d target=%d location=%d unit=%d)",
binding, programObj.samplerNameByBinding[binding].c_str(), texture->GetExternalIndex(),
static_cast<Int>(texture->GetTarget()), location, unit);
@@ -380,7 +380,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Int attachmentLevel = 0;
if (drawFbo &&
FindFramebufferAttachmentForTexture(*drawFbo, *texture, attachmentType, attachmentLevel)) {
MGLOG_W("ResolveSamplerDescriptor: framebuffer feedback loop detected: textureId=%d is bound "
MGLOG_W_ONCE("ResolveSamplerDescriptor: framebuffer feedback loop detected: textureId=%d is bound "
"for sampling at binding=%u, but is also attached to drawFbo=%u as %s (level=%d, "
"trackedLayout=%d)",
texture->GetExternalIndex(), binding, drawFbo->GetExternalIndex(),
@@ -390,7 +390,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const Bool readyForSampling = m_textureManager->TransitionTextureForSampling(commandBuffer, *texture);
if (!readyForSampling) {
MGLOG_E("ResolveSamplerDescriptor: failed to transition textureId=%d for sampler binding=%u",
MGLOG_E_ONCE("ResolveSamplerDescriptor: failed to transition textureId=%d for sampler binding=%u",
texture->GetExternalIndex(), binding);
return false;
}
@@ -432,7 +432,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
if (sampledViewFormat == VK_FORMAT_UNDEFINED) {
MGLOG_E("ResolveSamplerDescriptor: no compatible sampled view for binding=%u ('%s') "
MGLOG_E_ONCE("ResolveSamplerDescriptor: no compatible sampled view for binding=%u ('%s') "
"textureId=%d imageFormat=%d numericDomain=%d",
binding, programObj.samplerNameByBinding[binding].c_str(), texture->GetExternalIndex(),
static_cast<Int>(resource->format), static_cast<Int>(numericDomain));
@@ -445,7 +445,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
? resource->sampledView
: m_textureManager->GetOrCreateSampledImageView(*texture, sampledViewFormat);
if (sampledImageView == VK_NULL_HANDLE) {
MGLOG_E("ResolveSamplerDescriptor: failed to resolve sampled view for binding=%u ('%s') "
MGLOG_E_ONCE("ResolveSamplerDescriptor: failed to resolve sampled view for binding=%u ('%s') "
"textureId=%d imageFormat=%d viewFormat=%d numericDomain=%d",
binding, programObj.samplerNameByBinding[binding].c_str(), texture->GetExternalIndex(),
static_cast<Int>(resource->format), static_cast<Int>(sampledViewFormat),
@@ -671,14 +671,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
SharedPtr<MG_State::GLState::ITextureObject> texture;
if (!ResolveSamplerTexture(program, programObj, binding, texture) || texture == nullptr) {
MGLOG_E("ResolveTexelBufferDescriptor: texture buffer binding %u ('%s') is unbound", binding,
MGLOG_E_ONCE("ResolveTexelBufferDescriptor: texture buffer binding %u ('%s') is unbound", binding,
programObj.samplerNameByBinding[binding].c_str());
return false;
}
if (texture->GetStorageType() != TextureStorageType::Buffer ||
texture->GetTarget() != TextureTarget::TextureBuffer) {
MGLOG_E(
MGLOG_E_ONCE(
"ResolveTexelBufferDescriptor: binding %u ('%s') expected texture buffer, got textureId=%u target=%d storage=%d",
binding, programObj.samplerNameByBinding[binding].c_str(), texture->GetExternalIndex(),
static_cast<Int>(texture->GetTarget()), static_cast<Int>(texture->GetStorageType()));
@@ -688,14 +688,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
auto* textureBuffer = static_cast<MG_State::GLState::TextureObjectBuffer*>(texture.get());
const auto& bufferObject = textureBuffer->GetBufferBindingSlot().GetBoundObject();
if (bufferObject == nullptr) {
MGLOG_E("ResolveTexelBufferDescriptor: texture buffer binding %u ('%s') has no GL buffer bound",
MGLOG_E_ONCE("ResolveTexelBufferDescriptor: texture buffer binding %u ('%s') has no GL buffer bound",
binding, programObj.samplerNameByBinding[binding].c_str());
return false;
}
BufferSlice slice{};
if (!m_bufferManager->AcquireResidentSlice(BufferKind::TextureBuffer, bufferObject, slice) || !slice.IsValid()) {
MGLOG_E("ResolveTexelBufferDescriptor: failed to sync GL buffer %u for texture buffer %u",
MGLOG_E_ONCE("ResolveTexelBufferDescriptor: failed to sync GL buffer %u for texture buffer %u",
bufferObject->GetExternalIndex(), texture->GetExternalIndex());
return false;
}
@@ -703,7 +703,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const auto internalFormat = textureBuffer->GetFormat();
const VkFormat vkFormat = MG_Util::ConvertTextureInternalFormatToVkEnum(internalFormat);
if (vkFormat == VK_FORMAT_UNDEFINED) {
MGLOG_E("ResolveTexelBufferDescriptor: unsupported texture buffer internal format %d",
MGLOG_E_ONCE("ResolveTexelBufferDescriptor: unsupported texture buffer internal format %d",
static_cast<Int>(internalFormat));
return false;
}
@@ -719,7 +719,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
viewRange = (viewRange / texelSize) * texelSize;
}
if (viewRange == 0) {
MGLOG_E("ResolveTexelBufferDescriptor: texture buffer %u has empty view range", texture->GetExternalIndex());
MGLOG_E_ONCE("ResolveTexelBufferDescriptor: texture buffer %u has empty view range", texture->GetExternalIndex());
return false;
}
@@ -733,7 +733,151 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkBufferView bufferView = VK_NULL_HANDLE;
const VkResult result = vkCreateBufferView(m_device, &viewInfo, nullptr, &bufferView);
if (result != VK_SUCCESS || bufferView == VK_NULL_HANDLE) {
MGLOG_E("ResolveTexelBufferDescriptor: vkCreateBufferView failed result=%d format=%d range=%zu",
MGLOG_E_ONCE("ResolveTexelBufferDescriptor: vkCreateBufferView failed result=%d format=%d range=%zu",
result, static_cast<Int>(vkFormat), static_cast<SizeT>(viewRange));
return false;
}
m_frames[frameIndex].texelBufferViews.push_back(bufferView);
outBufferView = bufferView;
return true;
}
// GLSL `imageBuffer`. The one image uniform whose Vulkan descriptor is a VkBufferView rather
// than a VkImageView, so it is half ResolveStorageImageDescriptor (the resource comes from an
// IMAGE unit, i.e. from glBindImageTexture, not from a texture unit) and half
// ResolveTexelBufferDescriptor (the descriptor is a buffer view over the GL buffer the
// texture is attached to).
//
// Before this existed the descriptor kind reflected as SPV_REFLECT_DESCRIPTOR_TYPE_STORAGE_-
// TEXEL_BUFFER and fell into ReflectDescriptorTypeToBindingKind's `default:`, whose only
// complaint is an assert that compiles out above DEBUG - so a release build declared no
// binding at all for a uniform the shader still read, and lavapipe segfaulted inside pipeline
// creation on the JIT worker thread. KHR-GL44.multi_bind.dispatch_bind_image_textures is the
// case that carries it.
Bool UniformManager::ResolveStorageTexelBufferDescriptor(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj,
Uint32 binding, Uint32 frameIndex,
VkBufferView& outBufferView) {
outBufferView = VK_NULL_HANDLE;
MOBILEGL_ASSERT(m_bufferManager != nullptr, "ResolveStorageTexelBufferDescriptor: buffer manager is null");
MOBILEGL_ASSERT(MG_State::pGLContext != nullptr, "ResolveStorageTexelBufferDescriptor: GL context is null");
MOBILEGL_ASSERT(frameIndex < m_frames.size(),
"ResolveStorageTexelBufferDescriptor: frame index out of range");
MOBILEGL_ASSERT(binding < programObj.samplerUniformLocationByBinding.size(),
"ResolveStorageTexelBufferDescriptor: binding %u out of range", binding);
const Int location = programObj.samplerUniformLocationByBinding[binding];
if (location < 0) {
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: binding %u ('%s') has no uniform location", binding,
programObj.samplerNameByBinding[binding].c_str());
return false;
}
const Int imageUnit = program.GetUniformSamplerOrImageUnitIndex(static_cast<Uint>(location));
if (imageUnit < 0 || imageUnit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: image unit %d out of range for binding %u", imageUnit,
binding);
return false;
}
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(imageUnit);
const auto& texture = imageBinding.Texture;
if (texture == nullptr) {
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: image unit %d is unbound for binding %u", imageUnit,
binding);
return false;
}
if (texture->GetStorageType() != TextureStorageType::Buffer ||
texture->GetTarget() != TextureTarget::TextureBuffer) {
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: binding %u ('%s') expected a texture buffer on image "
"unit %d, got textureId=%u target=%d storage=%d",
binding, programObj.samplerNameByBinding[binding].c_str(), imageUnit,
texture->GetExternalIndex(), static_cast<Int>(texture->GetTarget()),
static_cast<Int>(texture->GetStorageType()));
return false;
}
auto* textureBuffer = static_cast<MG_State::GLState::TextureObjectBuffer*>(texture.get());
const auto& bufferObject = textureBuffer->GetBufferBindingSlot().GetBoundObject();
if (bufferObject == nullptr) {
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: texture buffer on image unit %d has no GL buffer bound",
imageUnit);
return false;
}
// Unlike the sampled texel buffer, the shader MAY write this one, and those writes land
// in GPU memory behind the frontend's CPU shadow - which is what MapBuffer and
// GetBufferSubData read. Same two calls, and for the same reason, as the storage-block
// path above - but only the residency is unconditional. Marking a GL_READ_ONLY binding
// GPU-written would make the next map or readback wait for a dispatch that could not have
// changed a byte of it.
bufferObject->EnsureGpuResidentStorage();
if (imageBinding.Access != GL_READ_ONLY) {
bufferObject->MarkGpuWritten();
}
BufferSlice slice{};
if (!m_bufferManager->AcquireResidentSlice(BufferKind::TextureBuffer, bufferObject, slice) ||
!slice.IsValid()) {
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: failed to sync GL buffer %u for texture buffer %u",
bufferObject->GetExternalIndex(), texture->GetExternalIndex());
return false;
}
// The format the SHADER declared wins over the one glBindImageTexture named, on the same
// policy as a storage image: a typed `layout(r32ui) uniform uimageBuffer` must be read as
// r32ui whatever the texture's own attachment format says. Falling back, in order:
// reflected format, then the bind format, then the texture's attached format.
MOBILEGL_ASSERT(binding < programObj.storageImageFormatByBinding.size(),
"ResolveStorageTexelBufferDescriptor: binding %u has no reflected format slot", binding);
const auto internalFormat = textureBuffer->GetFormat();
const VkFormat resourceFormat = MG_Util::ConvertTextureInternalFormatToVkEnum(internalFormat);
const VkFormat reflectedFormat = programObj.storageImageFormatByBinding[binding];
VkFormat vkFormat = reflectedFormat;
if (vkFormat == VK_FORMAT_UNDEFINED && imageBinding.Format != 0) {
vkFormat = MG_Util::ConvertTextureInternalFormatToVkEnum(
MG_Util::ConvertGLEnumToTextureInternalFormat(imageBinding.Format));
}
if (vkFormat == VK_FORMAT_UNDEFINED) {
vkFormat = resourceFormat;
}
if (vkFormat == VK_FORMAT_UNDEFINED) {
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: unsupported image buffer format (internal=%d bind=0x%x)",
static_cast<Int>(internalFormat), imageBinding.Format);
return false;
}
// Sized from the TEXTURE's attached format even though the view may carry a different
// one. That is not a shortcut: GL requires the shader's format qualifier, the format
// passed to glBindImageTexture and the texture's own internal format to belong to the
// same format CLASS (GL 4.6 core, table 8.27), and every member of a class has the same
// texel size. So the three can disagree on interpretation and never on bytes - which is
// what the range below has to be a whole multiple of.
const VkDeviceSize texelSize =
static_cast<VkDeviceSize>(MG_Util::GetSizedInternalFormatSizeInBytes(internalFormat));
const VkDeviceSize rangeOffset = static_cast<VkDeviceSize>(textureBuffer->GetBufferRangeOffset());
const VkDeviceSize rangeSize = static_cast<VkDeviceSize>(textureBuffer->GetBufferRangeSizeInBytes());
VkDeviceSize viewRange = std::min(rangeSize, slice.size > rangeOffset ? slice.size - rangeOffset : 0);
if (texelSize > 0) {
viewRange = (viewRange / texelSize) * texelSize;
}
if (viewRange == 0) {
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: texture buffer %u has empty view range",
texture->GetExternalIndex());
return false;
}
VkBufferViewCreateInfo viewInfo{};
viewInfo.sType = VK_STRUCTURE_TYPE_BUFFER_VIEW_CREATE_INFO;
viewInfo.buffer = slice.buffer;
viewInfo.format = vkFormat;
viewInfo.offset = slice.offset + rangeOffset;
viewInfo.range = viewRange;
VkBufferView bufferView = VK_NULL_HANDLE;
const VkResult result = vkCreateBufferView(m_device, &viewInfo, nullptr, &bufferView);
if (result != VK_SUCCESS || bufferView == VK_NULL_HANDLE) {
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: vkCreateBufferView failed result=%d format=%d range=%zu",
result, static_cast<Int>(vkFormat), static_cast<SizeT>(viewRange));
return false;
}
@@ -770,7 +914,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
auto& bindingPoint = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, frontendBinding);
const auto& bufferObject = bindingPoint.GetBoundObject();
if (bufferObject == nullptr) {
MGLOG_E("ResolveStorageBufferDescriptor: no SSBO bound at frontend binding %u for block '%s'",
MGLOG_E_ONCE("ResolveStorageBufferDescriptor: no SSBO bound at frontend binding %u for block '%s'",
frontendBinding, programObj.storageBlockNameByBinding[binding].c_str());
return false;
}
@@ -785,7 +929,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
BufferSlice slice{};
if (!m_bufferManager->AcquireResidentSlice(BufferKind::ShaderStorage, bufferObject, slice) || !slice.IsValid()) {
MGLOG_E("ResolveStorageBufferDescriptor: failed to sync GL buffer %u for block '%s'",
MGLOG_E_ONCE("ResolveStorageBufferDescriptor: failed to sync GL buffer %u for block '%s'",
bufferObject->GetExternalIndex(), programObj.storageBlockNameByBinding[binding].c_str());
return false;
}
@@ -799,7 +943,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
rangeEnd = bufferSize;
}
if (rangeEnd <= rangeStart) {
MGLOG_E("ResolveStorageBufferDescriptor: empty SSBO range for block '%s'",
MGLOG_E_ONCE("ResolveStorageBufferDescriptor: empty SSBO range for block '%s'",
programObj.storageBlockNameByBinding[binding].c_str());
return false;
}
@@ -823,7 +967,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const Int baseLocation = programObj.samplerUniformLocationByBinding[binding];
if (baseLocation < 0) {
MGLOG_E("ResolveStorageImageDescriptor: storage image binding %u has no uniform location", binding);
MGLOG_E_ONCE("ResolveStorageImageDescriptor: storage image binding %u has no uniform location", binding);
return false;
}
// Per ELEMENT, and this is where an image array differs from a storage-block array: GL
@@ -835,26 +979,26 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// uniform.
const Int location = baseLocation + static_cast<Int>(element);
if (!program.UniformLocationsAliasSameUniform(baseLocation, location)) {
MGLOG_E("ResolveStorageImageDescriptor: binding %u element %u is past the end of its image array",
MGLOG_E_ONCE("ResolveStorageImageDescriptor: binding %u element %u is past the end of its image array",
binding, element);
return false;
}
const Int imageUnit = program.GetUniformSamplerOrImageUnitIndex(static_cast<Uint>(location));
if (imageUnit < 0 || imageUnit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
MGLOG_E("ResolveStorageImageDescriptor: image unit %d out of range for binding %u",
MGLOG_E_ONCE("ResolveStorageImageDescriptor: image unit %d out of range for binding %u",
imageUnit, binding);
return false;
}
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(imageUnit);
if (imageBinding.Texture == nullptr) {
MGLOG_E("ResolveStorageImageDescriptor: image unit %d is unbound for binding %u", imageUnit, binding);
MGLOG_E_ONCE("ResolveStorageImageDescriptor: image unit %d is unbound for binding %u", imageUnit, binding);
return false;
}
const Bool ready = m_textureManager->TransitionTextureForStorageImage(commandBuffer, *imageBinding.Texture);
if (!ready) {
MGLOG_E("ResolveStorageImageDescriptor: failed to transition textureId=%d for image unit %d",
MGLOG_E_ONCE("ResolveStorageImageDescriptor: failed to transition textureId=%d for image unit %d",
imageBinding.Texture->GetExternalIndex(), imageUnit);
return false;
}
@@ -874,7 +1018,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const VkFormat viewFormat = ResolveStorageImageViewFormat(
reflectedFormat, imageBinding.Format, resource->format, useBindingFormat);
if (viewFormat == VK_FORMAT_UNDEFINED) {
MGLOG_E("ResolveStorageImageDescriptor: unsupported glBindImageTexture format=0x%x "
MGLOG_E_ONCE("ResolveStorageImageDescriptor: unsupported glBindImageTexture format=0x%x "
"for binding=%u imageUnit=%d textureId=%d bindingPolicy=%s",
imageBinding.Format, binding, imageUnit, imageBinding.Texture->GetExternalIndex(),
useBindingFormat ? "true" : "false");
@@ -883,7 +1027,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const VkImageView view = m_textureManager->GetOrCreateStorageImageView(
*imageBinding.Texture, mipLevel, viewFormat, imageBinding.Layered != GL_FALSE, imageBinding.Layer);
if (view == VK_NULL_HANDLE) {
MGLOG_E("ResolveStorageImageDescriptor: failed to resolve storage view textureId=%d mip=%u "
MGLOG_E_ONCE("ResolveStorageImageDescriptor: failed to resolve storage view textureId=%d mip=%u "
"bindingFormat=0x%x imageFormat=%d reflectedFormat=%d selectedFormat=%d bindingPolicy=%s",
imageBinding.Texture->GetExternalIndex(), mipLevel, imageBinding.Format,
static_cast<Int>(resource->format), static_cast<Int>(reflectedFormat),
@@ -904,7 +1048,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Report that there is no fallback and let the caller decline the draw - aborting the
// process over an unbound sampler is never the right answer.
if (target != TextureTarget::Texture2D && target != TextureTarget::TextureRectangle) {
MGLOG_E("UniformManager::GetFallbackTexture: no fallback exists for target=%d",
MGLOG_E_ONCE("UniformManager::GetFallbackTexture: no fallback exists for target=%d",
static_cast<Int>(target));
return nullptr;
}
@@ -1080,13 +1224,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
continue;
}
if (binding >= programObj.samplerUniformLocationByBinding.size()) {
MGLOG_E("CollectStorageImageTextures: binding %u has no uniform-location mapping", binding);
MGLOG_E_ONCE("CollectStorageImageTextures: binding %u has no uniform-location mapping", binding);
return false;
}
const Int baseLocation = programObj.samplerUniformLocationByBinding[binding];
if (baseLocation < 0) {
MGLOG_E("CollectStorageImageTextures: binding %u has no image uniform location", binding);
MGLOG_E_ONCE("CollectStorageImageTextures: binding %u has no image uniform location", binding);
return false;
}
// Per ELEMENT, for the same reason the sampled walk above is: an image ARRAY is one
@@ -1098,20 +1242,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
for (Uint32 element = 0; element < descriptorCount; ++element) {
const Int location = ResolveDescriptorElementLocation(program, baseLocation, element);
if (location < 0) {
MGLOG_E("CollectStorageImageTextures: binding %u element %u is past the end of its image array",
MGLOG_E_ONCE("CollectStorageImageTextures: binding %u element %u is past the end of its image array",
binding, element);
return false;
}
const Int imageUnit = program.GetUniformSamplerOrImageUnitIndex(static_cast<Uint>(location));
if (imageUnit < 0 || imageUnit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
MGLOG_E("CollectStorageImageTextures: image unit %d is invalid for binding %u element %u",
MGLOG_E_ONCE("CollectStorageImageTextures: image unit %d is invalid for binding %u element %u",
imageUnit, binding, element);
return false;
}
auto* texture = MG_State::pGLContext->GetImageTextureBinding(imageUnit).Texture.get();
if (texture == nullptr) {
MGLOG_E("CollectStorageImageTextures: image unit %d is unbound for binding %u element %u",
MGLOG_E_ONCE("CollectStorageImageTextures: image unit %d is unbound for binding %u element %u",
imageUnit, binding, element);
return false;
}
@@ -1262,12 +1406,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const Uint64 descriptorCount64 =
static_cast<Uint64>(maxSets) * static_cast<Uint64>(std::min(m_maxBindings, kEstimatedBindingsPerSet));
if (descriptorCount64 > static_cast<Uint64>(std::numeric_limits<Uint32>::max())) {
MGLOG_E("UniformDescriptorBinder::CreateDescriptorPool failed: descriptorCount overflow");
MGLOG_E_ONCE("UniformDescriptorBinder::CreateDescriptorPool failed: descriptorCount overflow");
return false;
}
const Uint32 descriptorCount = static_cast<Uint32>(descriptorCount64);
VkDescriptorPoolSize poolSizes[5]{};
VkDescriptorPoolSize poolSizes[6]{};
poolSizes[0].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC;
poolSizes[0].descriptorCount = descriptorCount;
poolSizes[1].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
@@ -1278,6 +1422,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
poolSizes[3].descriptorCount = descriptorCount;
poolSizes[4].type = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE;
poolSizes[4].descriptorCount = descriptorCount;
poolSizes[5].type = VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER;
poolSizes[5].descriptorCount = descriptorCount;
VkDescriptorPoolCreateInfo poolInfo{};
poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
@@ -1292,7 +1438,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const VkResult result = vkCreateDescriptorPool(m_device, &poolInfo, nullptr, &outPool);
if (result != VK_SUCCESS) {
MGLOG_E("UniformDescriptorBinder::CreateDescriptorPool failed: vkCreateDescriptorPool returned %d",
MGLOG_E_ONCE("UniformDescriptorBinder::CreateDescriptorPool failed: vkCreateDescriptorPool returned %d",
result);
return false;
}
@@ -1311,7 +1457,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkDescriptorPool grownPool = VK_NULL_HANDLE;
if (!CreateDescriptorPool(grownMaxSets, grownPool)) {
MGLOG_E("UniformDescriptorBinder::GrowFrameDescriptorPool failed: cannot create grown pool (%u -> %u sets)",
MGLOG_E_ONCE("UniformDescriptorBinder::GrowFrameDescriptorPool failed: cannot create grown pool (%u -> %u sets)",
currentMaxSets, grownMaxSets);
return false;
}
@@ -1370,7 +1516,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkResult allocResult = AllocateDescriptorSetsFromActivePool(frameIndex, programObj, outDescriptorSet);
if (allocResult == VK_ERROR_OUT_OF_POOL_MEMORY || allocResult == VK_ERROR_FRAGMENTED_POOL) {
if (!GrowFrameDescriptorPool(frame, frameIndex)) {
MGLOG_E("UniformDescriptorBinder::AcquireDescriptorSet failed: descriptor pool growth failed");
MGLOG_E_ONCE("UniformDescriptorBinder::AcquireDescriptorSet failed: descriptor pool growth failed");
return allocResult;
}
allocResult = AllocateDescriptorSetsFromActivePool(frameIndex, programObj, outDescriptorSet);
@@ -1501,7 +1647,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
auto& frame = m_frames[frameIndex];
if (frame.descriptorPools.empty()) {
MGLOG_E("UniformDescriptorBinder::BindProgramUniformBuffers failed: frame descriptor pools are invalid");
MGLOG_E_ONCE("UniformDescriptorBinder::BindProgramUniformBuffers failed: frame descriptor pools are invalid");
return false;
}
if (frame.activeDescriptorPoolIndex >= frame.descriptorPools.size()) {
@@ -1578,6 +1724,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// is reachable wherever m_maxBindings is small (it clamps to ~16 on Adreno and Mali),
// which is exactly where a 7-element CTS sampler array does not fit the slack.
imageInfos.reserve(m_maxBindings + arrayDescriptorExtra);
// Exact, and safe only because it is: BOTH texel kinds (samplerBuffer and imageBuffer)
// refuse descriptor arrays at program creation, so each contributes at most one view and
// the total cannot exceed the binding count. The branches below take the address of
// back(), so making a texel kind array-capable without also giving this the surplus
// imageInfos gets would dangle every pTexelBufferView already recorded in `writes`.
texelBufferViews.reserve(m_maxBindings);
dynamicOffsets.reserve(programObj.dynamicBindings.size() + uboArrayExtra);
@@ -1633,7 +1784,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkBufferView bufferView = VK_NULL_HANDLE;
if (!ResolveTexelBufferDescriptor(program, programObj, binding, frameIndex, bufferView) ||
bufferView == VK_NULL_HANDLE) {
MGLOG_E(
MGLOG_E_ONCE(
"UniformDescriptorBinder::BindProgramUniformBuffers failed: texture buffer binding %u has no valid descriptor",
binding);
return false;
@@ -1644,6 +1795,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
write.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER;
write.pTexelBufferView = &texelBufferViews.back();
writes.push_back(write);
} else if (kind == ProgramFactory::DescriptorBindingKind::StorageTexelBuffer) {
// Shares texelBufferViews with the sampled kind above, and may do so safely for
// the same reason: neither kind can be an array, so each contributes exactly one
// element and the reserve of m_maxBindings cannot be outrun - which is what keeps
// the &back() below from dangling when a later binding pushes.
VkBufferView bufferView = VK_NULL_HANDLE;
if (!ResolveStorageTexelBufferDescriptor(program, programObj, binding, frameIndex, bufferView) ||
bufferView == VK_NULL_HANDLE) {
MGLOG_E_ONCE("UniformDescriptorBinder::BindProgramUniformBuffers failed: image buffer binding %u "
"has no valid descriptor",
binding);
return false;
}
texelBufferViews.push_back(bufferView);
fastRebindKindsEligible = false;
write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER;
write.pTexelBufferView = &texelBufferViews.back();
writes.push_back(write);
} else if (kind == ProgramFactory::DescriptorBindingKind::StorageBuffer) {
// One write per binding, but `descriptorCount` buffer infos: a GLSL block
// instance array occupies a single binding whose elements each come from their
@@ -1653,7 +1823,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
for (Uint32 element = 0; element < descriptorCount; ++element) {
VkDescriptorBufferInfo bufferInfo{};
if (!ResolveStorageBufferDescriptor(program, programObj, binding, element, bufferInfo)) {
MGLOG_E(
MGLOG_E_ONCE(
"UniformDescriptorBinder::BindProgramUniformBuffers failed: storage buffer binding %u "
"element %u has no valid descriptor",
binding, element);
@@ -1680,7 +1850,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkDescriptorImageInfo imageInfo{};
if (!ResolveStorageImageDescriptor(commandBuffer, program, programObj, binding, element,
imageInfo)) {
MGLOG_E(
MGLOG_E_ONCE(
"UniformDescriptorBinder::BindProgramUniformBuffers failed: storage image binding %u "
"element %u has no valid descriptor",
binding, element);
@@ -1722,14 +1892,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
imageInfo, samplerDescriptorsUnchangedHint);
}
if (!hasImage) {
MGLOG_E(
MGLOG_E_ONCE(
"UniformDescriptorBinder::BindProgramUniformBuffers failed: sampler binding %u element %u "
"has no valid texture descriptor",
binding, element);
return false;
}
if (imageInfo.sampler == VK_NULL_HANDLE || imageInfo.imageView == VK_NULL_HANDLE) {
MGLOG_E(
MGLOG_E_ONCE(
"UniformDescriptorBinder::BindProgramUniformBuffers failed: sampler binding %u element %u "
"has null sampler or imageView",
binding, element);
@@ -1803,7 +1973,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} else {
VkResult allocResult = AcquireDescriptorSet(frameIndex, programObj, descriptorSet);
if (allocResult != VK_SUCCESS || descriptorSet == VK_NULL_HANDLE) {
MGLOG_E("UniformDescriptorBinder::BindProgramUniformBuffers failed: descriptor set acquire returned %d",
MGLOG_E_ONCE("UniformDescriptorBinder::BindProgramUniformBuffers failed: descriptor set acquire returned %d",
allocResult);
return false;
}
@@ -175,6 +175,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool ResolveTexelBufferDescriptor(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
Uint32 frameIndex, VkBufferView& outBufferView);
// GLSL `imageBuffer`: the same VkBufferView descriptor as the sampled texel buffer above,
// but resolved from an IMAGE unit (glBindImageTexture) rather than a texture unit, and
// made GPU-resident-writable because the shader may store to it. No `element` parameter:
// an imageBuffer ARRAY is refused at program creation, so a binding is always one
// descriptor (see the array gate in RemapDescriptorBindingsForVulkan).
Bool ResolveStorageTexelBufferDescriptor(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
Uint32 frameIndex, VkBufferView& outBufferView);
// `element` indexes a block INSTANCE array's descriptors; it is 0 for every ordinary
// block. Each element resolves through its own GL storage block, and so its own GL
// binding point, buffer and glBindBufferRange window.
@@ -110,7 +110,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const VkFormat sourceVkFormat =
ToVkVertexFormat(attr.Type, attr.Size, attr.Normalized, attr.IsInteger, attr.IsBgra, attr.IsLong);
if (sourceVkFormat == VK_FORMAT_UNDEFINED) {
MGLOG_E("Unsupported vertex attribute layout (location=%u, type=%s, size=%d): the array is "
MGLOG_E_ONCE("Unsupported vertex attribute layout (location=%u, type=%s, size=%d): the array is "
"enabled but cannot be mapped to a VkFormat",
location, MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size);
unsupportedAttribMask |= (1u << location);
@@ -125,7 +125,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (fallbackFormat != VK_FORMAT_UNDEFINED && SupportsVertexBufferFormat(fallbackFormat)) {
vkFormat = fallbackFormat;
conversion = VertexStreamConversion::ScaledIntegerToFloat32;
MGLOG_W("Vertex attribute location=%u format=%d lacks "
MGLOG_W_ONCE("Vertex attribute location=%u format=%d lacks "
"VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT; using float32 stream format=%d "
"(type=%s size=%d normalized=%s integer=%s)",
location, static_cast<Int>(sourceVkFormat), static_cast<Int>(vkFormat),
@@ -135,7 +135,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
if (conversion == VertexStreamConversion::None) {
MGLOG_E("Unsupported Vulkan vertex format (location=%u, format=%d, type=%s, size=%d): "
MGLOG_E_ONCE("Unsupported Vulkan vertex format (location=%u, format=%d, type=%s, size=%d): "
"VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT is unavailable and no semantic fallback exists",
location, static_cast<Int>(sourceVkFormat),
MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size);
@@ -146,15 +146,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const SizeT attribByteSize = GetAttributeByteSize(attr.Type, attr.Size, attr.IsBgra);
if (attribByteSize == 0) {
MGLOG_E("Vertex attribute with unknown component size (location=%u, type=%s): the array is "
MGLOG_E_ONCE("Vertex attribute with unknown component size (location=%u, type=%s): the array is "
"enabled but cannot be sized",
location, MG_Util::ConvertDataTypeToString(attr.Type).c_str());
unsupportedAttribMask |= (1u << location);
continue;
}
const Uint32 sourceStride =
attr.Stride > 0 ? static_cast<Uint32>(attr.Stride) : static_cast<Uint32>(attribByteSize);
// Verbatim, zero included. The frontend already resolved a pointer call's
// "tightly packed" stride 0 into the element size (see VertexAttribute::Stride),
// so a zero here is the binding model's stride 0 - every vertex reads the same
// element - which is exactly what a zero VkVertexInputBindingDescription::stride
// means. Substituting the element size fetched a fresh element per vertex and ran
// off the end of the buffer (KHR-GL43.vertex_attrib_binding.basic-input-case7/8).
// Client-memory arrays cannot reach zero: they only exist on the pointer path.
const Uint32 sourceStride = static_cast<Uint32>(attr.Stride);
const Bool packedAttribute = attr.Type == DataType::Int2101010Rev ||
attr.Type == DataType::Uint2101010Rev;
const SizeT requiredAlignment = packedAttribute ? attribByteSize : GetComponentSize(attr.Type);
@@ -169,16 +175,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// unless VK_EXT_legacy_vertex_attributes is available, so deinterleave this one
// attribute into a tightly packed transient stream without changing its format.
conversion = VertexStreamConversion::Repack;
MGLOG_W("Vertex attribute location=%u uses Vulkan-incompatible alignment "
MGLOG_W_ONCE("Vertex attribute location=%u uses Vulkan-incompatible alignment "
"(offset=%zu stride=%u required=%zu); using a tightly packed stream",
location, attr.Offset, sourceStride, requiredAlignment);
}
Uint32 stride = sourceStride;
if (conversion == VertexStreamConversion::Repack) {
stride = static_cast<Uint32>(attribByteSize);
} else if (conversion == VertexStreamConversion::ScaledIntegerToFloat32) {
stride = static_cast<Uint32>(attr.Size * static_cast<Int>(sizeof(Float)));
// A converted stream is tightly packed, so its stride is the converted element
// size - unless the source stride is zero, which does not describe a packing at
// all but "never advance". That survives the conversion unchanged: the draw path
// converts exactly one element and every vertex reads it.
if (sourceStride != 0) {
if (conversion == VertexStreamConversion::Repack) {
stride = static_cast<Uint32>(attribByteSize);
} else if (conversion == VertexStreamConversion::ScaledIntegerToFloat32) {
stride = static_cast<Uint32>(attr.Size * static_cast<Int>(sizeof(Float)));
}
}
const VkVertexInputRate inputRate =
(attr.Divisor == 0) ? VK_VERTEX_INPUT_RATE_VERTEX : VK_VERTEX_INPUT_RATE_INSTANCE;
@@ -23,7 +23,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT |
VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT |
VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT |
VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
// "Every usage" has to mean every usage: a buffer texture reached through an IMAGE
// unit takes a VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER descriptor, and the write is
// invalid unless the buffer was created with this bit. Nothing asked for it until
// imageBuffer support existed, so the omission was invisible.
VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
// Appended to kPersistentBackedUsage when VK_EXT_transform_feedback is enabled
// (see VkBufferManagerInitInfo::transformFeedbackUsageEnabled).
constexpr VkBufferUsageFlags kTransformFeedbackUsage =
@@ -298,7 +302,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
.requiredFlags = requiredFlags,
});
if (!created || resource.buffer.Map() == nullptr) {
MGLOG_E("VkBufferManager::CreateResidentStorage failed (size=%llu)",
MGLOG_E_ONCE("VkBufferManager::CreateResidentStorage failed (size=%llu)",
static_cast<unsigned long long>(size));
resource.buffer.Destroy();
resource.storageSize = 0;
@@ -320,7 +324,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return false;
}
if (!resource.buffer.Upload(bufferObject.MappedData(), size, 0)) {
MGLOG_E("VkBufferManager::SwapStorageAndUploadAll: upload failed");
MGLOG_E_ONCE("VkBufferManager::SwapStorageAndUploadAll: upload failed");
resource.pendingFullUpload = true;
return false;
}
@@ -379,6 +383,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
BumpSliceEpoch(*resource);
// Any cached streaming slice refers to the previous contents.
resource->transientFrameSerial = 0;
// Redefining the store hands any adopted mapping back to the CPU shadow
// (BufferObject::RedefineStorage), so a buffer that reaches here persistent-mapped
// is an ordinary resident one again: it needs the busy-tracking and conditional
// orphan below, and the next AcquirePersistentMap has to mint storage for the new
// store rather than hand back a mapping of the old one.
resource->persistentMapped = false;
if (!resource->buffer.IsValid()) {
return; // streaming-only resource: shadow + serial are enough
}
@@ -399,7 +409,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
if (!resource->buffer.Upload(bufferObject.MappedData(), size, 0)) {
MGLOG_E("VkBufferManager::OnRespecify: in-place upload failed");
MGLOG_E_ONCE("VkBufferManager::OnRespecify: in-place upload failed");
resource->pendingFullUpload = true;
}
}
@@ -424,7 +434,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (!IsResourceBusy(*resource)) {
if (!resource->buffer.Upload(bufferObject.MappedData() + offset,
static_cast<VkDeviceSize>(size), static_cast<VkDeviceSize>(offset))) {
MGLOG_E("VkBufferManager::OnSubData: host upload failed");
MGLOG_E_ONCE("VkBufferManager::OnSubData: host upload failed");
resource->pendingFullUpload = true;
}
return;
@@ -461,7 +471,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if ((appAccess & BufferMappingAccessBit::Unsynchronized) || !IsResourceBusy(*resource)) {
if (!resource->buffer.Upload(bufferObject.MappedData() + offset,
static_cast<VkDeviceSize>(size), static_cast<VkDeviceSize>(offset))) {
MGLOG_E("VkBufferManager::OnFlushMappedRange: host upload failed");
MGLOG_E_ONCE("VkBufferManager::OnFlushMappedRange: host upload failed");
resource->pendingFullUpload = true;
}
return;
@@ -553,7 +563,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const VkDeviceSize size = static_cast<VkDeviceSize>(bufferObject->GetSize());
if (size == 0) {
MGLOG_E("VkBufferManager::AcquireResidentSlice failed: buffer size is zero");
MGLOG_E_ONCE("VkBufferManager::AcquireResidentSlice failed: buffer size is zero");
return false;
}
@@ -575,7 +585,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return false;
}
if (!resource->buffer.Upload(bufferObject->MappedData(), size, 0)) {
MGLOG_E("VkBufferManager::AcquireResidentSlice failed: initial upload failed");
MGLOG_E_ONCE("VkBufferManager::AcquireResidentSlice failed: initial upload failed");
resource->buffer.Destroy();
resource->storageSize = 0;
resource->usageFlags = 0;
@@ -610,7 +620,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const VkDeviceSize size = static_cast<VkDeviceSize>(bufferObject->GetSize());
if (size == 0) {
MGLOG_E("VkBufferManager::AcquireStreamedSlice failed: buffer size is zero");
MGLOG_E_ONCE("VkBufferManager::AcquireStreamedSlice failed: buffer size is zero");
return false;
}
@@ -708,7 +718,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
case BufferKind::Uniform:
return VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT;
case BufferKind::TextureBuffer:
return VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT;
// Both texel roles, for the same reason vertex/index carry both bits: one GL buffer
// texture can be read as a samplerBuffer and written as an imageBuffer, and which of
// the two it is only becomes known when a shader that uses it is bound - long after
// the resident buffer was created. A VkBufferView for a storage-texel descriptor is
// invalid unless the buffer was created with the storage bit, so a buffer that
// acquired only the uniform bit could never be given one.
return VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT;
case BufferKind::ShaderStorage:
return VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT;
case BufferKind::Indirect:
@@ -76,7 +76,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const VkResult result =
vmaCreateBuffer(m_allocator, &bufferInfo, &allocationInfo, &m_buffer, &m_allocation, nullptr);
if (result != VK_SUCCESS) {
MGLOG_E("VkBufferObject::Create failed: vmaCreateBuffer returned %d", result);
MGLOG_E_ONCE("VkBufferObject::Create failed: vmaCreateBuffer returned %d", result);
m_allocator = nullptr;
m_buffer = VK_NULL_HANDLE;
m_allocation = nullptr;
@@ -108,7 +108,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const VkResult mapResult = vmaMapMemory(m_allocator, m_allocation, &m_mappedData);
if (mapResult != VK_SUCCESS || m_mappedData == nullptr) {
MGLOG_E("VkBufferObject::Map failed: vmaMapMemory returned %d", mapResult);
MGLOG_E_ONCE("VkBufferObject::Map failed: vmaMapMemory returned %d", mapResult);
m_mappedData = nullptr;
return nullptr;
}
@@ -138,14 +138,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const Bool wasMapped = IsMapped();
void* mapped = wasMapped ? m_mappedData : Map();
if (mapped == nullptr) {
MGLOG_E("VkBufferObject::Upload failed: unable to map buffer");
MGLOG_E_ONCE("VkBufferObject::Upload failed: unable to map buffer");
return false;
}
Memcpy(static_cast<Uint8*>(mapped) + offset, data, static_cast<SizeT>(size));
const VkResult flushResult = vmaFlushAllocation(m_allocator, m_allocation, offset, size);
if (flushResult != VK_SUCCESS) {
MGLOG_E("VkBufferObject::Upload failed: vmaFlushAllocation returned %d", flushResult);
MGLOG_E_ONCE("VkBufferObject::Upload failed: vmaFlushAllocation returned %d", flushResult);
if (!wasMapped) {
Unmap();
}
@@ -170,7 +170,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const VkResult result = vmaInvalidateAllocation(m_allocator, m_allocation, offset, resolvedSize);
if (result != VK_SUCCESS) {
MGLOG_E("VkBufferObject::Invalidate failed: vmaInvalidateAllocation returned %d", result);
MGLOG_E_ONCE("VkBufferObject::Invalidate failed: vmaInvalidateAllocation returned %d", result);
return false;
}
return true;
@@ -123,7 +123,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
if (!attachment.IsComplete()) {
MGLOG_W("GetOrCreateRenderPass: draw buffer slot %u (%s) on FBO %u has an incomplete texture attachment; using VK_ATTACHMENT_UNUSED",
MGLOG_W_ONCE("GetOrCreateRenderPass: draw buffer slot %u (%s) on FBO %u has an incomplete texture attachment; using VK_ATTACHMENT_UNUSED",
drawBufferIndex,
MG_Util::ConvertFramebufferAttachmentTypeToString(attachmentType).c_str(),
fbo.GetExternalIndex());
@@ -132,7 +132,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
auto* texture = attachment.GetTexture().get();
if (texture == nullptr) {
MGLOG_W("GetOrCreateRenderPass: draw buffer slot %u (%s) on FBO %u resolved to a null texture; using VK_ATTACHMENT_UNUSED",
MGLOG_W_ONCE("GetOrCreateRenderPass: draw buffer slot %u (%s) on FBO %u resolved to a null texture; using VK_ATTACHMENT_UNUSED",
drawBufferIndex,
MG_Util::ConvertFramebufferAttachmentTypeToString(attachmentType).c_str(),
fbo.GetExternalIndex());
@@ -311,7 +311,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
if (!TryResolveSampleCountFlagBits(renderbuffer->GetSamples(), sampleCount)) {
MGLOG_E("GetOrCreateRenderbufferResource: unsupported renderbuffer sample count %d for renderbuffer %u",
MGLOG_E_ONCE("GetOrCreateRenderbufferResource: unsupported renderbuffer sample count %d for renderbuffer %u",
renderbuffer->GetSamples(),
renderbuffer->GetExternalIndex());
return nullptr;
@@ -457,7 +457,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_physicalDevice, format, imageInfo.imageType, imageInfo.tiling, imageInfo.usage, imageInfo.flags,
&imageFormatProperties);
if (imageFormatResult != VK_SUCCESS || (imageFormatProperties.sampleCounts & sampleCount) == 0) {
MGLOG_E("GetOrCreateRenderbufferResource: unsupported renderbuffer format=%d samples=%d for renderbuffer %u",
MGLOG_E_ONCE("GetOrCreateRenderbufferResource: unsupported renderbuffer format=%d samples=%d for renderbuffer %u",
static_cast<Int>(format),
static_cast<Int>(sampleCount),
renderbuffer->GetExternalIndex());
@@ -929,7 +929,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const auto& renderbuffer = rbAtt.GetRenderbuffer();
auto* rbResource = GetOrCreateRenderbufferResource(renderbuffer);
if (rbResource == nullptr || (rbResource->aspect & VK_IMAGE_ASPECT_COLOR_BIT) == 0) {
MGLOG_E("GetOrCreateRenderPass: draw buffer slot %u on FBO %u has an unsupported color "
MGLOG_E_ONCE("GetOrCreateRenderPass: draw buffer slot %u on FBO %u has an unsupported color "
"renderbuffer %u; using VK_ATTACHMENT_UNUSED",
i, fbo.GetExternalIndex(), renderbuffer->GetExternalIndex());
continue;
@@ -1105,7 +1105,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
adoptRenderPassSampleCount(attachmentSampleCount, "color", texture->GetExternalIndex());
if (!hasClear && trackedColorLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
MGLOG_W("GetOrCreateRenderPass: color attachment textureId=%d starts with undefined layout and no clear; "
MGLOG_W_ONCE("GetOrCreateRenderPass: color attachment textureId=%d starts with undefined layout and no clear; "
"using LOAD_OP_DONT_CARE",
texture->GetExternalIndex());
desc.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
@@ -1161,7 +1161,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
isUsableDepthStencilAttachment(depthAtt) && isUsableDepthStencilAttachment(stencilAtt) &&
!sameDepthStencilAttachmentObject(depthAtt, stencilAtt);
if (hasDistinctDepthAndStencilAttachments) {
MGLOG_E("GetOrCreateRenderPass: separate depth/stencil attachments are not supported yet; using the depth attachment and ignoring the standalone stencil attachment for framebuffer %u",
MGLOG_E_ONCE("GetOrCreateRenderPass: separate depth/stencil attachments are not supported yet; using the depth attachment and ignoring the standalone stencil attachment for framebuffer %u",
fbo.GetExternalIndex());
}
if (selectedDepthStencilAttachment != nullptr) {
@@ -1223,7 +1223,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
depthAttachmentDescription.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
depthAttachmentDescription.initialLayout = loadInfo.initialLayout;
if (trackedDepthLayout == VK_IMAGE_LAYOUT_UNDEFINED && (!clearDepth || !clearStencil)) {
MGLOG_W("GetOrCreateRenderPass: depth/stencil attachment id=%d starts with undefined layout "
MGLOG_W_ONCE("GetOrCreateRenderPass: depth/stencil attachment id=%d starts with undefined layout "
"and partial/no clear; using DONT_CARE for uncleared aspects",
depthAttachmentId);
}
@@ -950,7 +950,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(texture.GetFormat());
const VkComponentMapping sampledComponents = ResolveSampledViewComponents(texture, formatInfo);
const VkImageAspectFlags sampledAspect = ResolveSampledImageViewAspectMask(resource->aspect);
const VkImageAspectFlags sampledAspect =
ResolveSampledImageViewAspectMask(resource->aspect, texture.GetDepthStencilTextureMode());
perMipSampledView = CreateImageView(resource->image, resource->format, sampledAspect, resource->viewType,
mipLevel, 1, 0, resource->arrayLayers, &sampledComponents);
if (perMipSampledView == VK_NULL_HANDLE) {
@@ -974,13 +975,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return resource->sampledView;
}
if (!AreSampledImageViewFormatsCompatible(resource->format, format)) {
MGLOG_E("%s: incompatible sampled image view format=%d for textureId=%d imageFormat=%d",
MGLOG_E_ONCE("%s: incompatible sampled image view format=%d for textureId=%d imageFormat=%d",
__func__, static_cast<Int>(format), texture.GetExternalIndex(),
static_cast<Int>(resource->format));
return VK_NULL_HANDLE;
}
if ((resource->imageCreateFlags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) == 0) {
MGLOG_E("%s: textureId=%d needs mutable image format=%d for sampled view format=%d",
MGLOG_E_ONCE("%s: textureId=%d needs mutable image format=%d for sampled view format=%d",
__func__, texture.GetExternalIndex(), static_cast<Int>(resource->format),
static_cast<Int>(format));
return VK_NULL_HANDLE;
@@ -1000,7 +1001,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkFormatProperties formatProperties{};
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties);
if ((formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) == 0) {
MGLOG_E("%s: sampled image view format=%d lacks VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT "
MGLOG_E_ONCE("%s: sampled image view format=%d lacks VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT "
"for textureId=%d (available=0x%x)",
__func__, static_cast<Int>(format), texture.GetExternalIndex(),
static_cast<Uint32>(formatProperties.optimalTilingFeatures));
@@ -1014,7 +1015,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
resource->sampledBaseMipLevel, resource->sampledLevelCount, 0, resource->arrayLayers,
&sampledComponents, VK_IMAGE_USAGE_SAMPLED_BIT);
if (view == VK_NULL_HANDLE) {
MGLOG_E("%s: failed to create sampled image view textureId=%d imageFormat=%d viewFormat=%d",
MGLOG_E_ONCE("%s: failed to create sampled image view textureId=%d imageFormat=%d viewFormat=%d",
__func__, texture.GetExternalIndex(), static_cast<Int>(resource->format),
static_cast<Int>(format));
return VK_NULL_HANDLE;
@@ -1042,14 +1043,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
format = resource->format;
}
if (!AreStorageImageViewFormatsCompatible(resource->format, format)) {
MGLOG_E("%s: incompatible storage image view format=%d for textureId=%d imageFormat=%d",
MGLOG_E_ONCE("%s: incompatible storage image view format=%d for textureId=%d imageFormat=%d",
__func__, static_cast<Int>(format), texture.GetExternalIndex(),
static_cast<Int>(resource->format));
return VK_NULL_HANDLE;
}
if (format != resource->format &&
(resource->imageCreateFlags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) == 0) {
MGLOG_E("%s: textureId=%d needs mutable image format=%d for storage view format=%d",
MGLOG_E_ONCE("%s: textureId=%d needs mutable image format=%d for storage view format=%d",
__func__, texture.GetExternalIndex(), static_cast<Int>(resource->format),
static_cast<Int>(format));
return VK_NULL_HANDLE;
@@ -1069,7 +1070,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
viewType = VK_IMAGE_VIEW_TYPE_2D;
break;
case VK_IMAGE_VIEW_TYPE_3D:
MGLOG_E("%s: non-layered 3D storage views are unsupported for textureId=%d",
MGLOG_E_ONCE("%s: non-layered 3D storage views are unsupported for textureId=%d",
__func__, texture.GetExternalIndex());
return VK_NULL_HANDLE;
default:
@@ -1078,7 +1079,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (viewType != resource->viewType) {
if (layer < 0 || static_cast<Uint32>(layer) >= resource->arrayLayers) {
MGLOG_E("%s: storage image layer=%d is out of range for textureId=%d arrayLayers=%u",
MGLOG_E_ONCE("%s: storage image layer=%d is out of range for textureId=%d arrayLayers=%u",
__func__, layer, texture.GetExternalIndex(), resource->arrayLayers);
return VK_NULL_HANDLE;
}
@@ -1113,7 +1114,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkFormatProperties formatProperties{};
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties);
if ((formatProperties.optimalTilingFeatures & requiredFormatFeatures) != requiredFormatFeatures) {
MGLOG_E("%s: storage image view format=%d lacks required features=0x%x for textureId=%d "
MGLOG_E_ONCE("%s: storage image view format=%d lacks required features=0x%x for textureId=%d "
"(available=0x%x)",
__func__, static_cast<Int>(format), static_cast<Uint32>(requiredFormatFeatures),
texture.GetExternalIndex(), static_cast<Uint32>(formatProperties.optimalTilingFeatures));
@@ -1124,7 +1125,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
mipLevel, 1, baseArrayLayer, layerCount, nullptr,
VK_IMAGE_USAGE_STORAGE_BIT);
if (view == VK_NULL_HANDLE) {
MGLOG_E("%s: failed to create storage image view for textureId=%d mip=%u imageFormat=%d viewFormat=%d",
MGLOG_E_ONCE("%s: failed to create storage image view for textureId=%d mip=%u imageFormat=%d viewFormat=%d",
__func__, texture.GetExternalIndex(), mipLevel, static_cast<Int>(resource->format),
static_cast<Int>(format));
return VK_NULL_HANDLE;
@@ -1222,7 +1223,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return true;
}
if (resource->layout == VK_IMAGE_LAYOUT_UNDEFINED) {
MGLOG_W("TransitionTextureForSampling: textureId=%d is still in VK_IMAGE_LAYOUT_UNDEFINED before sampling",
MGLOG_W_ONCE("TransitionTextureForSampling: textureId=%d is still in VK_IMAGE_LAYOUT_UNDEFINED before sampling",
texture.GetExternalIndex());
}
@@ -1573,7 +1574,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// targets this manager has no Vulkan image shape for yet (cube map arrays above all).
// Declining the sync leaves the texture unbacked - wrong, but recoverable - where an
// assertion would take the whole process down instead.
MGLOG_W("SyncTextureResource: unsupported uploadTarget=%s textureTarget=%s textureId=%d size=(%d,%d,%d) "
MGLOG_W_ONCE("SyncTextureResource: unsupported uploadTarget=%s textureTarget=%s textureId=%d size=(%d,%d,%d) "
"mipLevels=%u vkViewType=%d",
MG_Util::ConvertTextureUploadTargetToString(uploadTarget).c_str(),
MG_Util::ConvertTextureTargetToString(texture.GetTarget()).c_str(), texture.GetExternalIndex(),
@@ -1802,7 +1803,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Losing reinterpreted views only degrades the formatless-image feature for
// this texture; failing creation would lose the texture entirely, so retry
// as a plain immutable-format image.
MGLOG_W("%s: mutable image format=%d is unsupported for textureId=%d; creating "
MGLOG_W_ONCE("%s: mutable image format=%d is unsupported for textureId=%d; creating "
"without VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT (format reinterpretation "
"will be unavailable for it)",
__func__, static_cast<Int>(format), texture.GetExternalIndex());
@@ -1820,7 +1821,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Losing 2D-array compatibility only costs per-slice framebuffer attachment for this
// format; failing creation would lose the texture entirely. Remembered so later syncs
// neither reprobe nor flag-mismatch against this image and recreate it.
MGLOG_W("%s: VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT is unsupported for format=%d "
MGLOG_W_ONCE("%s: VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT is unsupported for format=%d "
"textureId=%d; creating without it (per-slice framebuffer attachment will be "
"unavailable for it)",
__func__, static_cast<Int>(format), texture.GetExternalIndex());
@@ -1852,7 +1853,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const VkResult createImageResult =
vmaCreateImage(m_allocator, &imageInfo, &allocationInfo, &resource.image, &resource.allocation, nullptr);
if (createImageResult != VK_SUCCESS) {
MGLOG_F("SyncTextureResource: vmaCreateImage failed (%d) textureId=%d extent=%ux%u depth=%u layers=%u "
// E_ONCE, not F: the comment above says it - this is a soft failure the caller
// recovers from, and it re-fires on every sync of every texture the driver refuses.
MGLOG_E_ONCE("SyncTextureResource: vmaCreateImage failed (%d) textureId=%d extent=%ux%u depth=%u layers=%u "
"mips=%u samples=%d format=%d",
createImageResult, texture.GetExternalIndex(), imageInfo.extent.width, imageInfo.extent.height,
imageInfo.extent.depth, imageInfo.arrayLayers, imageInfo.mipLevels,
@@ -2238,7 +2241,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (resource.fullView == VK_NULL_HANDLE) {
return false;
}
const VkImageAspectFlags sampledAspect = ResolveSampledImageViewAspectMask(resource.aspect);
const VkImageAspectFlags sampledAspect =
ResolveSampledImageViewAspectMask(resource.aspect, texture.GetDepthStencilTextureMode());
resource.sampledView = CreateImageView(resource.image, resource.format, sampledAspect, resource.viewType,
baseMipLevel, levelCount, 0, resource.arrayLayers, &sampledComponents);
if (resource.sampledView == VK_NULL_HANDLE) {
@@ -2424,7 +2428,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const Bool srcIsD24S8 = outResource.format == VK_FORMAT_D24_UNORM_S8_UINT;
const Bool srcIsD32FS8 = outResource.format == VK_FORMAT_D32_SFLOAT_S8_UINT;
if (!srcIsD24S8 && !srcIsD32FS8) {
MGLOG_E("UploadDirtyMipLevels: unsupported combined depth-stencil format %d for textureId=%d",
MGLOG_E_ONCE("UploadDirtyMipLevels: unsupported combined depth-stencil format %d for textureId=%d",
static_cast<Int>(outResource.format), mipmapTexture.GetExternalIndex());
for (const auto& item : uploadItems) {
mipmapTexture.MarkStorageDirty(item.target, item.level, false);
@@ -2860,10 +2864,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
VkImageAspectFlags VkTextureManager::ResolveSampledImageViewAspectMask(VkImageAspectFlags imageAspect) {
VkImageAspectFlags VkTextureManager::ResolveSampledImageViewAspectMask(VkImageAspectFlags imageAspect,
GLenum depthStencilTextureMode) {
if ((imageAspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0) {
return VK_IMAGE_ASPECT_COLOR_BIT;
}
// A sampled view of a combined depth/stencil image may name exactly one aspect
// (VUID-VkDescriptorImageInfo-imageView-01976), and GL_DEPTH_STENCIL_TEXTURE_MODE is
// what picks it - the whole content of GL_ARB_stencil_texturing. Depth stays the
// default, so nothing that never sets the mode changes shape. The texture's params
// version moves with the mode, which is what makes the cached views be rebuilt.
if (depthStencilTextureMode == GL_STENCIL_INDEX && (imageAspect & VK_IMAGE_ASPECT_STENCIL_BIT) != 0) {
return VK_IMAGE_ASPECT_STENCIL_BIT;
}
if ((imageAspect & VK_IMAGE_ASPECT_DEPTH_BIT) != 0) {
return VK_IMAGE_ASPECT_DEPTH_BIT;
}
@@ -379,7 +379,11 @@ public:
// true - a false positive merely ends the render pass, a false negative would skip a barrier.
Bool NeedsStorageImagePreparation(MG_State::GLState::ITextureObject& texture) const;
static VkImageAspectFlags ResolveSampledImageViewAspectMask(VkImageAspectFlags imageAspect);
// `depthStencilTextureMode` is the texture's GL_DEPTH_STENCIL_TEXTURE_MODE; it only decides
// anything for an image that carries both aspects. Defaulted so the call sites that have no
// texture in hand keep the depth-aspect answer they have always given.
static VkImageAspectFlags ResolveSampledImageViewAspectMask(VkImageAspectFlags imageAspect,
GLenum depthStencilTextureMode = GL_DEPTH_COMPONENT);
static VkFormat ResolveSampledImageViewFormat(VkFormat imageFormat, SamplerNumericDomain numericDomain);
static Bool AreSampledImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat);
static Bool AreStorageImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat);
@@ -15,7 +15,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MOBILEGL_ASSERT(initInfo.device != VK_NULL_HANDLE, "VkTimerQueryManager::Initialize requires valid VkDevice");
MOBILEGL_ASSERT(initInfo.frameCount > 0, "VkTimerQueryManager::Initialize requires non-zero frame count");
if (initInfo.timestampValidBits == 0 || initInfo.timestampPeriodNs <= 0.0f || initInfo.slotsPerPool == 0) {
MGLOG_W("VkTimerQueryManager: timestamps unsupported (validBits=%u, period=%f, slots=%u)",
MGLOG_W_ONCE("VkTimerQueryManager: timestamps unsupported (validBits=%u, period=%f, slots=%u)",
initInfo.timestampValidBits, initInfo.timestampPeriodNs, initInfo.slotsPerPool);
return false;
}
@@ -35,7 +35,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
for (auto& poolState : m_pools) {
const VkResult result = vkCreateQueryPool(m_device, &poolInfo, nullptr, &poolState.pool);
if (result != VK_SUCCESS) {
MGLOG_E("VkTimerQueryManager: vkCreateQueryPool failed with %s", VkResultToString(result));
MGLOG_E_ONCE("VkTimerQueryManager: vkCreateQueryPool failed with %s", VkResultToString(result));
Shutdown();
return false;
}
@@ -90,7 +90,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
auto& poolState = m_pools[frameIndex];
if (poolState.cursor >= m_slotsPerPool) {
if (!poolState.exhaustionWarned) {
MGLOG_W("VkTimerQueryManager: frame %u timestamp pool exhausted (%u slots); further timer queries "
MGLOG_W_ONCE("VkTimerQueryManager: frame %u timestamp pool exhausted (%u slots); further timer queries "
"this frame fall back to the frontend path",
frameIndex, m_slotsPerPool);
poolState.exhaustionWarned = true;
@@ -120,7 +120,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_device, m_pools[record.poolIndex].pool, record.slot, 1, sizeof(resultWithAvailability),
resultWithAvailability, sizeof(Uint64), VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WITH_AVAILABILITY_BIT);
if (result != VK_SUCCESS && result != VK_NOT_READY) {
MGLOG_E("VkTimerQueryManager: vkGetQueryPoolResults failed with %s", VkResultToString(result));
MGLOG_E_ONCE("VkTimerQueryManager: vkGetQueryPoolResults failed with %s", VkResultToString(result));
return false;
}
if (resultWithAvailability[1] == 0) {
File diff suppressed because it is too large Load Diff
@@ -773,6 +773,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
ProgramFactory::CompileOptionFlags m_lastLodBaseFlags = {};
ProgramFactory::CompileOptionFlags m_lastLodResultFlags = {};
// Does the current program's vertex stage declare the BaseVertex builtin? A property
// of the program's SPIR-V, so (lifetime id, backend-state version) is the whole key.
//
// Memoized rather than re-asked because asking means resolving the UN-zeroed program
// variant, and a program that only ever draws non-indexed would then compile a variant
// no draw uses AND re-stamp its use every draw, so the idle sweep could never retire
// it. With the memo the answer is known before the first lookup and only the variant
// the draw actually needs is resolved.
Bool m_lastBaseVertexQueryValid = false;
Uint64 m_lastBaseVertexProgramLifetimeId = 0;
Uint32 m_lastBaseVertexProgramVersion = 0;
Bool m_lastBaseVertexReads = false;
// Snapshot behind TrySetupDrawFastPath. Values only: the program and
// render-pass caches are open-addressing maps whose entries move on
// insert, so no pointers into them are cached; the pipeline handle is
@@ -74,6 +74,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// The context line (__VA_ARGS__ = its own format string + args) must be a SEPARATE log
// call: appending its format to the base format while its arguments precede the base
// arguments makes every conversion read the wrong slot (a %s pulling an int crashes).
//
// MGLOG_F and deliberately NOT latched. VK_VERIFY is the invariant-check macro: a Vulkan call
// MobileGL believes it has already made legal came back non-success, which is a
// should-never-happen state, not an expected failure mode a user hits. Those fast-fail loudly
// and keep saying so - the log-quietness rules that latch W/E cover expected failures (driver
// capability gaps, app misuse), not broken internal invariants. MOBILEGL_ASSERT below traps in
// a DEBUG build; MGLOG_F is what makes the same condition visible in an INFO test run, where
// the assert is compiled out by contract.
//
// A soft, recoverable failure must therefore NOT be routed through VK_VERIFY. Check the
// VkResult directly and report it with MGLOG_E_ONCE - see VkTextureManager::SyncTextureResource,
// where a driver legitimately refuses an image the format pre-check accepted.
#define VK_VERIFY(expr, ...) \
do { \
VkResult _vk_verify_result = (expr); \
+11 -11
View File
@@ -21,7 +21,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
EGLStateContext* GetState() {
if (!MG_State::pEGLContext) {
MGLOG_E("pEGLContext is null. MG_State may not be initialized.");
MGLOG_E_ONCE("pEGLContext is null. MG_State may not be initialized.");
}
return MG_State::pEGLContext.get();
}
@@ -146,7 +146,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
auto* backendObject = GetBackendObject(state);
if (!backendObject) {
MGLOG_E("activeBackendObject not initialized!");
MGLOG_E_ONCE("activeBackendObject not initialized!");
state->DestroySurface(dpy, surface);
return EGL_NO_SURFACE;
}
@@ -172,11 +172,11 @@ namespace MobileGL::MG_Impl::EGLImpl {
auto* backendObject = GetBackendObject(state);
if (!backendObject) {
MGLOG_E("activeBackendObject not initialized!");
MGLOG_E_ONCE("activeBackendObject not initialized!");
return EGL_FALSE;
}
if (!backendObject->SwapEGLBuffers(dpy, draw)) {
MGLOG_E("eglSwapBuffers failed on thread=%s dpy=%p draw=%p", CurrentThreadIdString().c_str(), dpy, draw);
MGLOG_E_ONCE("eglSwapBuffers failed on thread=%s dpy=%p draw=%p", CurrentThreadIdString().c_str(), dpy, draw);
state->SetError(EGL_BAD_SURFACE);
return EGL_FALSE;
}
@@ -211,7 +211,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
auto* backendObject = GetBackendObject(state);
if (!backendObject) {
MGLOG_E("activeBackendObject not initialized!");
MGLOG_E_ONCE("activeBackendObject not initialized!");
return EGL_FALSE;
}
if (!backendObject->InitializeEGLDisplay(dpy, major, minor)) {
@@ -265,7 +265,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
if (releaseCurrentRequest) {
if (auto* backendObject = MG_Backend::pActiveBackendObject.get()) {
if (!backendObject->MakeEGLCurrent(dpy, draw, read, ctx)) {
MGLOG_E("eglMakeCurrent release failed in backend thread=%s", threadId.c_str());
MGLOG_E_ONCE("eglMakeCurrent release failed in backend thread=%s", threadId.c_str());
state->MakeCurrent(oldDisplay, oldDraw, oldRead, oldContext);
state->SetError(EGL_BAD_ACCESS);
return EGL_FALSE;
@@ -277,12 +277,12 @@ namespace MobileGL::MG_Impl::EGLImpl {
auto* backendObject = GetBackendObject(state);
if (!backendObject) {
MGLOG_E("activeBackendObject not initialized!");
MGLOG_E_ONCE("activeBackendObject not initialized!");
state->MakeCurrent(oldDisplay, oldDraw, oldRead, oldContext);
return EGL_FALSE;
}
if (!backendObject->MakeEGLCurrent(dpy, draw, read, ctx)) {
MGLOG_E("eglMakeCurrent backend attach failed thread=%s dpy=%p draw=%p read=%p ctx=%p", threadId.c_str(),
MGLOG_E_ONCE("eglMakeCurrent backend attach failed thread=%s dpy=%p draw=%p read=%p ctx=%p", threadId.c_str(),
dpy, draw, read, ctx);
state->SetError(EGL_BAD_ACCESS);
state->MakeCurrent(oldDisplay, oldDraw, oldRead, oldContext);
@@ -703,7 +703,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
auto* backendObject = GetBackendObject(state);
if (!backendObject) {
MGLOG_E("activeBackendObject not initialized!");
MGLOG_E_ONCE("activeBackendObject not initialized!");
state->DestroySurface(dpy, surface);
return EGL_NO_SURFACE;
}
@@ -726,7 +726,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
}
auto* backendObject = GetBackendObject(state);
if (!backendObject) {
MGLOG_E("activeBackendObject not initialized!");
MGLOG_E_ONCE("activeBackendObject not initialized!");
return EGL_FALSE;
}
width = std::max<EGLint>(width, 1);
@@ -764,7 +764,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
MGLOG_D("eglGetProcAddress(%s)", name);
void* proc = MG_Impl::GetProcAddress(name);
if (!proc) {
MGLOG_W("Failed to get function: %s", name);
MGLOG_D("Failed to get function: %s", name);
return nullptr;
}
return (__eglMustCastToProperFunctionPointerType)proc;
+1 -1
View File
@@ -149,7 +149,7 @@ namespace MobileGL::MG_Impl::GLImpl {
// quietly writing a differently-sized pattern.
const SizeT sourceSize = MG_Util::GetInputBytesPerPixel(inputFormat, pixelType);
if (sourceSize != elementSize) {
MGLOG_W("%s: clear pattern is %zu bytes but internalformat 0x%X stores %zu; "
MGLOG_W_ONCE("%s: clear pattern is %zu bytes but internalformat 0x%X stores %zu; "
"converting between them is not implemented",
GetBufferOpName(op), sourceSize, internalformat, elementSize);
}
@@ -1259,7 +1259,15 @@ namespace MobileGL::MG_Impl::GLImpl {
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "instancecount must be non-negative."));
return;
}
if (!MG_State::pGLContext->ValidateTransformFeedbackName(id)) {
// "id is not the name of a transform feedback object" has to mean the same thing here
// as it does to glIsTransformFeedback, and the two predicates are not interchangeable:
// a name glGenTransformFeedbacks handed out is only reserved until it is first bound,
// and only the bind turns it into an object (GL 4.6 core 13.2.1). ValidateTransformFeedbackName
// answers the reservation question - the right one for glBindTransformFeedback, which is
// what turns a reserved name into an object - so using it here let a generated-but-unbound
// name through to the completed-span check below and raised INVALID_OPERATION where the
// spec asks for INVALID_VALUE. Name 0 is the default object and always drawable.
if (id != 0 && !MG_State::pGLContext->IsTransformFeedbackObject(id)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
@@ -25,12 +25,12 @@
#define DECLARE_GL_FUNCTION_STUB_HEAD(type, name, ...) MOBILEGL_GL_API type gl##name(__VA_ARGS__) {
#define DECLARE_GL_FUNCTION_STUB_END(type, name, ...) \
MGLOG_W("Stub function: %s(...)", __FUNCTION__); \
MGLOG_W_ONCE("Stub function: %s(...)", __FUNCTION__); \
return (type)1; \
}
#define DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(type, name, ...) \
MGLOG_W("Stub function: %s(...)", __FUNCTION__); \
MGLOG_W_ONCE("Stub function: %s(...)", __FUNCTION__); \
}
#define DECLARE_GL_FUNCTION_HEAD(type, name, ...) MOBILEGL_GL_API type gl##name(__VA_ARGS__) {
@@ -1003,9 +1003,9 @@ DECLARE_GL_FUNCTION_HEAD(void, ClearTexImage, GLuint texture, GLint level, GLenu
DECLARE_GL_FUNCTION_HEAD(void, ClearTexSubImage, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearTexSubImage, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, type, data)
DECLARE_GL_FUNCTION_HEAD(void, BindBuffersBase, GLenum target, GLuint first, GLsizei count, const GLuint* buffers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindBuffersBase, target, first, count, buffers)
DECLARE_GL_FUNCTION_HEAD(void, BindBuffersRange, GLenum target, GLuint first, GLsizei count, const GLuint* buffers, const GLintptr* offsets, const GLsizeiptr* sizes) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindBuffersRange, target, first, count, buffers, offsets, sizes)
DECLARE_GL_FUNCTION_STUB_HEAD(void, BindTextures, GLuint first, GLsizei count, const GLuint* textures) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BindTextures, first, count, textures)
DECLARE_GL_FUNCTION_HEAD(void, BindTextures, GLuint first, GLsizei count, const GLuint* textures) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindTextures, first, count, textures)
DECLARE_GL_FUNCTION_HEAD(void, BindSamplers, GLuint first, GLsizei count, const GLuint* samplers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindSamplers, first, count, samplers)
DECLARE_GL_FUNCTION_STUB_HEAD(void, BindImageTextures, GLuint first, GLsizei count, const GLuint* textures) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BindImageTextures, first, count, textures)
DECLARE_GL_FUNCTION_HEAD(void, BindImageTextures, GLuint first, GLsizei count, const GLuint* textures) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindImageTextures, first, count, textures)
DECLARE_GL_FUNCTION_HEAD(void, BindVertexBuffers, GLuint first, GLsizei count, const GLuint* buffers, const GLintptr* offsets, const GLsizei* strides) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindVertexBuffers, first, count, buffers, offsets, strides)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ClipControl, GLenum origin, GLenum depth) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClipControl, origin, depth)
DECLARE_GL_FUNCTION_HEAD(void, CreateTransformFeedbacks, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CreateTransformFeedbacks, n, ids)
@@ -1061,7 +1061,7 @@ DECLARE_GL_FUNCTION_HEAD(void, TextureSubImage1D, GLuint texture, GLint level, G
DECLARE_GL_FUNCTION_HEAD(void, TextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLenum type, const void* pixels) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, type, pixels)
DECLARE_GL_FUNCTION_HEAD(void, TextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* pixels) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureSubImage3D, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, type, pixels)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLsizei width, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage1D, texture, level, xoffset, width, format, imageSize, data)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, imageSize, data)
DECLARE_GL_FUNCTION_HEAD(void, CompressedTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, imageSize, data)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, data)
DECLARE_GL_FUNCTION_HEAD(void, CopyTextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyTextureSubImage1D, texture, level, xoffset, x, y, width)
DECLARE_GL_FUNCTION_HEAD(void, CopyTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyTextureSubImage2D, texture, level, xoffset, yoffset, x, y, width, height)
@@ -1849,7 +1849,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureImage3DEXT, GLuint texture,
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureImage2DEXT, GLuint texture, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureImage2DEXT, texture, target, level, internalformat, width, height, border, imageSize, bits)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureImage1DEXT, GLuint texture, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureImage1DEXT, texture, target, level, internalformat, width, border, imageSize, bits)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage3DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage3DEXT, texture, target, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, bits)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage2DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage2DEXT, texture, target, level, xoffset, yoffset, width, height, format, imageSize, bits)
DECLARE_GL_FUNCTION_HEAD(void, CompressedTextureSubImage2DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, imageSize, bits)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage1DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLsizei width, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage1DEXT, texture, target, level, xoffset, width, format, imageSize, bits)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetCompressedTextureImageEXT, GLuint texture, GLenum target, GLint lod, void* img) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetCompressedTextureImageEXT, texture, target, lod, img)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedMultiTexImage3DEXT, GLenum texunit, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedMultiTexImage3DEXT, texunit, target, level, internalformat, width, height, depth, border, imageSize, bits)
@@ -2585,7 +2585,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, BindTransformFeedbackNV, GLenum target, GLui
DECLARE_GL_FUNCTION_STUB_HEAD(void, DeleteTransformFeedbacksNV, GLsizei n, const GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DeleteTransformFeedbacksNV, n, ids)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GenTransformFeedbacksNV, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GenTransformFeedbacksNV, n, ids)
MOBILEGL_GL_API GLboolean glIsTransformFeedbackNV(GLuint id) {
MGLOG_W("Stub function: %s(...)", __FUNCTION__);
MGLOG_W_ONCE("Stub function: %s(...)", __FUNCTION__);
return GL_FALSE;
}
DECLARE_GL_FUNCTION_STUB_HEAD(void, PauseTransformFeedbackNV, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PauseTransformFeedbackNV, )
@@ -3181,5 +3181,5 @@ MOBILEGL_GL_API void glVertexAttribDivisorARB(GLuint index, GLuint divisor) {
}
MOBILEGL_GL_API void glWindowRectanglesEXT(GLenum mode, GLsizei count, const GLint* box) {
MGLOG_W("Stub function: %s(...)", __FUNCTION__);
MGLOG_W_ONCE("Stub function: %s(...)", __FUNCTION__);
}
@@ -547,7 +547,7 @@ namespace MobileGL::MG_Impl::GLImpl {
GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter) {
auto blitNamedFramebuffer = MG_Backend::gBackendFunctionsTable.GL.BlitNamedFramebuffer;
if (!blitNamedFramebuffer) {
MGLOG_E("glBlitNamedFramebuffer skipped: backend does not implement explicit framebuffer blit.");
MGLOG_E_ONCE("glBlitNamedFramebuffer skipped: backend does not implement explicit framebuffer blit.");
return;
}
blitNamedFramebuffer(readFramebuffer, drawFramebuffer, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1,
@@ -558,7 +558,7 @@ namespace MobileGL::MG_Impl::GLImpl {
GLenum buffer, GLint drawbuffer, const GLfloat* value) {
auto clearNamedFramebufferfv = MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferfv;
if (!clearNamedFramebufferfv) {
MGLOG_E("glClearNamedFramebufferfv skipped: backend does not implement explicit framebuffer clear.");
MGLOG_E_ONCE("glClearNamedFramebufferfv skipped: backend does not implement explicit framebuffer clear.");
return;
}
clearNamedFramebufferfv(framebuffer, buffer, drawbuffer, value);
@@ -568,7 +568,7 @@ namespace MobileGL::MG_Impl::GLImpl {
GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) {
auto clearNamedFramebufferfi = MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferfi;
if (!clearNamedFramebufferfi) {
MGLOG_E("glClearNamedFramebufferfi skipped: backend does not implement explicit framebuffer clear.");
MGLOG_E_ONCE("glClearNamedFramebufferfi skipped: backend does not implement explicit framebuffer clear.");
return;
}
clearNamedFramebufferfi(framebuffer, buffer, drawbuffer, depth, stencil);
@@ -578,7 +578,7 @@ namespace MobileGL::MG_Impl::GLImpl {
GLenum buffer, GLint drawbuffer, const GLint* value) {
auto clearNamedFramebufferiv = MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferiv;
if (!clearNamedFramebufferiv) {
MGLOG_E("glClearNamedFramebufferiv skipped: backend does not implement explicit framebuffer clear.");
MGLOG_E_ONCE("glClearNamedFramebufferiv skipped: backend does not implement explicit framebuffer clear.");
return;
}
clearNamedFramebufferiv(framebuffer, buffer, drawbuffer, value);
@@ -588,7 +588,7 @@ namespace MobileGL::MG_Impl::GLImpl {
GLenum buffer, GLint drawbuffer, const GLuint* value) {
auto clearNamedFramebufferuiv = MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferuiv;
if (!clearNamedFramebufferuiv) {
MGLOG_E("glClearNamedFramebufferuiv skipped: backend does not implement explicit framebuffer clear.");
MGLOG_E_ONCE("glClearNamedFramebufferuiv skipped: backend does not implement explicit framebuffer clear.");
return;
}
clearNamedFramebufferuiv(framebuffer, buffer, drawbuffer, value);
@@ -620,6 +620,34 @@ namespace MobileGL::MG_Impl::GLImpl {
return std::max(MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxSamples, 1);
}
// GL_MAX_SAMPLES is the ceiling over all formats; an integer format has its own, lower
// one (GL_MAX_INTEGER_SAMPLES) and GL 4.6 core 9.2.4 makes exceeding it INVALID_OPERATION.
// The multisample TEXTURE path already resolves the limit per format
// (GL_Texture.cpp, GetMaxTextureSamplesForFormat); renderbuffers only ever compared
// against GL_MAX_SAMPLES, so on a driver where the two differ - Adreno reports
// GL_MAX_SAMPLES 4 and GL_MAX_INTEGER_SAMPLES 1 - an integer renderbuffer accepted a
// sample count the format cannot deliver, and said GL_NO_ERROR about it.
Int GetMaxRenderbufferSamplesForFormat_State(TextureInternalFormat format) {
if (MG_Backend::pActiveBackendObject == nullptr) {
return std::numeric_limits<Int>::max();
}
const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
GLenum normalizedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(format);
GLenum normalizedFormat = GL_RGBA;
GLenum normalizedType = GL_UNSIGNED_BYTE;
MG_Util::TextureFormatProcessor::NormalizePixelFormat(normalizedInternalFormat,
PixelFormatNormalizeOptionBit::None,
&normalizedInternalFormat, &normalizedFormat,
&normalizedType);
const Bool isIntegerFormat = normalizedFormat == GL_RED_INTEGER || normalizedFormat == GL_RG_INTEGER ||
normalizedFormat == GL_RGB_INTEGER || normalizedFormat == GL_RGBA_INTEGER;
if (!isIntegerFormat) {
return GetMaxRenderbufferSamples_State();
}
return std::max(dynamicParameters.MaxIntegerSamples, 1);
}
Bool ValidateRenderbufferStorageSize_State(GLsizei width, GLsizei height, const char* caller) {
if (width < 0 || height < 0) {
MG_State::pGLContext->RecordError(
@@ -641,7 +669,7 @@ namespace MobileGL::MG_Impl::GLImpl {
return true;
}
Bool ValidateRenderbufferStorageSamples_State(GLsizei samples, const char* caller) {
Bool ValidateRenderbufferStorageSamples_State(GLsizei samples, TextureInternalFormat format, const char* caller) {
if (samples < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
@@ -649,9 +677,10 @@ namespace MobileGL::MG_Impl::GLImpl {
return false;
}
const Int maxSamples = GetMaxRenderbufferSamples_State();
// TODO: Resolve the remaining per-internalformat renderbuffer sample limits once
// glGetInternalformativ is backed; integer formats are handled below.
const Int maxSamples = GetMaxRenderbufferSamplesForFormat_State(format);
if (samples > maxSamples) {
// TODO: Use per-internalformat renderbuffer sample limits once glGetInternalformativ is backed.
// GL 4.6 core 9.2.4 makes asking for more samples than the format supports
// INVALID_OPERATION, not INVALID_VALUE - the count is well formed, this format just
// cannot deliver it. Only a negative count is INVALID_VALUE.
@@ -659,7 +688,7 @@ namespace MobileGL::MG_Impl::GLImpl {
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", caller,
std::format("Sample count {} exceeds GL_MAX_SAMPLES ({}).", samples, maxSamples)));
std::format("Sample count {} exceeds this format's sample limit ({}).", samples, maxSamples)));
return false;
}
return true;
@@ -684,7 +713,7 @@ namespace MobileGL::MG_Impl::GLImpl {
TextureInternalFormat format = MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);
if (!TextureImpl::ValidateTextureInternalFormat(format)) return;
if (!ValidateRenderbufferStorageSamples_State(samples, kCaller)) return;
if (!ValidateRenderbufferStorageSamples_State(samples, format, kCaller)) return;
if (!ValidateRenderbufferStorageSize_State(width, height, kCaller)) return;
renderbufferObject->AllocateStorage({width, height});
@@ -931,7 +960,8 @@ namespace MobileGL::MG_Impl::GLImpl {
TextureInternalFormat format = MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);
if (!TextureImpl::ValidateTextureInternalFormat(format)) return;
if (!ValidateRenderbufferStorageSamples_State(samples, "NamedRenderbufferStorageMultisample_State")) return;
if (!ValidateRenderbufferStorageSamples_State(samples, format, "NamedRenderbufferStorageMultisample_State"))
return;
if (!ValidateRenderbufferStorageSize_State(width, height, "NamedRenderbufferStorageMultisample_State")) return;
renderbufferObject->AllocateStorage({width, height});
+70 -17
View File
@@ -51,6 +51,15 @@ namespace MobileGL::MG_Impl::GLImpl {
constexpr GLint kFrontendMaxTessControlAtomicCounters = 0;
constexpr GLint kFrontendMaxTessEvaluationAtomicCounters = 0;
constexpr GLint kFrontendMaxVertexAtomicCounters = 0;
// Zero counters means zero buffers to hold them. These have to be ANSWERED rather than
// left to the default INVALID_ENUM: a well-behaved application queries the limit exactly
// to find out that the stage cannot do this, and an error instead both leaves its output
// untouched (so it reads uninitialised memory and may conclude the opposite) and leaves a
// GL error pending that surfaces at whatever unrelated call checks next.
constexpr GLint kFrontendMaxGeometryAtomicCounterBuffers = 0;
constexpr GLint kFrontendMaxTessControlAtomicCounterBuffers = 0;
constexpr GLint kFrontendMaxTessEvaluationAtomicCounterBuffers = 0;
constexpr GLint kFrontendMaxVertexAtomicCounterBuffers = 0;
// One atomic counter is a uint, and a buffer never has to hold more counters than the
// combined limit the frontend advertises. GL 4.6 table 23.63 floors this at 32 bytes.
constexpr GLint kFrontendMaxAtomicCounterBufferSize =
@@ -174,6 +183,30 @@ namespace MobileGL::MG_Impl::GLImpl {
return frontendCount;
}
// A per-stage or combined BLOCK count is an amount of indexed binding points an
// application will occupy, and GL 4.6 table 23.64 orders the two accordingly:
// MAX_UNIFORM_BUFFER_BINDINGS >= MAX_COMBINED_UNIFORM_BLOCKS >= every per-stage count,
// and the same for the shader-storage family. The two families are answered from
// unrelated places here - frontend constants, backend dynamic parameters, and a few
// hard-coded TODOs - so nothing kept them ordered, and a backend that reports Vulkan
// descriptor-indexing counts advertised 256 compute uniform blocks over 36 binding
// points. KHR-GL44.multi_bind.dispatch_bind_buffers_base reads the block count and binds
// that many buffers in ONE glBindBuffersBase, which is then INVALID_OPERATION before it
// binds anything. Clamping is the only direction available: the binding count is the
// capacity of the state layer's indexed-binding array, not a number we may inflate.
GLint ClampBlockCountToBindingPoints(GLint blockCount, BufferTarget bufferTarget) {
const GLint bindingPoints = static_cast<GLint>(GetIndexedBufferQueryPointCount(bufferTarget));
return std::min(std::max(blockCount, 0), bindingPoints);
}
GLint ClampUniformBlockCount(GLint blockCount) {
return ClampBlockCountToBindingPoints(blockCount, BufferTarget::Uniform);
}
GLint ClampStorageBlockCount(GLint blockCount) {
return ClampBlockCountToBindingPoints(blockCount, BufferTarget::ShaderStorage);
}
bool TryDecodeDrawBufferQuery(GLenum pname, SizeT& drawBufferIndex) {
if (pname == GL_DRAW_BUFFER) {
drawBufferIndex = 0;
@@ -350,7 +383,7 @@ namespace MobileGL::MG_Impl::GLImpl {
MGLOG_D("glGetString, name: %s", MG_Util::ConvertGLEnumToString(name).c_str());
if (!activeBackendObject) {
MGLOG_E("activeBackendObject is not initialized!");
MGLOG_E_ONCE("activeBackendObject is not initialized!");
return (GLubyte*)"Unknown";
}
@@ -409,7 +442,7 @@ namespace MobileGL::MG_Impl::GLImpl {
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
if (!activeBackendObject) {
MGLOG_E("activeBackendObject is not initialized!");
MGLOG_E_ONCE("activeBackendObject is not initialized!");
return (GLubyte*)"Unknown";
}
const auto& rendererInfo = activeBackendObject->GetRendererInfo();
@@ -1397,7 +1430,7 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = kFrontendMaxCombinedAtomicCounters;
return;
case GL_MAX_COMBINED_UNIFORM_BLOCKS:
*params = kFrontendMaxCombinedUniformBlocks;
*params = ClampUniformBlockCount(kFrontendMaxCombinedUniformBlocks);
return;
case GL_MAX_DUAL_SOURCE_DRAW_BUFFERS:
*params = 1; // TODO
@@ -1412,7 +1445,7 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = kFrontendMaxFragmentAtomicCounters;
return;
case GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS:
*params = 16; // TODO
*params = ClampStorageBlockCount(16); // TODO
return;
case GL_MAX_FRAGMENT_INPUT_COMPONENTS:
*params = kFrontendMaxFragmentInputComponents;
@@ -1429,13 +1462,16 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = kFrontendMaxFragmentUniformVectors;
return;
case GL_MAX_FRAGMENT_UNIFORM_BLOCKS:
*params = kFrontendMaxFragmentUniformBlocks;
*params = ClampUniformBlockCount(kFrontendMaxFragmentUniformBlocks);
return;
case GL_MAX_GEOMETRY_ATOMIC_COUNTERS:
*params = kFrontendMaxGeometryAtomicCounters;
return;
case GL_MAX_GEOMETRY_ATOMIC_COUNTER_BUFFERS:
*params = kFrontendMaxGeometryAtomicCounterBuffers;
return;
case GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS:
*params = 16; // TODO
*params = ClampStorageBlockCount(16); // TODO
return;
case GL_MAX_GEOMETRY_INPUT_COMPONENTS:
*params = kFrontendMaxGeometryInputComponents;
@@ -1458,7 +1494,7 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = kFrontendMaxGeometryTotalOutputComponents;
return;
case GL_MAX_GEOMETRY_UNIFORM_BLOCKS:
*params = kFrontendMaxGeometryUniformBlocks;
*params = ClampUniformBlockCount(kFrontendMaxGeometryUniformBlocks);
return;
case GL_MAX_GEOMETRY_UNIFORM_COMPONENTS:
*params = kFrontendMaxGeometryUniformComponents;
@@ -1490,9 +1526,15 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_MAX_TESS_CONTROL_ATOMIC_COUNTERS:
*params = kFrontendMaxTessControlAtomicCounters;
return;
case GL_MAX_TESS_CONTROL_ATOMIC_COUNTER_BUFFERS:
*params = kFrontendMaxTessControlAtomicCounterBuffers;
return;
case GL_MAX_TESS_EVALUATION_ATOMIC_COUNTERS:
*params = kFrontendMaxTessEvaluationAtomicCounters;
return;
case GL_MAX_TESS_EVALUATION_ATOMIC_COUNTER_BUFFERS:
*params = kFrontendMaxTessEvaluationAtomicCounterBuffers;
return;
case GL_MAX_TESS_CONTROL_IMAGE_UNIFORMS:
*params = 0;
return;
@@ -1500,10 +1542,10 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = 0;
return;
case GL_MAX_TESS_CONTROL_SHADER_STORAGE_BLOCKS:
*params = 16; // TODO
*params = ClampStorageBlockCount(16); // TODO
return;
case GL_MAX_TESS_EVALUATION_SHADER_STORAGE_BLOCKS:
*params = 16; // TODO
*params = ClampStorageBlockCount(16); // TODO
return;
case GL_MAX_TEXTURE_LOD_BIAS:
*params = 15; // TODO
@@ -1520,13 +1562,16 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_MAX_VERTEX_ATOMIC_COUNTERS:
*params = kFrontendMaxVertexAtomicCounters;
return;
case GL_MAX_VERTEX_ATOMIC_COUNTER_BUFFERS:
*params = kFrontendMaxVertexAtomicCounterBuffers;
return;
case GL_MAX_VERTEX_IMAGE_UNIFORMS:
*params = MG_Backend::pActiveBackendObject
? MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxVertexImageUniforms
: MG_Backend::DynamicBackendParameters{}.MaxVertexImageUniforms;
return;
case GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS:
*params = 16; // TODO
*params = ClampStorageBlockCount(16); // TODO
return;
case GL_MAX_VERTEX_UNIFORM_COMPONENTS:
*params = kFrontendMaxVertexUniformComponents;
@@ -1538,7 +1583,7 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = kFrontendMaxVertexOutputComponents;
return;
case GL_MAX_VERTEX_UNIFORM_BLOCKS:
*params = kFrontendMaxVertexUniformBlocks;
*params = ClampUniformBlockCount(kFrontendMaxVertexUniformBlocks);
return;
case GL_NUM_COMPRESSED_TEXTURE_FORMATS:
*params = 0; // compressed texture upload entrypoints are still unimplemented
@@ -1909,7 +1954,7 @@ namespace MobileGL::MG_Impl::GLImpl {
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
if (!activeBackendObject) {
MGLOG_E("activeBackendObject is not initialized!");
MGLOG_E_ONCE("activeBackendObject is not initialized!");
return;
}
const auto& rendererInfo = activeBackendObject->GetRendererInfo();
@@ -1938,13 +1983,13 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = dynamicParameters.SubgroupQuadOperationsInAllStages ? GL_TRUE : GL_FALSE;
break;
case GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS:
*params = dynamicParameters.MaxComputeShaderStorageBlocks;
*params = ClampStorageBlockCount(dynamicParameters.MaxComputeShaderStorageBlocks);
break;
case GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS:
*params = dynamicParameters.MaxCombinedShaderStorageBlocks;
*params = ClampStorageBlockCount(dynamicParameters.MaxCombinedShaderStorageBlocks);
break;
case GL_MAX_COMPUTE_UNIFORM_BLOCKS:
*params = dynamicParameters.MaxComputeUniformBlocks;
*params = ClampUniformBlockCount(dynamicParameters.MaxComputeUniformBlocks);
break;
case GL_MAX_COMPUTE_TEXTURE_IMAGE_UNITS:
*params = dynamicParameters.MaxComputeTextureImageUnits;
@@ -2074,7 +2119,15 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = static_cast<GLint>(GetIndexedBufferQueryPointCount(BufferTarget::AtomicCounter));
break;
case GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE:
*params = kFrontendMaxAtomicCounterBufferSize;
// The conformance suite splits this evenly across every advertised binding point and
// binds all of them in one glBindBuffersRange
// (KHR-GL44.multi_bind.functional_bind_buffers_range), so the pair has to divide:
// 32 bytes over 36 binding points is a zero-sized range, which BindBufferRange
// rejects with INVALID_VALUE before it binds anything. Floor the advertised size at
// one counter per binding point.
*params = std::max<GLint>(
kFrontendMaxAtomicCounterBufferSize,
static_cast<GLint>(GetIndexedBufferQueryPointCount(BufferTarget::AtomicCounter) * sizeof(GLuint)));
break;
case GL_MAX_TEXTURE_BUFFER_SIZE:
*params = dynamicParameters.MaxTextureBufferSize;
@@ -2195,7 +2248,7 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = static_cast<GLint>(std::lround(dynamicParameters.MaxTextureMaxAnisotropy));
break;
default:
MGLOG_E("glGetIntegerv: Invalid enum %s (0x%X)", MG_Util::ConvertGLEnumToString(pname).c_str(), pname);
MGLOG_D("glGetIntegerv: Invalid enum %s (0x%X)", MG_Util::ConvertGLEnumToString(pname).c_str(), pname);
MG_State::pGLContext->RecordError(ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetIntegerv",
std::format("Invalid enum: 0x{:X}", pname)));
+98 -63
View File
@@ -850,7 +850,12 @@ namespace MobileGL::MG_Impl::GLImpl {
// vector per column - while the value glGetUniform* must return is tightly packed
// columns * rows floats. Only mat4 is the same either way; every other shape needs the
// padding undone, and the readback has to undo exactly what UniformMatrixfv_Object put
// there. Returns false when `ttype` is not a float matrix (nothing to unpack).
// there. Returns false when there is nothing here to unpack.
//
// A DOUBLE matrix is declined not because it is laid out differently - it is not, the
// demotion makes a dmat4 a mat4 in the shader and a mat4-shaped slot here - but because it
// is ROUTED differently: the caller's component-by-component EbtDouble branch has to widen
// each float back to the queried type, and it undoes the same padding itself.
Bool TryGatherFloatMatrixColumns(const glslang::TType* ttype, const char* pBase, void* params) {
if (ttype == nullptr || !ttype->isMatrix() || ttype->getBasicType() == glslang::EbtDouble) return false;
const Int columns = ttype->getMatrixCols();
@@ -903,13 +908,19 @@ namespace MobileGL::MG_Impl::GLImpl {
const SizeT span = UniformStorageSpanInBytes(ttype, size);
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
offset + span > programObject->GetUBOSize()) {
MGLOG_E("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__,
MGLOG_E_ONCE("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__,
program, location);
return;
}
if (!TryGatherFloatMatrixColumns(ttype, pUBO + offset, params)) {
Memcpy(params, pUBO + offset, size);
// Never more than the uniform actually occupies. `size` is the GL type size,
// which for a `double` uniform is twice its storage - every 64-bit float is
// narrowed before the module reaches a backend, so the slot holds floats. The
// typed entry points (glGetUniformdv and friends) go through
// GetUniformScalar_State, which converts component by component; this raw
// copy has no type to convert with, so it is bounded rather than converted.
Memcpy(params, pUBO + offset, std::min<SizeT>(size, span));
}
}
// TODO: handle 1i variant as texture unit
@@ -951,7 +962,7 @@ namespace MobileGL::MG_Impl::GLImpl {
const SizeT span = UniformStorageSpanInBytes(ttype, size);
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
offset + span > programObject->GetUBOSize()) {
MGLOG_E("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__,
MGLOG_E_ONCE("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__,
program, location);
return;
}
@@ -960,22 +971,27 @@ namespace MobileGL::MG_Impl::GLImpl {
if (TryGatherFloatMatrixColumns(ttype, pUBO + offset, params)) return;
}
// A double-precision uniform is the one case where the stored component type can
// differ from the queried one for a non-opaque uniform, and the difference is not
// just a reinterpretation: it is twice as wide, so a raw copy would overrun the
// caller's buffer as well as return nonsense. Read component by component and let
// GL's conversion rules (7.6: round to nearest for the integer queries) apply.
// A double-precision uniform is the one case where the stored component type differs
// from the DECLARED one for a non-opaque uniform: the shader's 64-bit floats are
// narrowed to 32 bits before the module reaches a backend
// (ShaderTranspiler::DemoteFloat64Pass), so what is in the global UBO is a float per
// component, laid out exactly like the float-typed twin of this uniform - std140
// 16-byte column stride for a matrix included. Reading it as a GLdouble would return
// two components reinterpreted as one. Read component by component and let GL's
// conversion rules (7.6: round to nearest for the integer queries) apply; the value
// widens back to the queried type, having lost precision at the glUniform*d that
// stored it and not here.
if (ttype->getBasicType() == glslang::EbtDouble) {
const Int columns = ttype->isMatrix() ? ttype->getMatrixCols() : 1;
const Int rows = ttype->isMatrix() ? ttype->getMatrixRows()
: (ttype->isVector() ? ttype->getVectorSize() : 1);
// The slot the linker handed out is exactly `columns` columns wide, so it also
// states the column stride - which for a double matrix is not a float's 16 bytes.
const SizeT columnStride = columns > 0 ? size / static_cast<SizeT>(columns) : size;
// std140 gives every matrix column its own 16-byte slot; a non-matrix is one
// tightly packed run and never reaches the stride at all.
const SizeT columnStride = 4 * sizeof(GLfloat);
for (Int column = 0; column < columns; ++column) {
for (Int row = 0; row < rows; ++row) {
GLdouble component = 0.0;
Memcpy(&component, pUBO + offset + column * columnStride + row * sizeof(GLdouble),
GLfloat component = 0.0f;
Memcpy(&component, pUBO + offset + column * columnStride + row * sizeof(GLfloat),
sizeof(component));
if constexpr (std::is_integral_v<T>) {
// Rounded to the nearest integer and clamped into the queried type's
@@ -1046,7 +1062,7 @@ namespace MobileGL::MG_Impl::GLImpl {
if (!initialized) {
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
if (!activeBackendObject) {
MGLOG_E("activeBackendObject is not initialized!");
MGLOG_E_ONCE("activeBackendObject is not initialized!");
return;
}
const auto& rendererInfo = activeBackendObject->GetRendererInfo();
@@ -1136,7 +1152,7 @@ namespace MobileGL::MG_Impl::GLImpl {
SizeT writeSize = ItemCount * sizeof(T);
if (size < writeSize) {
// Metadata bug: degrade to a clamped copy instead of killing the process.
MGLOG_E("%s: uniform size mismatch at program %u location %u: expected at least %zu bytes, got %zu "
MGLOG_E_ONCE("%s: uniform size mismatch at program %u location %u: expected at least %zu bytes, got %zu "
"bytes; clamping",
__func__, programObject.GetExternalIndex(), location, ItemCount * sizeof(T), size);
writeSize = size;
@@ -1157,7 +1173,7 @@ namespace MobileGL::MG_Impl::GLImpl {
offset + byteOffsetInsideUniform + writeSize > uboSize) {
// Should not happen: linking gives every settable uniform backing
// storage. Log and drop the write instead of faulting.
MGLOG_E("%s: uniform at program %u location %u has no backing storage (ubo=%p offset=%u size=%zu "
MGLOG_E_ONCE("%s: uniform at program %u location %u has no backing storage (ubo=%p offset=%u size=%zu "
"uboSize=%zu); dropping write",
__func__, programObject.GetExternalIndex(), location, static_cast<void*>(pUBO), offset,
writeSize, uboSize);
@@ -1248,36 +1264,39 @@ namespace MobileGL::MG_Impl::GLImpl {
}
}
// glUniform*d / glUniformMatrix*dv. The vector forms need nothing beyond the shared
// upload template - it is already typed on the component - but a matrix does: the
// column stride the linker used for a double matrix is not the 16 bytes a float one
// gets. It is not guessed here; the slot the uniform was given is exactly `columns`
// columns wide, so dividing states the stride the rest of the pipeline agreed on.
template <typename Program>
void UniformMatrixdv_Object(Program& programObject, GLint location, GLsizei count, GLboolean transpose,
const GLdouble* value, Int columns, Int rows) {
const SizeT slotSize = programObject.GetUniformSizesInBytes(location);
const SizeT columnStride = columns > 0 ? slotSize / static_cast<SizeT>(columns) : slotSize;
const SizeT componentCount = static_cast<SizeT>(columns) * static_cast<SizeT>(rows);
Vector<GLdouble> column(static_cast<SizeT>(rows));
for (GLint matrix = 0; matrix < count; ++matrix) {
if (matrix > 0 && !programObject.UniformLocationsAliasSameUniform(location, location + matrix)) break;
if (!programObject.IsValidUniformLocation(location + matrix)) {
RecordInvalidUniformLocationError(__func__, location + matrix, "the current program object");
return;
}
const GLdouble* source = value + matrix * componentCount;
for (Int c = 0; c < columns; ++c) {
for (Int r = 0; r < rows; ++r) {
column[r] = transpose == GL_TRUE ? source[r * columns + c] : source[c * rows + r];
}
Uniform_State<1>(programObject, location + matrix, column.data(), c * columnStride);
for (Int r = 1; r < rows; ++r) {
Uniform_State<1>(programObject, location + matrix, column.data() + r,
c * columnStride + r * sizeof(GLdouble));
}
}
// glUniform*d / glUniformMatrix*dv. Neither needs a layout of its own any more: the
// transpile chain narrows every 64-bit float in the shader to 32 bits
// (ShaderTranspiler::DemoteFloat64Pass) and the global UBO is laid out by reflecting that
// demoted module, so a double uniform's storage IS a float uniform's - same offset, same
// 4-byte components, same std140 column padding for matrices. Narrowing here, at the one
// place the 64-bit value enters, and then handing the bytes to the ordinary float upload
// path is what keeps the two in step; a separate double-shaped layout here would write
// 8-byte components into 4-byte slots and silently address the wrong ones.
//
// The narrowing is the same static_cast the shader's own arithmetic now performs, so the
// value the shader reads is the value glUniform*d was given, at float precision.
template <GLsizei ItemCount>
void UniformvNarrowed_State(GLint location, GLsizei count, const GLdouble* value) {
if (value == nullptr || count <= 0) {
// Same shape as the float entry points: the location validation still runs, and a
// null pointer is left to fault exactly where glUniform*fv would.
Uniformv_State<ItemCount>(location, count, reinterpret_cast<const GLfloat*>(value));
return;
}
Vector<GLfloat> narrowed(static_cast<SizeT>(count) * ItemCount);
for (SizeT i = 0; i < narrowed.size(); ++i) narrowed[i] = static_cast<GLfloat>(value[i]);
Uniformv_State<ItemCount>(location, count, narrowed.data());
}
template <GLsizei ItemCount>
void ProgramUniformvNarrowed_State(GLuint program, GLint location, GLsizei count, const GLdouble* value) {
if (value == nullptr || count <= 0) {
ProgramUniformv_State<ItemCount>(program, location, count, reinterpret_cast<const GLfloat*>(value));
return;
}
Vector<GLfloat> narrowed(static_cast<SizeT>(count) * ItemCount);
for (SizeT i = 0; i < narrowed.size(); ++i) narrowed[i] = static_cast<GLfloat>(value[i]);
ProgramUniformv_State<ItemCount>(program, location, count, narrowed.data());
}
// glUniformMatrix*fv / glProgramUniformMatrix*fv, every shape (square and non-square).
@@ -1326,6 +1345,22 @@ namespace MobileGL::MG_Impl::GLImpl {
}
}
// glUniformMatrix*dv / glProgramUniformMatrix*dv. Narrowed to the float form and handed
// straight to it: after DemoteFloat64Pass a `dmat4` uniform is a `mat4` in the shader and a
// mat4-shaped slot in the global UBO, columns padded to a vec4 and all. Everything else
// about the call - transpose handling, the array-element walk, the opaque-uniform refusal -
// is then the one implementation both spellings share.
template <typename Program>
void UniformMatrixdv_Object(Program& programObject, GLint location, GLsizei count, GLboolean transpose,
const GLdouble* value, Int columns, Int rows) {
if (value == nullptr || count <= 0) return;
const SizeT componentCount = static_cast<SizeT>(columns) * static_cast<SizeT>(rows);
Vector<GLfloat> narrowed(static_cast<SizeT>(count) * componentCount);
for (SizeT i = 0; i < narrowed.size(); ++i) narrowed[i] = static_cast<GLfloat>(value[i]);
UniformMatrixfv_Object(programObject, "glUniformMatrixdv", location, count, transpose, narrowed.data(),
columns, rows, "the current program object");
}
// Helper function to transpose a 2x2 matrix
void TransposeMatrix2x2(const GLfloat* input, GLfloat* output) {
// Input matrix is in column-major order (OpenGL default)
@@ -1772,7 +1807,7 @@ namespace MobileGL::MG_Impl::GLImpl {
break;
}
default:
MGLOG_E("%s: unknown pname = %p %s", __func__, pname, MG_Util::ConvertGLEnumToString(pname).c_str());
MGLOG_D("%s: unknown pname = %p %s", __func__, pname, MG_Util::ConvertGLEnumToString(pname).c_str());
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
@@ -2089,71 +2124,71 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void Uniform1d(GLint location, GLdouble v0) {
const GLdouble v[] = {v0};
Uniformv_State<1>(location, 1, v);
UniformvNarrowed_State<1>(location, 1, v);
}
void Uniform1dv(GLint location, GLsizei count, const GLdouble* value) {
Uniformv_State<1>(location, count, value);
UniformvNarrowed_State<1>(location, count, value);
}
void ProgramUniform1d(GLuint program, GLint location, GLdouble v0) {
const GLdouble v[] = {v0};
ProgramUniformv_State<1>(program, location, 1, v);
ProgramUniformvNarrowed_State<1>(program, location, 1, v);
}
void ProgramUniform1dv(GLuint program, GLint location, GLsizei count, const GLdouble* value) {
ProgramUniformv_State<1>(program, location, count, value);
ProgramUniformvNarrowed_State<1>(program, location, count, value);
}
void Uniform2d(GLint location, GLdouble v0, GLdouble v1) {
const GLdouble v[] = {v0, v1};
Uniformv_State<2>(location, 1, v);
UniformvNarrowed_State<2>(location, 1, v);
}
void Uniform2dv(GLint location, GLsizei count, const GLdouble* value) {
Uniformv_State<2>(location, count, value);
UniformvNarrowed_State<2>(location, count, value);
}
void ProgramUniform2d(GLuint program, GLint location, GLdouble v0, GLdouble v1) {
const GLdouble v[] = {v0, v1};
ProgramUniformv_State<2>(program, location, 1, v);
ProgramUniformvNarrowed_State<2>(program, location, 1, v);
}
void ProgramUniform2dv(GLuint program, GLint location, GLsizei count, const GLdouble* value) {
ProgramUniformv_State<2>(program, location, count, value);
ProgramUniformvNarrowed_State<2>(program, location, count, value);
}
void Uniform3d(GLint location, GLdouble v0, GLdouble v1, GLdouble v2) {
const GLdouble v[] = {v0, v1, v2};
Uniformv_State<3>(location, 1, v);
UniformvNarrowed_State<3>(location, 1, v);
}
void Uniform3dv(GLint location, GLsizei count, const GLdouble* value) {
Uniformv_State<3>(location, count, value);
UniformvNarrowed_State<3>(location, count, value);
}
void ProgramUniform3d(GLuint program, GLint location, GLdouble v0, GLdouble v1, GLdouble v2) {
const GLdouble v[] = {v0, v1, v2};
ProgramUniformv_State<3>(program, location, 1, v);
ProgramUniformvNarrowed_State<3>(program, location, 1, v);
}
void ProgramUniform3dv(GLuint program, GLint location, GLsizei count, const GLdouble* value) {
ProgramUniformv_State<3>(program, location, count, value);
ProgramUniformvNarrowed_State<3>(program, location, count, value);
}
void Uniform4d(GLint location, GLdouble v0, GLdouble v1, GLdouble v2, GLdouble v3) {
const GLdouble v[] = {v0, v1, v2, v3};
Uniformv_State<4>(location, 1, v);
UniformvNarrowed_State<4>(location, 1, v);
}
void Uniform4dv(GLint location, GLsizei count, const GLdouble* value) {
Uniformv_State<4>(location, count, value);
UniformvNarrowed_State<4>(location, count, value);
}
void ProgramUniform4d(GLuint program, GLint location, GLdouble v0, GLdouble v1, GLdouble v2, GLdouble v3) {
const GLdouble v[] = {v0, v1, v2, v3};
ProgramUniformv_State<4>(program, location, 1, v);
ProgramUniformvNarrowed_State<4>(program, location, 1, v);
}
void ProgramUniform4dv(GLuint program, GLint location, GLsizei count, const GLdouble* value) {
ProgramUniformv_State<4>(program, location, count, value);
ProgramUniformvNarrowed_State<4>(program, location, count, value);
}
void UniformMatrix2dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
if (location == -1) return;
@@ -210,11 +210,66 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
// ---- model construction --------------------------------------------------------
// GL_REFERENCED_BY_*_SHADER for an ARRAYED block instance, refined per element.
//
// glslang records a block reference by walking up to the base symbol and calling
// addBlockName with the whole ARRAY type, which ORs the referencing stage into every
// element at once - it has not resolved the subscript yet at that point. So reading
// "e[0].b" marks both TrickyBlock[0] and TrickyBlock[1] as referenced by the fragment
// stage (KHR-GL43.program_interface_query.uniform-block-types).
//
// The MEMBER masks are exact: EShReflectionAllBlockVariables enumerates every member of
// every element with the stage mask suppressed, and only the dereference chain actually
// walked turns a bit on - and that chain carries the subscript. So the union of a block
// instance's members is the reference set of that instance.
//
// Applied ONLY to arrayed instances, because for a scalar block glslang is already exact.
// Note the union is used even when it is empty: an array element nobody dereferenced has
// no member bits and is genuinely referenced by nobody, which is the whole point - falling
// back to the block's own mask there would restore the over-approximation.
Vector<Uint32> BuildBlockStagesFromMembers(const glslang::TProgram& reflection, Int blockCount) {
auto& mutableReflection = const_cast<glslang::TProgram&>(reflection);
Vector<Uint32> stagesByBlock(static_cast<SizeT>(blockCount < 0 ? 0 : blockCount), 0u);
const Int uniformCount = mutableReflection.getNumUniformVariables();
for (Int index = 0; index < uniformCount; ++index) {
const auto& uniform = mutableReflection.getUniform(index);
const Int owner = uniform.index;
if (owner < 0 || owner >= blockCount) continue;
stagesByBlock[static_cast<SizeT>(owner)] |= static_cast<Uint32>(uniform.stages);
}
return stagesByBlock;
}
// UNIFORM blocks only, and that scope is load-bearing rather than cautious. The member
// names glslang produces for a uniform block array carry the subscript
// ("TrickyBlock[0].b", via EShReflectionStrictArraySuffix), so each element's members are
// distinct entries and the bits land on the right one. A SHADER STORAGE block array does
// NOT get that treatment - its buffer variables reflect under one subscript-free spelling
// shared by every element - so a union over them credits element 0 and starves the rest.
// KHR-GL43.program_interface_query.ssb-types is the case that says so: it reads ss[0] and
// ss[1] and requires both to report the fragment stage, which only glslang's own
// (deliberately over-approximating) block mask gets right. Storage and atomic-counter
// blocks therefore keep that mask untouched.
Uint32 UniformBlockStages(const glslang::TObjectReflection& block, const Vector<Uint32>& stagesFromMembers,
Int tIndex) {
String arrayBase;
Uint element = 0;
Bool malformed = false;
if (!SplitTrailingSubscript(block.name, arrayBase, element, malformed) || malformed) {
return static_cast<Uint32>(block.stages);
}
if (tIndex < 0 || tIndex >= static_cast<Int>(stagesFromMembers.size())) {
return static_cast<Uint32>(block.stages);
}
return stagesFromMembers[static_cast<SizeT>(tIndex)];
}
void BuildBlocks(ProgramObject& program, const glslang::TProgram& reflection, Model& model,
Vector<BlockKind>& blockKind, Vector<Int>& blockInterfaceIndex) {
const Int blockCount = const_cast<glslang::TProgram&>(reflection).getNumUniformBlocks();
blockKind.assign(blockCount, BlockKind::Uniform);
blockInterfaceIndex.assign(blockCount, -1);
const Vector<Uint32> stagesFromMembers = BuildBlockStagesFromMembers(reflection, blockCount);
for (Int tIndex = 0; tIndex < blockCount; ++tIndex) {
const auto& block = const_cast<glslang::TProgram&>(reflection).getUniformBlock(tIndex);
@@ -260,8 +315,8 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
resource.bufferDataSize = static_cast<GLint>(program.GetUBOSizeAt(glIndex));
const Int tIndex = program.TProgramBlockIndex(static_cast<Uint>(glIndex));
if (tIndex >= 0 && tIndex < blockCount) {
resource.stages =
static_cast<Uint32>(const_cast<glslang::TProgram&>(reflection).getUniformBlock(tIndex).stages);
resource.stages = UniformBlockStages(const_cast<glslang::TProgram&>(reflection).getUniformBlock(tIndex),
stagesFromMembers, tIndex);
}
model.uniformBlocks.push_back(Move(resource));
}
@@ -352,6 +407,17 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
}
}
// A built-in interface block that a shader redeclares with fewer members keeps the
// omitted ones in its type when the redeclaration is ANONYMOUS - glslang hides them
// (basic type void) instead of erasing them, because the original shared declaration
// has to stay usable. Only the instance-named form erases. So a separable vertex
// program that redeclares `out gl_PerVertex { vec4 gl_Position; }` still carries
// gl_PointSize and gl_ClipDistance through the block-unwrapping reflection, and they
// are not part of its output interface.
Bool IsHiddenBlockMember(const glslang::TType* type) {
return type != nullptr && type->getBasicType() == glslang::EbtVoid;
}
void BuildStageIO(ProgramObject& program, const glslang::TProgram& reflection, Model& model) {
auto& mutableReflection = const_cast<glslang::TProgram&>(reflection);
@@ -359,6 +425,7 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
for (Int index = 0; index < inputCount; ++index) {
const auto& refl = mutableReflection.getPipeInput(index);
const glslang::TType* type = refl.getType();
if (IsHiddenBlockMember(type)) continue;
Resource resource;
// The Vulkan-semantics parse reflects the vertex builtins under their SPIR-V
// names; GL enumerates the GL spellings.
@@ -373,18 +440,28 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
model.programInputs.push_back(Move(resource));
}
// A color number, and therefore a color INDEX, exists only for a fragment stage's
// outputs. The output interface belongs to the program's last stage, so for a
// separable tessellation/geometry/vertex program these are varyings: asking the
// frag-data maps about them can still answer a location (a tess-control output
// carries its own layout(location=N)), and a location then manufactures a color
// index of 0 where GL requires -1
// (KHR-GL43.program_interface_query.separate-programs-tess-control).
const Bool lastStageIsFragment = mutableReflection.getIntermediate(EShLangFragment) != nullptr;
const Int outputCount = mutableReflection.getNumPipeOutputs();
for (Int index = 0; index < outputCount; ++index) {
const auto& refl = mutableReflection.getPipeOutput(index);
const glslang::TType* type = refl.getType();
if (IsHiddenBlockMember(type)) continue;
Resource resource;
resource.name = WithArraySuffix(refl.name, type);
resource.type = static_cast<GLenum>(refl.glDefineType);
resource.arraySize = ArraySizeOf(type, refl.size);
resource.location = MappedLocation(program.GetFragmentDataLocation(refl.name.c_str()));
if (resource.location < 0) {
// A built-in output (gl_FragDepth, gl_SampleMask) and a non-fragment stage
// output both have no location, and therefore no color index either.
if (resource.location < 0 || !lastStageIsFragment) {
// A built-in output (gl_FragDepth, gl_SampleMask) has no location, and a
// non-fragment stage's outputs have no color number at all - either way there
// is no color index.
resource.locationIndex = -1;
} else {
resource.locationIndex = program.GetFragmentDataIndex(refl.name.c_str());
@@ -9,6 +9,7 @@
#include "GL_Sampler.h"
#include "Validators.h"
#include "../Getter/GL_Getter.h"
#include "../Texture/GL_Texture.h"
#include <MG_State/GLState/Core.h>
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
@@ -269,15 +270,13 @@ namespace MobileGL::MG_Impl::GLImpl {
}
}
// The number of texture units a sampler may be bound to. GL 3.3 core 3.8.2 names
// GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, which is what the backend advertises; the frontend's
// MAX_TEXTURE_IMAGE_UNITS is only the capacity of the unit array, so it is a clamp on the
// answer and never the answer itself - gating on it alone accepts every unit up to 192 no
// matter what the driver reports.
// The number of texture units a sampler may be bound to is the same count a TEXTURE may be
// bound to - GL 3.3 core 3.8.2 names GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS for both - so it is
// computed once, in GetCombinedTextureImageUnitCount, and named here for the sampler-side
// readers below. Two copies of that arithmetic is how glBindSamplers and glBindTextures would
// come to disagree about which units exist.
static GLint GetSamplerBindableTextureUnitCount() {
GLint maxTextureUnits = 0;
GetIntegerv(GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, &maxTextureUnits);
return std::min<GLint>(std::max(maxTextureUnits, 0), MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS);
return GetCombinedTextureImageUnitCount();
}
void BindSampler_State(GLuint unit, GLuint sampler) {
+394 -37
View File
@@ -621,7 +621,7 @@ namespace MobileGL::MG_Impl::GLImpl {
// the process down, which is never an acceptable answer to a query - see the same reasoning
// above for the compressed-format path.
void RecordUnsupportedLevelQueryStorage(const char* caller, GLenum pname) {
MGLOG_I("%s: glGetTexLevelParameter(pname=%s) is not implemented for texture-buffer "
MGLOG_W_ONCE("%s: glGetTexLevelParameter(pname=%s) is not implemented for texture-buffer "
"storage; recording GL_INVALID_OPERATION instead of terminating",
caller, MG_Util::ConvertGLEnumToString(pname).c_str());
MG_State::pGLContext->RecordError(
@@ -870,7 +870,7 @@ namespace MobileGL::MG_Impl::GLImpl {
MG_Util::GetInputBytesPerPixel(MG_Util::ConvertGLEnumToTextureInputFormat(format),
MG_Util::ConvertGLEnumToTexturePixelDataType(type));
if (readBytesPerTexel != bytesPerTexel) {
MGLOG_I("%s: cannot copy into a %zu-byte texel from a %zu-byte readback layout", caller,
MGLOG_W_ONCE("%s: cannot copy into a %zu-byte texel from a %zu-byte readback layout", caller,
bytesPerTexel, readBytesPerTexel);
return false;
}
@@ -1376,6 +1376,47 @@ namespace MobileGL::MG_Impl::GLImpl {
return true;
}
// The same rules for the COMPRESSED entry points, whose payload size is the imageSize the
// caller passed rather than something derived from a (format, type) pair - and which have no
// datum size, so the alignment rule above does not apply to them. Shared by
// glCompressedTexImage2D and glCompressedTexSubImage2D so the two cannot drift; the point
// that is easy to get wrong and that KHR-GL44.buffer_storage.map_persistent_texture exists to
// check is the first one: a PERSISTENT mapping stays a legal transfer source.
Bool ValidateCompressedUnpackBufferSource(const void* data, SizeT imageSize, const char* caller) {
const auto& unpackBuffer =
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelUnpack).GetBoundObject();
if (!unpackBuffer) return true;
if (unpackBuffer->IsMapped() && !(unpackBuffer->GetMappingAccess() & BufferMappingAccessBit::Persistent)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Pixel unpack buffer is currently mapped."));
return false;
}
const SizeT offset = reinterpret_cast<SizeT>(data);
const SizeT bufferSize = unpackBuffer->GetSize();
if (offset > bufferSize || imageSize > bufferSize - offset) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"Unpacking would read past the end of the pixel unpack buffer."));
return false;
}
return true;
}
// Where a compressed upload reads its blocks from: `data` is an offset into the bound unpack
// buffer when there is one, and a client pointer otherwise. Only meaningful once
// ValidateCompressedUnpackBufferSource has passed. Null means there is nothing to read, which
// GL leaves undefined and which callers must not dereference.
const void* CompressedUnpackSource(const void* data) {
const auto& unpackBuffer =
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelUnpack).GetBoundObject();
if (!unpackBuffer) return data;
return reinterpret_cast<const char*>(unpackBuffer->MappedData()) + reinterpret_cast<SizeT>(data);
}
void TexSubImage3D_State(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width,
GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* pixels) {
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
@@ -1449,7 +1490,7 @@ namespace MobileGL::MG_Impl::GLImpl {
if (xoffset + width > static_cast<GLsizei>(texelSize.x()) ||
yoffset + height > static_cast<GLsizei>(texelSize.y()) ||
zoffset + depth > static_cast<GLsizei>(texelSize.z())) {
MGLOG_E("TexSubImage3D_State: Specified region exceeds texture level dimensions");
MGLOG_E_ONCE("TexSubImage3D_State: Specified region exceeds texture level dimensions");
free(processedPixels);
return;
}
@@ -1558,7 +1599,7 @@ namespace MobileGL::MG_Impl::GLImpl {
{width, height, 1}, false, inputSize);
if (!processedPixels || inputSize == 0) {
MGLOG_E("TexSubImage2D_State: Failed to process pixel data for TexSubImage2D, width: %d, height: %d", width,
MGLOG_E_ONCE("TexSubImage2D_State: Failed to process pixel data for TexSubImage2D, width: %d, height: %d", width,
height);
if (processedPixels) free(processedPixels);
return;
@@ -1572,7 +1613,7 @@ namespace MobileGL::MG_Impl::GLImpl {
if (xoffset + width > static_cast<GLsizei>(texelSize.x()) ||
yoffset + height > static_cast<GLsizei>(texelSize.y())) {
MGLOG_E("TexSubImage2D_State: Specified region exceeds texture dimensions");
MGLOG_E_ONCE("TexSubImage2D_State: Specified region exceeds texture dimensions");
free(processedPixels);
return;
}
@@ -2123,7 +2164,7 @@ namespace MobileGL::MG_Impl::GLImpl {
if (processedPixels && imageSize > 0) {
if (imageSize != internalBytes) {
MGLOG_W("%s: Processed pixel data size (%zu) does not match expected size (%zu). "
MGLOG_W_ONCE("%s: Processed pixel data size (%zu) does not match expected size (%zu). "
"This may indicate an alignment or processing issue.",
__func__, imageSize, internalBytes);
}
@@ -2238,6 +2279,23 @@ namespace MobileGL::MG_Impl::GLImpl {
DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level);
textureMipmapObject->AllocateStorage(textureUploadTarget, level,
{{width, height, 1}, internalBytes});
// GL 4.6 core 8.5: a SPECIFIC compressed internalformat (unlike a generic
// GL_COMPRESSED_* one, where the implementation is free to choose) commits the
// level to that format - GL_TEXTURE_COMPRESSED must then answer true for it and
// GL_TEXTURE_INTERNAL_FORMAT must report it, which is how an application asks for
// the size to hand glCompressedTexSubImage2D afterwards. Only the tag and the size
// are recorded: there is no BC/ETC codec here, so the texel shadow keeps the
// uncompressed storage this format resolved to (which is also what lets the level
// sample as the application's texels), and the compressed image the tag describes
// is zero-filled - the one reproducible answer glGetCompressedTexImage can give for
// an image nothing ever compressed. AllocateStorage above clears the tag, so this
// has to follow it.
const auto compressedInfo = MG_Util::GetCompressedFormatInfo(static_cast<GLenum>(internalformat));
if (compressedInfo.blockWidth != 0) {
textureMipmapObject->SetMipmapCompressedImage(
textureUploadTarget, level, static_cast<GLenum>(internalformat), nullptr,
MG_Util::CalculateCompressedTextureImageSize(compressedInfo, {width, height, 1}));
}
}
if (!originalPixels) {
@@ -2252,7 +2310,7 @@ namespace MobileGL::MG_Impl::GLImpl {
if (processedPixels && imageSize > 0) {
if (imageSize != internalBytes) {
MGLOG_W("TexImage2D_State: Processed pixel data size (%zu) does not match expected size (%zu). "
MGLOG_W_ONCE("TexImage2D_State: Processed pixel data size (%zu) does not match expected size (%zu). "
"This may indicate an alignment or processing issue.",
imageSize, internalBytes);
}
@@ -2965,9 +3023,9 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_TEXTURE_INTERNAL_FORMAT:
if (params) {
// A level stored compressed must report the token it was given, not the
// uncompressed format backing it (GL 4.6 core 8.11). Only glCompressedTexImage* sets
// that tag, so every level created by glTexImage*D - including one given a compressed
// internalformat - still answers with its resolved storage format.
// uncompressed format backing it (GL 4.6 core 8.11). glCompressedTexImage2D sets
// that tag, and so does a glTexImage2D given a SPECIFIC compressed internalformat;
// every other level answers with its resolved storage format.
const GLenum compressedFormat = GetCompressedLevelFormat(textureObject, textureUploadTarget, level);
*params = (compressedFormat != GL_NONE)
? (GLint)compressedFormat
@@ -3103,9 +3161,9 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_TEXTURE_INTERNAL_FORMAT:
if (params) {
// A level stored compressed must report the token it was given, not the
// uncompressed format backing it (GL 4.6 core 8.11). Only glCompressedTexImage* sets
// that tag, so every level created by glTexImage*D - including one given a compressed
// internalformat - still answers with its resolved storage format.
// uncompressed format backing it (GL 4.6 core 8.11). glCompressedTexImage2D sets
// that tag, and so does a glTexImage2D given a SPECIFIC compressed internalformat;
// every other level answers with its resolved storage format.
const GLenum compressedFormat = GetCompressedLevelFormat(textureObject, textureUploadTarget, level);
*params = (GLfloat)((compressedFormat != GL_NONE)
? compressedFormat
@@ -3346,6 +3404,17 @@ namespace MobileGL::MG_Impl::GLImpl {
!TextureImpl::ValidateTextureLevelNumber(dstLevel)) {
return false;
}
// ValidateTextureLevelNumber only bounds the index by GL_MAX_TEXTURE_SIZE; it cannot
// see that this particular texture stops at level 0. Both backends turn <level> into an
// image subresource with no further checking (DirectVulkan builds a VkImageCopy from it,
// DirectGLES forwards it to the ES copy), so a level the texture never had reached the
// driver as an out-of-range mip index - on Adreno that is a SIGSEGV inside
// vkCmdCopyImage, which is what KHR-GL43.copy_image.non_existent_mipmap used to do to
// the whole glcts process. The answer the spec asks for is GL_INVALID_VALUE.
if (!TextureImpl::ValidateTextureLevelExists(srcTexture, srcLevel, __func__) ||
!TextureImpl::ValidateTextureLevelExists(dstTexture, dstLevel, __func__)) {
return false;
}
if (srcWidth < 0 || srcHeight < 0 || srcDepth < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
@@ -3468,10 +3537,170 @@ namespace MobileGL::MG_Impl::GLImpl {
RecordUnsupportedCompressedFormat(__func__);
}
// Replaces a block-aligned rectangle of the compressed image glCompressedTexImage2D (or a
// compressed glTexImage2D) shadowed for this level. Same deviation as the image call it
// patches: the uncompressed texel shadow beside it is NOT touched, because there is no
// BC/ETC codec here to decode the incoming blocks with - so what changes is the image
// glGetCompressedTexImage hands back, not what the level samples as. Marking the texels
// dirty would therefore only re-upload bytes that did not change.
void CompressedTexSubImage2D_State(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width,
GLsizei height, GLenum format, GLsizei imageSize, const void* data) {
// TODO: implement compressed upload - see CompressedTexImage2D_State.
RecordUnsupportedCompressedFormat(__func__);
// ======================= Converting ================================
const auto textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
const auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
// Zero block width doubles as "format is not a specific compressed format", the
// INVALID_ENUM case - one lookup answers both questions.
const auto compressedInfo = MG_Util::GetCompressedFormatInfo(format);
// ===================== Error Checking ==============================
if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return;
// A proxy holds no image to modify; only the glTexImage*/glCompressedTexImage* pair
// accepts one.
if (TextureImpl::IsProxyTextureTarget(textureUploadTarget)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"A proxy target has no texture image to modify."));
return;
}
if (!TextureImpl::ValidateTextureLevelNumber(level)) return;
if (!TextureImpl::ValidateTextureLevelWithUploadTarget(textureUploadTarget, level)) return;
if (width < 0 || height < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "width and height must be non-negative."));
return;
}
if (compressedInfo.blockWidth == 0) {
RecordUnsupportedCompressedFormat(__func__);
return;
}
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
auto* textureMipmapObject = MG_State::GLState::AsMipmapTexture(textureObject.get());
if (textureMipmapObject == nullptr) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Texture storage is not mipmap-backed."));
return;
}
// GL 4.6 core 8.7: INVALID_OPERATION unless the image being modified is stored in
// exactly this compressed format. That is also what makes the block arithmetic below
// sound - the level's grid is measured with THIS format's block size.
const GLenum levelFormat =
textureMipmapObject->GetMipmapCompressedFormat(textureUploadTarget, static_cast<Uint>(level));
if (levelFormat != format) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"format does not match the internal format of the texture image."));
return;
}
const IntVec3 levelSize = textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, static_cast<Uint>(level));
// Written as a subtraction rather than `xoffset + width > levelSize.x()`: both operands
// are application-supplied GLints, so the sum is free to overflow, and a signed overflow
// is undefined behaviour that a compiler may resolve by assuming the check passes.
// levelSize is our own and non-negative, and the offsets are known non-negative by the
// time the subtraction runs, so this form cannot wrap.
if (xoffset < 0 || yoffset < 0 || width > levelSize.x() - xoffset || height > levelSize.y() - yoffset) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"The replaced region does not lie within the texture image."));
return;
}
// GL 4.6 core 8.7 for block-based formats: the region must start on a block boundary
// and must either be a whole number of blocks wide/high or run to the image's edge.
const Int blockWidth = static_cast<Int>(compressedInfo.blockWidth);
const Int blockHeight = static_cast<Int>(compressedInfo.blockHeight);
const Bool alignedX = (xoffset % blockWidth == 0) &&
(width % blockWidth == 0 || xoffset + width == levelSize.x());
const Bool alignedY = (yoffset % blockHeight == 0) &&
(height % blockHeight == 0 || yoffset + height == levelSize.y());
if (!alignedX || !alignedY) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"The replaced region is not aligned to the format's compressed blocks."));
return;
}
// Exactly the size the format and dimensions imply, which is also what keeps the copy
// below in bounds.
const SizeT expectedImageSize =
MG_Util::CalculateCompressedTextureImageSize(compressedInfo, {width, height, 1});
if (imageSize < 0 || static_cast<SizeT>(imageSize) != expectedImageSize) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"imageSize does not match the compressed image size."));
return;
}
// ======================= Processing ================================
if (!ValidateCompressedUnpackBufferSource(data, expectedImageSize, __func__)) return;
const void* compressedBytes = CompressedUnpackSource(data);
if (expectedImageSize == 0) return; // a zero-sized region is a legal no-op
if (compressedBytes == nullptr) {
// No unpack buffer and a null client pointer: there is nothing to read. GL leaves
// this undefined rather than erroring, and dereferencing it is the one answer that
// is never acceptable.
MGLOG_D("%s: null data with no pixel unpack buffer bound, nothing to replace", __func__);
return;
}
// Once per process: the call is about to succeed, and what it does is narrower than what
// an application has every right to expect from it. Before this existed the call answered
// GL_INVALID_ENUM, which was wrong but at least visible; a silent success that leaves the
// sampled texels untouched is the kind of thing that costs a day to find from the other
// end. MGLOG_W is the right level and now survives at INFO; it sat at MGLOG_I only
// while the Log.h ordering compiled warnings out of the builds that ship.
static std::atomic<Bool> announcedNoCodec{false};
if (!announcedNoCodec.exchange(true)) {
MGLOG_W("%s: the compressed blocks are stored verbatim and returned by "
"glGetCompressedTexImage, but there is no BC/ETC decoder here, so they do not "
"reach the texels this level SAMPLES as. Upload through glTexSubImage2D for "
"that.",
__func__);
}
// The level's compressed image is stored as one blob, so the rectangle is patched into
// a copy of it and the whole thing handed back. Compressed sub-image uploads are not a
// hot path, and this keeps the storage layer's compressed API to the two calls it has.
const SizeT blobSize =
textureMipmapObject->GetMipmapCompressedByteSize(textureUploadTarget, static_cast<Uint>(level));
const void* existing =
textureMipmapObject->MapMipmapCompressedImage(textureUploadTarget, static_cast<Uint>(level));
if (blobSize == 0 || existing == nullptr) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"The texture level holds no compressed image to modify."));
return;
}
Vector<Uint8> blob(blobSize);
Memcpy(blob.data(), existing, blobSize);
const SizeT blockByteSize = compressedInfo.blockByteSize;
const SizeT levelBlocksX = (static_cast<SizeT>(levelSize.x()) + compressedInfo.blockWidth - 1) /
compressedInfo.blockWidth;
const SizeT levelRowBytes = levelBlocksX * blockByteSize;
const SizeT regionBlocksX = (static_cast<SizeT>(width) + compressedInfo.blockWidth - 1) /
compressedInfo.blockWidth;
const SizeT regionBlocksY = (static_cast<SizeT>(height) + compressedInfo.blockHeight - 1) /
compressedInfo.blockHeight;
const SizeT firstBlockX = static_cast<SizeT>(xoffset) / compressedInfo.blockWidth;
const SizeT firstBlockY = static_cast<SizeT>(yoffset) / compressedInfo.blockHeight;
const SizeT regionRowBytes = regionBlocksX * blockByteSize;
const auto* source = static_cast<const Uint8*>(compressedBytes);
for (SizeT row = 0; row < regionBlocksY; ++row) {
const SizeT destOffset = (firstBlockY + row) * levelRowBytes + firstBlockX * blockByteSize;
if (destOffset + regionRowBytes > blobSize) break; // a level whose blob predates its size
Memcpy(blob.data() + destOffset, source + row * regionRowBytes, regionRowBytes);
}
textureMipmapObject->SetMipmapCompressedImage(textureUploadTarget, static_cast<Uint>(level), format,
blob.data(), blobSize);
}
void CompressedTexSubImage1D_State(GLenum target, GLint level, GLint xoffset, GLsizei width, GLenum format,
@@ -3564,28 +3793,8 @@ namespace MobileGL::MG_Impl::GLImpl {
// SetMipmapCompressedImage re-arms it.
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, height, 1}, internalBytes});
const void* compressedBytes = data;
const auto& pixelUnpackBufferObject =
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelUnpack).GetBoundObject();
if (pixelUnpackBufferObject) {
if (pixelUnpackBufferObject->IsMapped()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"Pixel unpack buffer is currently mapped."));
return;
}
const SizeT offset = reinterpret_cast<SizeT>(data);
const SizeT bufferSize = pixelUnpackBufferObject->GetSize();
if (offset > bufferSize || expectedImageSize > bufferSize - offset) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"Unpacking would read past the end of the pixel unpack buffer."));
return;
}
compressedBytes = reinterpret_cast<const char*>(pixelUnpackBufferObject->MappedData()) + offset;
}
if (!ValidateCompressedUnpackBufferSource(data, expectedImageSize, __func__)) return;
const void* compressedBytes = CompressedUnpackSource(data);
textureMipmapObject->SetMipmapCompressedImage(textureUploadTarget, level, internalformat, compressedBytes,
expectedImageSize);
textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, true);
@@ -4095,6 +4304,13 @@ namespace MobileGL::MG_Impl::GLImpl {
// core 8.19). Allocating only the primary one left the object cube-incomplete, so every
// framebuffer it was attached to reported GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT. Every other
// 2D target has exactly one upload target, so this loop is a no-op change for them.
// A specific compressed internalformat commits every level it allocates to that
// format, the same way glTexImage2D does - and here it matters twice over, because
// immutable storage plus glCompressedTexSubImage2D IS the modern way to upload a
// compressed texture: without the tag that sub-image call finds an uncompressed
// level and refuses it. Zero width means a generic (implementation's choice)
// format, which MobileGL answers with uncompressed storage, so it is not tagged.
const auto compressedInfo = MG_Util::GetCompressedFormatInfo(internalformat);
for (const auto uploadTarget : textureObject->GetUploadTargets()) {
for (GLsizei level = 0; level < levels; ++level) {
const GLsizei levelWidth = std::max<GLsizei>(1, width >> level);
@@ -4103,6 +4319,13 @@ namespace MobileGL::MG_Impl::GLImpl {
static_cast<SizeT>(levelWidth) * static_cast<SizeT>(levelHeight) * bytesPerPixel;
textureMipmapObject->AllocateStorage(uploadTarget, level, {{levelWidth, levelHeight, 1}, byteSize});
textureMipmapObject->MarkStorageDirty(uploadTarget, level, false);
if (compressedInfo.blockWidth != 0) {
// After AllocateStorage, which clears the tag.
textureMipmapObject->SetMipmapCompressedImage(
uploadTarget, static_cast<Uint>(level), internalformat, nullptr,
MG_Util::CalculateCompressedTextureImageSize(compressedInfo,
{levelWidth, levelHeight, 1}));
}
}
// See TextureStorage1D.
textureMipmapObject->TruncateMipmapLevels(uploadTarget, static_cast<Uint>(levels));
@@ -4472,6 +4695,14 @@ namespace MobileGL::MG_Impl::GLImpl {
free(processedPixels);
}
void CompressedTextureSubImage2D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width,
GLsizei height, GLenum format, GLsizei imageSize, const void* data) {
auto textureObject = GetTextureObjectByName(texture, __func__);
WithTemporarilyBoundNamedTexture(textureObject, [&](GLenum target) {
CompressedTexSubImage2D_State(target, level, xoffset, yoffset, width, height, format, imageSize, data);
});
}
void TextureSubImage3D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width,
GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* pixels) {
auto textureObject = GetTextureObjectByName(texture, __func__);
@@ -4549,6 +4780,132 @@ namespace MobileGL::MG_Impl::GLImpl {
MG_State::pGLContext->NoteTextureUnitTouched(static_cast<Int>(unit), changed);
}
GLint GetCombinedTextureImageUnitCount() {
GLint maxTextureUnits = 0;
GetIntegerv(GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, &maxTextureUnits);
return std::min<GLint>(std::max(maxTextureUnits, 0), MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS);
}
namespace {
// ARB_multi_bind checks the whole [first, first + count) range before binding anything and
// reports an overrun as INVALID_OPERATION - not the INVALID_VALUE the single-bind entry
// points report for an out-of-range unit, and not after binding the in-range prefix.
Bool ValidateMultiBindUnitRange(GLuint first, GLsizei count, GLint unitCount, const char* funcName) {
if (count < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName, "count must be non-negative."));
return false;
}
if (static_cast<Uint64>(first) + static_cast<Uint64>(count) > static_cast<Uint64>(unitCount)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
std::format("first + count ({} + {}) exceeds the {} available units.",
first, count, unitCount)));
return false;
}
return true;
}
// ARB_multi_bind states the equivalence to a loop of single binds "except that <textures>
// will not be created if they do not exist": glBindTexture instantiates a name GenTextures
// merely reserved, the multi-bind entry points must refuse it. The error class is
// INVALID_OPERATION for both of them, where the scalar glBindImageTexture reports
// INVALID_VALUE - hence the check here rather than inside BindImageTexture.
//
// Deliberately PER ELEMENT: the extension defines these calls as a loop, so a bad entry
// costs its own unit and leaves the rest of the range bound.
SharedPtr<MG_State::GLState::ITextureObject> ResolveMultiBindTexture(GLuint texture, GLsizei index,
const char* funcName) {
SharedPtr<MG_State::GLState::ITextureObject> textureObject =
MG_State::pGLContext->GetTextureObject(texture);
if (!textureObject) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", funcName,
std::format("textures[{}] ({}) is not the name of an existing texture object.", index,
texture)));
}
return textureObject;
}
// ARB_multi_bind: an element naming texture zero unbinds EVERY target of its unit, i.e.
// rebinds each target's default texture object - the unit's initial state. Same rule
// glBindTextureUnit(unit, 0) follows.
void UnbindAllTargetsOnUnit(Int unit) {
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
Bool changed = false;
for (auto& slot : textureUnit.GetAllBindingSlots()) {
if (slot.Bind(MG_State::pGLContext->GetDefaultTextureObject(slot.GetTarget()))) changed = true;
}
MG_State::pGLContext->NoteTextureUnitTouched(unit, changed);
}
} // namespace
// ARB_multi_bind: glBindTextures binds each texture to ITS OWN target on unit <first> + i, so
// there is no target parameter and no way to express it through glBindTexture - the per-unit,
// by-object form glBindTextureUnit uses is the one that matches. A NULL <textures> unbinds the
// whole range.
void BindTextures(GLuint first, GLsizei count, const GLuint* textures) {
if (!ValidateMultiBindUnitRange(first, count, GetCombinedTextureImageUnitCount(), __func__)) return;
for (GLsizei i = 0; i < count; ++i) {
const GLuint texture = textures ? textures[i] : 0;
const Int unit = static_cast<Int>(first) + i;
if (texture == 0) {
UnbindAllTargetsOnUnit(unit);
continue;
}
const SharedPtr<MG_State::GLState::ITextureObject> textureObject =
ResolveMultiBindTexture(texture, i, __func__);
if (!textureObject) continue;
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
const Bool changed = textureUnit.GetBindingSlot(textureObject->GetTarget()).Bind(textureObject);
MG_State::pGLContext->NoteTextureUnitTouched(unit, changed);
}
}
// ARB_multi_bind: glBindImageTextures is a loop of glBindImageTexture with every parameter but
// the unit and the texture fixed by the spec - level 0, layered, layer 0, READ_WRITE, and the
// texture's own internal format. An element that names texture zero resets the unit.
void BindImageTextures(GLuint first, GLsizei count, const GLuint* textures) {
if (!ValidateMultiBindUnitRange(first, count, static_cast<GLint>(GetAdvertisedImageUnitCount()), __func__)) {
return;
}
for (GLsizei i = 0; i < count; ++i) {
const GLuint texture = textures ? textures[i] : 0;
const GLuint unit = first + static_cast<GLuint>(i);
if (texture == 0) {
BindImageTexture(unit, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_R8);
continue;
}
const SharedPtr<MG_State::GLState::ITextureObject> textureObject =
ResolveMultiBindTexture(texture, i, __func__);
if (!textureObject) continue;
// "An INVALID_OPERATION error is generated if the internal format of any texture is not
// supported for image textures" - a texture that has never been given storage has no
// format at all and lands here too, rather than being reported as a bad enum by the
// scalar path.
const GLenum format = MG_Util::ConvertTextureInternalFormatToGLEnum(textureObject->GetFormat());
if (!IsValidImageTextureFormat(format)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", __func__,
std::format("textures[{}] ({}) has an internal format that is not supported for image "
"textures.",
i, texture)));
continue;
}
BindImageTexture(unit, texture, 0, GL_TRUE, 0, GL_READ_WRITE, format);
}
}
void GetTextureImage(GLuint texture, GLint level, GLenum format, GLenum type, GLsizei bufSize, void* pixels) {
auto textureObject = GetTextureObjectByName(texture, __func__);
if (!textureObject) return;
@@ -37,6 +37,8 @@ namespace MobileGL::MG_Impl::GLImpl {
GLenum format, GLenum type, const void* pixels);
void TextureSubImage3D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width,
GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* pixels);
void CompressedTextureSubImage2D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width,
GLsizei height, GLenum format, GLsizei imageSize, const void* data);
void TextureParameterf(GLuint texture, GLenum pname, GLfloat param);
void TextureParameterfv(GLuint texture, GLenum pname, const GLfloat* params);
void TextureParameteri(GLuint texture, GLenum pname, GLint param);
@@ -132,5 +134,11 @@ namespace MobileGL::MG_Impl::GLImpl {
void CompressedTexImage1D(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLint border,
GLsizei imageSize, const void* data);
void BindTexture(GLenum target, GLuint texture);
void BindTextures(GLuint first, GLsizei count, const GLuint* textures);
void BindImageTextures(GLuint first, GLsizei count, const GLuint* textures);
void ActiveTexture(GLenum texture);
// The number of texture image units a texture or a sampler may be bound to: what the backend
// advertises as GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, clamped by the frontend's fixed unit-array
// capacity. Shared so the texture and sampler multi-bind range checks cannot drift apart.
GLint GetCombinedTextureImageUnitCount();
} // namespace MobileGL::MG_Impl::GLImpl
@@ -353,6 +353,63 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
return true;
}
Bool ValidateTextureLevelExists(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, Int level,
const char* caller) {
// A null object is somebody else's error to report - ValidateTextureObject runs
// first at every call site and has already recorded it.
if (!textureObject) return false;
const auto* mipmapTexture = MG_State::GLState::AsMipmapTexture(textureObject.get());
if (mipmapTexture == nullptr) {
// The only non-mipmap storage class is a buffer texture, and GL_TEXTURE_BUFFER is
// not a target glCopyImageSubData accepts at all (it is in the CTS's invalid-target
// set). Declining here is not the error code the spec asks for - that would be
// INVALID_ENUM from a target check this validator is not - but it does keep a
// texture with no image levels whatsoever from reaching a backend that would
// dereference a backend texture it never created.
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"Texture has no mipmap levels to address."));
return false;
}
// What this number is, exactly, because two other things are almost it and neither is
// safe to assume: it is the number of level SLOTS the shadow has allocated - holes
// included, since MipmapStorage::AllocateLevel grows to level+1 and never fills the gap.
// For a cube map MipmapUploadTargetArray reports face +X's chain rather than the union.
//
// The guarantee that matters is one-sided: this count is always >= the level count the
// backends derive (VkTextureManager::GetUploadMipLevelCount stops at the first level
// with a non-positive extent, so it can only be shorter). That is the safe direction -
// no copy to a level the texture genuinely has is ever rejected here. It is NOT an
// exact match, so the backends keep their own range guard for the band in between: a
// chain with a hole (level 0 and 2 defined, 1 not) is accepted by this predicate and
// declined by the backend, which is a silent no-op rather than a copy. That band is a
// backend storage limitation, not a validation one - rejecting it here with
// INVALID_VALUE would be refusing a copy the spec permits.
const Uint levelCount = mipmapTexture->GetMipmapLevelCount();
if (levelCount == 0) {
// No image has ever been defined on this texture, so the fault is the texture,
// not the number: GL 4.6 core 18.3.2 asks for INVALID_OPERATION when an object a
// copy names is an incomplete texture.
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"Texture has no image defined at any level."));
return false;
}
if (level < 0 || static_cast<Uint>(level) >= levelCount) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"Texture level does not exist in this texture."));
return false;
}
return true;
}
Bool ValidateTextureObject(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject) {
if (!textureObject) {
MG_State::pGLContext->RecordError(
@@ -30,6 +30,16 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
TextureInternalFormat internalFormat,
TexturePixelDataType type);
Bool ValidateTextureLevelWithUploadTarget(TextureUploadTarget target, Int level);
// "Is <level> a level this texture actually has?", which ValidateTextureLevelNumber above
// does NOT answer - that one only bounds the index by GL_MAX_TEXTURE_SIZE and knows nothing
// about the object. Entry points that resolve a level straight into a backend image
// subresource need this one: a level the texture never had is GL_INVALID_VALUE (GL 4.6 core
// 18.3.2), and passing it through instead reaches the driver as an out-of-range subresource.
// Note the error split is per-entry-point, so this is not universally reusable:
// glClearTexImage owes INVALID_OPERATION for the same out-of-range level and spells its own
// copy of this predicate in GL_Texture.cpp (GetClearTextureObject).
Bool ValidateTextureLevelExists(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, Int level,
const char* caller);
Bool ValidateTextureObject(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject);
// Rejects the per-target default texture objects (name 0) with GL_INVALID_OPERATION for entry
// points that require a GenTextures-created texture, e.g. TexStorage* ("An INVALID_OPERATION
@@ -315,9 +315,10 @@ namespace MobileGL::MG_Impl::GLImpl {
auto offset = reinterpret_cast<SizeT>(pointer);
vao->SetAttributeFormat(index, size, dataType, false, stride, offset, true, false);
const int effectiveStride = EffectiveVertexStride(stride, size, type);
vao->SetAttributeFormat(index, size, dataType, false, stride, offset, true, false, effectiveStride);
vao->BindAttributeBuffer(index, vbo);
vao->MirrorPointerIntoBinding(index, vbo, offset, EffectiveVertexStride(stride, size, type));
vao->MirrorPointerIntoBinding(index, vbo, offset, effectiveStride);
}
void VertexAttribPointer_State(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride,
@@ -345,9 +346,11 @@ namespace MobileGL::MG_Impl::GLImpl {
// backend can pick the reversed VkFormat / pass GL_BGRA through to a GLES driver.
const bool isBgra = (size == static_cast<GLint>(GL_BGRA));
const int effectiveSize = isBgra ? 4 : size;
vao->SetAttributeFormat(index, effectiveSize, dataType, normalized, stride, offset, false, isBgra);
const int effectiveStride = EffectiveVertexStride(stride, effectiveSize, type);
vao->SetAttributeFormat(index, effectiveSize, dataType, normalized, stride, offset, false, isBgra,
effectiveStride);
vao->BindAttributeBuffer(index, vbo);
vao->MirrorPointerIntoBinding(index, vbo, offset, EffectiveVertexStride(stride, effectiveSize, type));
vao->MirrorPointerIntoBinding(index, vbo, offset, effectiveStride);
}
void BindVertexArray_State(GLuint array) {
@@ -524,7 +527,7 @@ namespace MobileGL::MG_Impl::GLImpl {
if (!MG_Backend::pActiveBackendObject ||
!MG_Backend::pActiveBackendObject->GetDynamicParameters().SupportsFloat64VertexAttributes) {
MGLOG_I("VertexAttribLFormat: attribute %u asked for a 64-bit (GL_DOUBLE) format, but this "
MGLOG_W_ONCE("VertexAttribLFormat: attribute %u asked for a 64-bit (GL_DOUBLE) format, but this "
"backend has no double-precision vertex attribute support - see the "
"\"64-bit vertex attributes\" / \"shaderFloat64\" POST row for what that costs",
attribindex);
@@ -166,32 +166,32 @@ MOBILEGL_GLX_API int glXSwapIntervalSGI(int interval) {
// Legacy entry points some loaders probe for; harmless no-op stubs.
MOBILEGL_GLX_API void glXCopyContext(Display*, void*, void*, unsigned long) {
MGLOG_W("glx: glXCopyContext is not supported");
MGLOG_W_ONCE("glx: glXCopyContext is not supported");
}
MOBILEGL_GLX_API unsigned long glXCreateGLXPixmap(Display*, void*, unsigned long) {
MGLOG_W("glx: glXCreateGLXPixmap is not supported");
MGLOG_W_ONCE("glx: glXCreateGLXPixmap is not supported");
return 0;
}
MOBILEGL_GLX_API void glXDestroyGLXPixmap(Display*, unsigned long) {}
MOBILEGL_GLX_API unsigned long glXCreatePixmap(Display*, void*, unsigned long, const int*) {
MGLOG_W("glx: glXCreatePixmap is not supported");
MGLOG_W_ONCE("glx: glXCreatePixmap is not supported");
return 0;
}
MOBILEGL_GLX_API void glXDestroyPixmap(Display*, unsigned long) {}
MOBILEGL_GLX_API unsigned long glXCreatePbuffer(Display*, void*, const int*) {
MGLOG_W("glx: glXCreatePbuffer is not supported");
MGLOG_W_ONCE("glx: glXCreatePbuffer is not supported");
return 0;
}
MOBILEGL_GLX_API void glXDestroyPbuffer(Display*, unsigned long) {}
MOBILEGL_GLX_API void glXUseXFont(unsigned long, int, int, int) {
MGLOG_W("glx: glXUseXFont is not supported");
MGLOG_W_ONCE("glx: glXUseXFont is not supported");
}
MOBILEGL_GLX_API void glXSelectEvent(Display*, unsigned long, unsigned long) {}
+9 -9
View File
@@ -149,7 +149,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
fns->Sync = reinterpret_cast<decltype(fns->Sync)>(dlsym(fns->Library, "XSync"));
}
if (!fns->Valid()) {
MGLOG_E("glx: failed to load libX11 entry points");
MGLOG_E_ONCE("glx: failed to load libX11 entry points");
}
return fns;
}();
@@ -314,7 +314,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
Uint32 width = 0;
Uint32 height = 0;
if (!QueryDrawableSize(dpy, drawable, width, height)) {
MGLOG_E("glx: XGetGeometry failed for drawable 0x%lx", drawable);
MGLOG_E_ONCE("glx: XGetGeometry failed for drawable 0x%lx", drawable);
return nullptr;
}
@@ -326,7 +326,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
EGLSurface surface = EGLImpl::CreatePlatformWindowSurface(
context.Display, context.Config, reinterpret_cast<void*>(drawable), attribs);
if (surface == EGL_NO_SURFACE) {
MGLOG_E("glx: failed to create window surface for drawable 0x%lx (%ux%u)", drawable,
MGLOG_E_ONCE("glx: failed to create window surface for drawable 0x%lx (%ux%u)", drawable,
width, height);
return nullptr;
}
@@ -347,7 +347,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
EGLDisplay display = EnsureDisplay();
if (display == EGL_NO_DISPLAY) {
MGLOG_E("glx: no EGL display");
MGLOG_E_ONCE("glx: no EGL display");
return nullptr;
}
EGLImpl::BindAPI(EGL_OPENGL_API);
@@ -376,13 +376,13 @@ namespace MobileGL::MG_Impl::GLXImpl {
EGLint configCount = 0;
if (!EGLImpl::ChooseConfig(display, configAttribs, &config, 1, &configCount) ||
configCount <= 0) {
MGLOG_E("glx: eglChooseConfig failed");
MGLOG_E_ONCE("glx: eglChooseConfig failed");
return nullptr;
}
EGLContext eglContext = EGLImpl::CreateContext(display, config, shareContext, contextAttribs);
if (eglContext == EGL_NO_CONTEXT) {
MGLOG_E("glx: eglCreateContext failed");
MGLOG_E_ONCE("glx: eglCreateContext failed");
return nullptr;
}
@@ -931,7 +931,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
if (!EGLImpl::MakeCurrent(object->Display, surface->Surface, surface->Surface,
object->Context)) {
MGLOG_E("glx: eglMakeCurrent failed (drawable=0x%lx, ctx=%p)", drawable, context);
MGLOG_E_ONCE("glx: eglMakeCurrent failed (drawable=0x%lx, ctx=%p)", drawable, context);
return 0;
}
t_current = {dpy, drawable, drawable, context};
@@ -943,7 +943,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
if (context && draw != read) {
// MobileGL's backends reject split draw/read surfaces; bind the draw
// drawable for both, which is what every real caller here needs.
MGLOG_W("glx: glXMakeContextCurrent draw 0x%lx != read 0x%lx, using draw for both", draw,
MGLOG_W_ONCE("glx: glXMakeContextCurrent draw 0x%lx != read 0x%lx, using draw for both", draw,
read);
}
const int result = MakeCurrent(dpy, draw, context);
@@ -958,7 +958,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
auto& surfaces = DrawableSurfaces();
auto it = surfaces.find(drawable);
if (it == surfaces.end()) {
MGLOG_W("glx: glXSwapBuffers with no surface for drawable 0x%lx", drawable);
MGLOG_W_ONCE("glx: glXSwapBuffers with no surface for drawable 0x%lx", drawable);
return;
}
SyncSurfaceSize(dpy, drawable, it->second);
+1 -1
View File
@@ -31,7 +31,7 @@ namespace MG_Impl::GLXImpl {
#endif
void* proc = MobileGL::MG_Impl::GetProcAddress(name);
if (!proc) {
MGLOG_W("Failed to get function: %s", (const char*)name);
MGLOG_D("Failed to get function: %s", (const char*)name);
return nullptr;
}
+1 -1
View File
@@ -1403,7 +1403,7 @@ namespace MobileGL::MG_Impl {
GETPROC(glFramebufferTextureMultiviewOVR, name);
// GETPROC(glNamedFramebufferTextureMultiviewOVR, name);
MGLOG_W("GetProcAddress(%s) = nullptr!", name);
MGLOG_D("GetProcAddress(%s) = nullptr!", name);
return nullptr;
}
} // namespace MobileGL::MG_Impl
@@ -269,7 +269,7 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
}
id metalLayerClass = reinterpret_cast<id>(objc_getClass("CAMetalLayer"));
if (!metalLayerClass) {
MGLOG_E("NSOpenGLImpl: CAMetalLayer class not found");
MGLOG_E_ONCE("NSOpenGLImpl: CAMetalLayer class not found");
return nil;
}
@@ -310,7 +310,7 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
static_cast<GLint>(geometry.DrawableSize.width),
static_cast<GLint>(geometry.DrawableSize.height));
if (error != kCGLNoError) {
MGLOG_E("NSOpenGLImpl: failed to attach drawable: %s", CGLImpl::ErrorString(error));
MGLOG_E_ONCE("NSOpenGLImpl: failed to attach drawable: %s", CGLImpl::ErrorString(error));
}
}
@@ -325,7 +325,7 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
}
const auto error = CGLImpl::SetCurrentContext(context);
if (error != kCGLNoError) {
MGLOG_E("NSOpenGLImpl: makeCurrentContext failed: %s", CGLImpl::ErrorString(error));
MGLOG_E_ONCE("NSOpenGLImpl: makeCurrentContext failed: %s", CGLImpl::ErrorString(error));
}
}
@@ -345,7 +345,7 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
}
const auto error = CGLImpl::FlushDrawable(context);
if (error != kCGLNoError) {
MGLOG_E("NSOpenGLImpl: flushBuffer failed: %s", CGLImpl::ErrorString(error));
MGLOG_E_ONCE("NSOpenGLImpl: flushBuffer failed: %s", CGLImpl::ErrorString(error));
}
}
@@ -377,7 +377,7 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
static_cast<GLint>(geometry.DrawableSize.width),
static_cast<GLint>(geometry.DrawableSize.height));
if (error != kCGLNoError) {
MGLOG_E("NSOpenGLImpl: update failed to attach drawable: %s", CGLImpl::ErrorString(error));
MGLOG_E_ONCE("NSOpenGLImpl: update failed to attach drawable: %s", CGLImpl::ErrorString(error));
return;
}
CGLImpl::UpdateContext(context);
@@ -421,7 +421,7 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
SEL selector = sel_registerName(selectorName);
Method method = class_getInstanceMethod(cls, selector);
if (!method) {
MGLOG_W("NSOpenGLImpl: missing instance method %s", selectorName);
MGLOG_W_ONCE("NSOpenGLImpl: missing instance method %s", selectorName);
return;
}
if (original) {
@@ -434,7 +434,7 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
SEL selector = sel_registerName(selectorName);
Method method = class_getClassMethod(cls, selector);
if (!method) {
MGLOG_W("NSOpenGLImpl: missing class method %s", selectorName);
MGLOG_W_ONCE("NSOpenGLImpl: missing class method %s", selectorName);
return;
}
method_setImplementation(method, replacement);
@@ -444,7 +444,7 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
Class pixelFormatClass = objc_getClass("NSOpenGLPixelFormat");
Class contextClass = objc_getClass("NSOpenGLContext");
if (!pixelFormatClass || !contextClass) {
MGLOG_W("NSOpenGLImpl: NSOpenGL classes are not loaded; hooks not installed");
MGLOG_W_ONCE("NSOpenGLImpl: NSOpenGL classes are not loaded; hooks not installed");
return false;
}
@@ -56,7 +56,7 @@ extern "C" HGLRC WINAPI wglCreateLayerContext(HDC hdc, int iLayerPlane) {
}
extern "C" BOOL WINAPI wglCopyContext(HGLRC, HGLRC, UINT) {
MGLOG_W("wglCopyContext is not supported");
MGLOG_W_ONCE("wglCopyContext is not supported");
SetLastError(ERROR_NOT_SUPPORTED);
return FALSE;
}
@@ -132,24 +132,24 @@ extern "C" DWORD WINAPI wglSwapMultipleBuffers(UINT n, CONST WGLSWAP* ps) {
// ---- Font rendering (legacy immediate-mode feature; not supported) ----
extern "C" BOOL WINAPI wglUseFontBitmapsA(HDC, DWORD, DWORD, DWORD) {
MGLOG_W("wglUseFontBitmapsA is not supported");
MGLOG_W_ONCE("wglUseFontBitmapsA is not supported");
return FALSE;
}
extern "C" BOOL WINAPI wglUseFontBitmapsW(HDC, DWORD, DWORD, DWORD) {
MGLOG_W("wglUseFontBitmapsW is not supported");
MGLOG_W_ONCE("wglUseFontBitmapsW is not supported");
return FALSE;
}
extern "C" BOOL WINAPI wglUseFontOutlinesA(HDC, DWORD, DWORD, DWORD, FLOAT, FLOAT, int,
LPGLYPHMETRICSFLOAT) {
MGLOG_W("wglUseFontOutlinesA is not supported");
MGLOG_W_ONCE("wglUseFontOutlinesA is not supported");
return FALSE;
}
extern "C" BOOL WINAPI wglUseFontOutlinesW(HDC, DWORD, DWORD, DWORD, FLOAT, FLOAT, int,
LPGLYPHMETRICSFLOAT) {
MGLOG_W("wglUseFontOutlinesW is not supported");
MGLOG_W_ONCE("wglUseFontOutlinesW is not supported");
return FALSE;
}
+7 -7
View File
@@ -215,7 +215,7 @@ namespace MobileGL::MG_Impl::WGLImpl {
Uint32 width = 0;
Uint32 height = 0;
if (!QueryClientSize(hwnd, width, height)) {
MGLOG_E("wgl: GetClientRect failed for HWND %p", hwnd);
MGLOG_E_ONCE("wgl: GetClientRect failed for HWND %p", hwnd);
return nullptr;
}
@@ -227,7 +227,7 @@ namespace MobileGL::MG_Impl::WGLImpl {
EGLSurface surface =
EGLImpl::CreatePlatformWindowSurface(context.Display, context.Config, hwnd, attribs);
if (surface == EGL_NO_SURFACE) {
MGLOG_E("wgl: failed to create window surface for HWND %p (%ux%u)", hwnd, width, height);
MGLOG_E_ONCE("wgl: failed to create window surface for HWND %p (%ux%u)", hwnd, width, height);
return nullptr;
}
@@ -244,7 +244,7 @@ namespace MobileGL::MG_Impl::WGLImpl {
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
EGLDisplay display = EnsureDisplay();
if (display == EGL_NO_DISPLAY) {
MGLOG_E("wgl: no EGL display");
MGLOG_E_ONCE("wgl: no EGL display");
return nullptr;
}
EGLImpl::BindAPI(EGL_OPENGL_API);
@@ -275,13 +275,13 @@ namespace MobileGL::MG_Impl::WGLImpl {
EGLConfig config = nullptr;
EGLint configCount = 0;
if (!EGLImpl::ChooseConfig(display, configAttribs, &config, 1, &configCount) || configCount <= 0) {
MGLOG_E("wgl: eglChooseConfig failed");
MGLOG_E_ONCE("wgl: eglChooseConfig failed");
return nullptr;
}
EGLContext eglContext = EGLImpl::CreateContext(display, config, shareContext, contextAttribs);
if (eglContext == EGL_NO_CONTEXT) {
MGLOG_E("wgl: eglCreateContext failed");
MGLOG_E_ONCE("wgl: eglCreateContext failed");
return nullptr;
}
@@ -612,7 +612,7 @@ namespace MobileGL::MG_Impl::WGLImpl {
auto& surfaces = WindowSurfaces();
auto it = surfaces.find(hwnd);
if (it == surfaces.end()) {
MGLOG_W("wglSwapBuffers: no surface for HWND %p", hwnd);
MGLOG_W_ONCE("wglSwapBuffers: no surface for HWND %p", hwnd);
return FALSE;
}
SyncSurfaceSize(hwnd, it->second);
@@ -685,7 +685,7 @@ namespace MobileGL::MG_Impl::WGLImpl {
}
if (!EGLImpl::MakeCurrent(object->Display, surface->Surface, surface->Surface, object->Context)) {
MGLOG_E("wglMakeCurrent: eglMakeCurrent failed (hdc=%p, hglrc=%p)", hdc, hglrc);
MGLOG_E_ONCE("wglMakeCurrent: eglMakeCurrent failed (hdc=%p, hglrc=%p)", hdc, hglrc);
return FALSE;
}
t_current = {hdc, hglrc};
@@ -50,6 +50,7 @@ add_executable(MobileGLIntegrationTest
Scenarios/CrossFrameBufferScenario.cpp
Scenarios/ResidentIndexScenario.cpp
Scenarios/MultiDrawScenario.cpp
Scenarios/DrawParametersScenario.cpp
Scenarios/AsyncCompileScenario.cpp
Scenarios/XfbAfterClipDistanceScenario.cpp
Scenarios/ThreeChannelAttachmentScenario.cpp
@@ -59,15 +60,26 @@ add_executable(MobileGLIntegrationTest
Scenarios/FragCoordOriginScenario.cpp
Scenarios/ClearThenReadPixelsScenario.cpp
Scenarios/DepthStencilReadbackScenario.cpp
Scenarios/DepthStencilReadbackMatrixScenario.cpp
Scenarios/DepthStencilReadbackAttachmentShapeScenario.cpp
Scenarios/ClipDistanceScenario.cpp
Scenarios/SsboArrayLengthScenario.cpp
Scenarios/DoublePrecisionScenario.cpp
Scenarios/UniformInitializerScenario.cpp
Scenarios/SwizzleAccessRoutineScenario.cpp
Scenarios/ProgramPipelineScenario.cpp
Scenarios/ImageLoadStoreSsoScenario.cpp
Scenarios/ImageTargetKindScenario.cpp
Scenarios/ImageFormatQualifierScenario.cpp
Scenarios/SsboDeclarationFormScenario.cpp
Scenarios/Glsl420DeclarationScenario.cpp
Scenarios/FragmentOutputArrayIndexScenario.cpp
Scenarios/BufferTextureScenario.cpp
Scenarios/VertexAttribBindingScenario.cpp
Scenarios/XfbCaptureBufferReuseScenario.cpp
Scenarios/VertexArrayEnableDisableScenario.cpp
Scenarios/CopyImageLevelRangeScenario.cpp
Scenarios/CopyImageLayeredScenario.cpp
)
target_include_directories(MobileGLIntegrationTest PRIVATE
@@ -235,6 +247,8 @@ mgl_itest_join_environment(MGL_ITEST_VULKAN_ENVIRONMENT
"MOBILEGL_BACKEND_TYPE=DirectVulkan" ${MGL_ITEST_VULKAN_ENV})
mgl_itest_join_environment(MGL_ITEST_VULKAN_ASYNC_ENVIRONMENT
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_ASYNC_SHADER_COMPILE=1" ${MGL_ITEST_VULKAN_ENV})
mgl_itest_join_environment(MGL_ITEST_GLES_FORCED_DS_ENVIRONMENT
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION=1" ${MGL_ITEST_COMMON_ENV})
# TIMEOUT on every entry: a GPU test that wedges must fail the run, not hang it.
set(MGL_ITEST_TIMEOUT 120)
@@ -282,3 +296,21 @@ gtest_discover_tests(MobileGLIntegrationTest
TIMEOUT ${MGL_ITEST_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_VULKAN_ASYNC_ENVIRONMENT}"
)
# A fourth registration, of the depth/stencil readback scenarios, with the ES
# shader-sampling emulation forced on. Not paranoia - without it these scenarios are
# UNFALSIFIABLE on the machines this suite runs on: OpenGL ES has no depth or stencil
# readback in core, but Mesa accepts the reads anyway, so on llvmpipe every one of them
# goes green through a native path that the Adreno device does not have. Deleting the
# entire emulation left all of them passing. With the flag the native spellings are off
# the table and only the path the device actually takes remains. DirectGLES only - the
# emulation is DirectGLES's.
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectGLES.ForcedDepthStencilEmulation."
TEST_FILTER "DepthStencilReadback*Scenario.*"
DISCOVERY_TIMEOUT 30
PROPERTIES
LABELS integration-gpu
TIMEOUT ${MGL_ITEST_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_GLES_FORCED_DS_ENVIRONMENT}"
)
@@ -94,6 +94,89 @@ namespace MGITest {
}
}
// A per-stage block count is an amount of BINDING POINTS an application will use, so it
// can never exceed the number of binding points that exist. GL 4.6 Table 23.64 states the
// relation the other way round (MAX_UNIFORM_BUFFER_BINDINGS >= MAX_COMBINED_UNIFORM_BLOCKS
// >= every per-stage count), and DirectVulkan broke it by clamping the two families
// independently: a device reporting 256 compute uniform blocks and 84 uniform binding
// points passes both ceilings and still cannot serve
// KHR-GL44.multi_bind.dispatch_bind_buffers_base, which reads the block count and binds
// that many buffers in one glBindBuffersBase - INVALID_OPERATION before a single bind.
TEST_F(AdvertisedLimitsScenario, PerStageBlockCountsFitInTheirBindingPoints) {
struct Relation {
GLenum blocks;
const char* blocksName;
GLenum bindings;
const char* bindingsName;
};
const Relation relations[] = {
{GL_MAX_COMPUTE_UNIFORM_BLOCKS, "GL_MAX_COMPUTE_UNIFORM_BLOCKS", GL_MAX_UNIFORM_BUFFER_BINDINGS,
"GL_MAX_UNIFORM_BUFFER_BINDINGS"},
{GL_MAX_VERTEX_UNIFORM_BLOCKS, "GL_MAX_VERTEX_UNIFORM_BLOCKS", GL_MAX_UNIFORM_BUFFER_BINDINGS,
"GL_MAX_UNIFORM_BUFFER_BINDINGS"},
{GL_MAX_FRAGMENT_UNIFORM_BLOCKS, "GL_MAX_FRAGMENT_UNIFORM_BLOCKS", GL_MAX_UNIFORM_BUFFER_BINDINGS,
"GL_MAX_UNIFORM_BUFFER_BINDINGS"},
{GL_MAX_COMBINED_UNIFORM_BLOCKS, "GL_MAX_COMBINED_UNIFORM_BLOCKS", GL_MAX_UNIFORM_BUFFER_BINDINGS,
"GL_MAX_UNIFORM_BUFFER_BINDINGS"},
{GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS, "GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS",
GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS, "GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS"},
{GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS, "GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS",
GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS, "GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS"},
};
for (const Relation& relation : relations) {
GLint blocks = -1;
GLint bindings = -1;
glGetIntegerv(relation.blocks, &blocks);
glGetIntegerv(relation.bindings, &bindings);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << relation.blocksName;
EXPECT_LE(blocks, bindings)
<< relation.blocksName << " = " << blocks << " exceeds " << relation.bindingsName << " = "
<< bindings << "; a shader may declare more blocks than there are binding points to bind them to";
}
}
// KHR-GL44.multi_bind.functional_bind_buffers_range sizes each of an indexed target's
// binding points at MAX_<target>_SIZE / MAX_<target>_BINDINGS and binds all of them in
// one glBindBuffersRange. That quotient has to be a legal BindBufferRange size, which
// makes the two limits of every indexed family a PAIR: advertise a size that does not
// survive division by the binding count and the call fails with INVALID_VALUE before any
// of it binds.
TEST_F(AdvertisedLimitsScenario, IndexedTargetSizeSurvivesDivisionByItsBindingCount) {
struct IndexedFamily {
GLenum maxSize;
const char* maxSizeName;
GLenum maxBindings;
const char* maxBindingsName;
GLint sizeGranularity; // BindBufferRange's size rule for the target
};
const IndexedFamily families[] = {
{GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE, "GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE",
GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS, "GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS", 1},
{GL_MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS, "GL_MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS",
GL_MAX_TRANSFORM_FEEDBACK_BUFFERS, "GL_MAX_TRANSFORM_FEEDBACK_BUFFERS", 4},
{GL_MAX_UNIFORM_BLOCK_SIZE, "GL_MAX_UNIFORM_BLOCK_SIZE", GL_MAX_UNIFORM_BUFFER_BINDINGS,
"GL_MAX_UNIFORM_BUFFER_BINDINGS", 1},
{GL_MAX_SHADER_STORAGE_BLOCK_SIZE, "GL_MAX_SHADER_STORAGE_BLOCK_SIZE",
GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS, "GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS", 1},
};
for (const IndexedFamily& family : families) {
GLint maxSize = -1;
GLint maxBindings = -1;
glGetIntegerv(family.maxSize, &maxSize);
glGetIntegerv(family.maxBindings, &maxBindings);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << family.maxSizeName;
ASSERT_GT(maxBindings, 0) << family.maxBindingsName;
const GLint perBinding = maxSize / maxBindings;
EXPECT_GT(perBinding, 0)
<< family.maxSizeName << " (" << maxSize << ") / " << family.maxBindingsName << " ("
<< maxBindings << ") is zero, and BindBufferRange rejects a zero size";
EXPECT_EQ(perBinding % family.sizeGranularity, 0)
<< family.maxSizeName << " (" << maxSize << ") / " << family.maxBindingsName << " ("
<< maxBindings << ") = " << perBinding << " is not a multiple of the "
<< family.sizeGranularity << "-byte size granularity BindBufferRange requires for it";
}
}
// The OOM case in isolation, because it is the one with a known CTS victim and the one a
// future refactor is most likely to reintroduce by copying the Vulkan limit back.
TEST_F(AdvertisedLimitsScenario, ComputeUniformBlocksIsAnAmountAnApplicationCouldActuallyAllocate) {
@@ -33,6 +33,7 @@
// branch on. The driver POST's "Buffer textures" row is where that verdict is stated.
#include <cstdint>
#include <cstring>
#include <string>
#include <vector>
@@ -73,7 +74,60 @@ out vec4 o_color;
void main() { o_color = vec4(float(vFace) / 255.0, 0.0, 0.0, 1.0); }
)";
class BufferTextureScenario : public ScenarioTest {};
// A buffer texture bound as a WRITABLE image: the shader reads one texel and writes
// another, so a single dispatch proves the read direction (which already worked) and
// the write direction (which is what this exists for) apart from each other.
constexpr const char* kImageBufferCS = R"(#version 430 core
layout(local_size_x = 1) in;
layout(binding = 0, rgba8) uniform imageBuffer uImage;
void main() {
vec4 read = imageLoad(uImage, 1);
imageStore(uImage, 0, vec4(0.0, 1.0, 0.0, 1.0));
imageStore(uImage, 2, read);
}
)";
class BufferTextureScenario : public ScenarioTest {
protected:
bool ComputeImagesAreUsable() const {
GLint maxImageUnits = 0;
GLint maxComputeImageUniforms = 0;
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
glGetIntegerv(GL_MAX_COMPUTE_IMAGE_UNIFORMS, &maxComputeImageUniforms);
while (glGetError() != GL_NO_ERROR) {
}
return maxImageUnits >= 1 && maxComputeImageUniforms >= 1;
}
unsigned int MakeComputeProgram(const char* source) {
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
glShaderSource(shader, 1, &source, nullptr);
glCompileShader(shader);
GLint compiled = GL_FALSE;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (compiled == GL_FALSE) {
char log[4096] = {};
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
ADD_FAILURE() << "the compute shader did not compile: " << log;
glDeleteShader(shader);
return 0;
}
const GLuint program = glCreateProgram();
glAttachShader(program, shader);
glLinkProgram(program);
glDeleteShader(shader);
GLint linked = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
if (linked == GL_FALSE) {
char log[4096] = {};
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
ADD_FAILURE() << "the compute program did not link: " << log;
glDeleteProgram(program);
return 0;
}
return program;
}
};
// Draws the full-viewport quad and returns the red byte every fragment was painted with,
// or -1 if the quad did not come out uniform (which would mean the flat varying, not the
@@ -171,4 +225,78 @@ void main() { o_color = vec4(float(vFace) / 255.0, 0.0, 0.0, 1.0); }
EXPECT_EQ(FirstGLError(), 0u);
}
// A shader may WRITE a buffer texture too, through an image unit, and the bytes it writes
// land in the backend's buffer - not in the frontend's CPU shadow, which is what MapBuffer
// and GetBufferSubData hand back. A storage-block write is flagged for exactly this reason
// and the shadow is refreshed on the next read; a buffer reached through an image unit is
// the same write through a different binding, and Espryt used to flag only the first, so
// an imageStore into a buffer texture was invisible to every CPU read that followed it -
// silently, with the correct value sitting in the driver's buffer the whole time.
//
// The read direction is asserted in the same dispatch (texel 2 is a copy of texel 1) so a
// failure here cannot be blamed on the image binding not working at all.
TEST_F(BufferTextureScenario, AnImageStoreIntoABufferTextureIsVisibleToTheCpu) {
if (!Ready()) return;
if (!ComputeImagesAreUsable()) GTEST_SKIP() << "no compute image units on this host";
constexpr GLuint kRed = 0x000000ffu; // RGBA8 little-endian: r = 255
constexpr GLuint kGreen = 0xff00ff00u; // what the shader stores: (0, 1, 0, 1)
constexpr int kTexels = 16;
FirstGLError();
const unsigned int program = MakeComputeProgram(kImageBufferCS);
ASSERT_NE(program, 0u);
const std::vector<GLuint> texels(kTexels, kRed);
GLuint buffer = 0;
glGenBuffers(1, &buffer);
glBindBuffer(GL_TEXTURE_BUFFER, buffer);
glBufferData(GL_TEXTURE_BUFFER, static_cast<GLsizeiptr>(texels.size() * sizeof(GLuint)), texels.data(),
GL_DYNAMIC_COPY);
GLuint texture = 0;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_BUFFER, texture);
glTexBuffer(GL_TEXTURE_BUFFER, GL_RGBA8, buffer);
EXPECT_EQ(FirstGLError(), 0u) << "glTexBuffer(GL_RGBA8) was refused";
glBindImageTexture(0, texture, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA8);
EXPECT_EQ(FirstGLError(), 0u) << "glBindImageTexture on a buffer texture was refused";
glUseProgram(program);
glDispatchCompute(1, 1, 1);
glMemoryBarrier(GL_ALL_BARRIER_BITS);
// Both CPU read paths, because they are two entry points onto the same refresh and a
// fix that reaches only one of them is not a fix. Everything below is EXPECT rather than
// ASSERT so that a failure still reaches the cleanup at the end: the harness shares one
// context across every scenario in the process, and a leaked buffer or image binding
// here would surface as a failure somewhere else entirely.
std::vector<GLuint> readBack(kTexels, 0u);
glBindBuffer(GL_TEXTURE_BUFFER, buffer);
glGetBufferSubData(GL_TEXTURE_BUFFER, 0, static_cast<GLsizeiptr>(readBack.size() * sizeof(GLuint)),
readBack.data());
EXPECT_EQ(readBack[0], kGreen) << "glGetBufferSubData did not see the imageStore";
EXPECT_EQ(readBack[2], kRed) << "the imageLoad side of the same dispatch read the wrong texel";
const void* mapped = glMapBuffer(GL_TEXTURE_BUFFER, GL_READ_ONLY);
EXPECT_NE(mapped, nullptr) << "glMapBuffer(GL_READ_ONLY) on the texture's buffer failed";
if (mapped != nullptr) {
GLuint mappedTexel0 = 0;
std::memcpy(&mappedTexel0, mapped, sizeof(mappedTexel0));
EXPECT_EQ(mappedTexel0, kGreen) << "glMapBuffer did not see the imageStore";
glUnmapBuffer(GL_TEXTURE_BUFFER);
}
glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
glBindBuffer(GL_TEXTURE_BUFFER, 0);
glBindTexture(GL_TEXTURE_BUFFER, 0);
glUseProgram(0);
glDeleteProgram(program);
glDeleteTextures(1, &texture);
glDeleteBuffers(1, &buffer);
EXPECT_EQ(FirstGLError(), 0u);
}
} // namespace MGITest
@@ -0,0 +1,388 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ClipDistanceScenario.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Scenario - gl_ClipDistance ACTUALLY CLIPS, AND ONLY WHERE IT IS ENABLED.
//
// CapabilityInput::ClipDistance0..7 existed end to end - the GL enum converted to it, the
// string converter named it, glEnable(GL_CLIP_DISTANCE0 + i) raised no error - and then
// RenderState::SetCapability had no case for it and dropped it into `default: break`. Nothing
// was stored, no version was bumped, and neither backend ever heard about it. The shader half
// worked all along (SPIRV-Cross emits gl_ClipDistance with a
// `#extension GL_EXT_clip_cull_distance : require` that Adreno accepts), so the distances were
// computed and then ignored: no clipping ever happened on DirectGLES, which is the whole of
// KHR-GLxx.clip_distance.functional. glIsEnabled lied about it too - it returned GL_FALSE
// immediately after a successful glEnable.
//
// The assertions are behavioural, not query-shaped, because a query-only test passes against a
// backend that stores the bit and never forwards it. Each case draws one full-viewport triangle
// whose clip distance is positive on one side of the viewport and negative on the other, then
// checks BOTH sides: the kept side proves the draw happened at all, and the clipped side is the
// actual claim. The disabled case is the negative control - the identical shader with the
// identical distances and the enable turned off must leave both sides painted, which is what
// says the pixels below are being removed by clipping and not by something else.
//
// HONEST LIMIT OF THIS FILE IN CI. Of the four cases, only EnableIsObservableThroughIsEnabled is
// falsifiable on the software rasterizers every automated lane runs on. llvmpipe and lavapipe
// clip by EVERY declared gl_ClipDistance regardless of the enables, so
// AnEnabledClipDistanceRemovesTheNegativeHalf goes green there against the broken tree as well,
// and the two cases that need real per-distance semantics skip (see
// DriverHonoursPerDistanceEnables). What actually pins the behaviour is Adreno, through
// KHR-GLxx.clip_distance.functional - whose "without dynamic redeclaration" variants declare all
// gl_MaxClipDistances slots and enable only the first N, i.e. exactly the subset semantics these
// skipped cases assert. Read a green CI run here as "the state survives the frontend", not as
// "clipping is correct"; the second claim is a device claim.
//
// Every case disables all eight distances on entry rather than assuming they start off:
// XfbAfterClipDistanceScenario deliberately leaves one enabled for the rest of the process, and
// forwarding the enables is what turned that leftover from inert bookkeeping into live driver
// state.
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
#ifndef GL_CLIP_DISTANCE0
#define GL_CLIP_DISTANCE0 0x3000
#endif
#ifndef GL_CLIP_DISTANCE1
#define GL_CLIP_DISTANCE1 0x3001
#endif
namespace MGITest {
namespace {
// One clip distance per half of the viewport: distance 0 is positive on the right half
// (x > 0 in clip space) and distance 1 is positive on the top half. A vertex shader
// producing a full-screen triangle from gl_VertexID, so no buffers are needed.
const char* const kVertexSource = R"(#version 400 core
out float gl_ClipDistance[2];
void main() {
vec2 positions[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));
vec2 p = positions[gl_VertexID];
gl_Position = vec4(p, 0.0, 1.0);
gl_ClipDistance[0] = p.x;
gl_ClipDistance[1] = p.y;
}
)";
const char* const kFragmentSource = R"(#version 400 core
out vec4 fragColor;
void main() { fragColor = vec4(0.0, 1.0, 0.0, 1.0); }
)";
class ClipDistanceScenario : public ScenarioTest {
protected:
GLuint BuildProgram() {
const GLuint vs = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vs, 1, &kVertexSource, nullptr);
glCompileShader(vs);
GLint compiled = 0;
glGetShaderiv(vs, GL_COMPILE_STATUS, &compiled);
if (!compiled) {
m_buildLog = ShaderLog(vs);
glDeleteShader(vs);
return 0;
}
const GLuint fs = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fs, 1, &kFragmentSource, nullptr);
glCompileShader(fs);
glGetShaderiv(fs, GL_COMPILE_STATUS, &compiled);
if (!compiled) {
m_buildLog = ShaderLog(fs);
glDeleteShader(vs);
glDeleteShader(fs);
return 0;
}
const GLuint program = glCreateProgram();
glAttachShader(program, vs);
glAttachShader(program, fs);
glLinkProgram(program);
GLint linked = 0;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
glDeleteShader(vs);
glDeleteShader(fs);
if (!linked) {
GLint length = 0;
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
std::vector<char> log(static_cast<size_t>(length > 1 ? length : 1), '\0');
glGetProgramInfoLog(program, static_cast<GLsizei>(log.size()), nullptr, log.data());
m_buildLog = log.data();
glDeleteProgram(program);
return 0;
}
return program;
}
const std::string& BuildLog() const { return m_buildLog; }
// Paints the whole viewport red, then draws the clipped triangle in green.
void DrawClippedTriangle(GLuint program, GLuint vao) const {
glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(program);
glBindVertexArray(vao);
glDrawArrays(GL_TRIANGLES, 0, 3);
}
static bool IsGreen(const unsigned char* px) {
return px[0] < 64 && px[1] > 192;
}
static bool IsRed(const unsigned char* px) {
return px[0] > 192 && px[1] < 64;
}
void PixelAt(int x, int y, unsigned char* out) const {
glReadPixels(x, y, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, out);
}
// Never assume the eight start disabled - see the header note about
// XfbAfterClipDistanceScenario leaving one on for the rest of the process.
static void DisableEveryClipDistance() {
for (int i = 0; i < 8; ++i) {
glDisable(static_cast<GLenum>(GL_CLIP_DISTANCE0 + i));
}
}
// True when the driver under this backend actually implements PER-DISTANCE enable
// state, i.e. when a written-but-disabled gl_ClipDistance leaves its fragments
// alone. Not every stack does, and the difference is not MobileGL's to hide:
//
// - Adreno's ES driver honours GL_CLIP_DISTANCE0_EXT..7_EXT, which is what makes
// KHR-GLxx.clip_distance.functional pass on the device once the enables are
// forwarded at all.
// - Vulkan has no such state: every clip distance a shader declares is active,
// always. DirectVulkan therefore clips by a disabled distance.
// - Mesa's llvmpipe ES driver behaves like Vulkan here.
//
// Emulating GL's semantics on those two would mean forcing the disabled slots to a
// non-negative value inside the shader, which makes the enable mask part of the
// pipeline key - a feature, not a fix, and deliberately not attempted here. The
// cases that need the real semantics gate on this probe and say so when they skip,
// rather than being deleted or silently weakened.
bool DriverHonoursPerDistanceEnables(GLuint program, GLuint vao) const {
for (int i = 0; i < 8; ++i) {
glDisable(static_cast<GLenum>(GL_CLIP_DISTANCE0 + i));
}
DrawClippedTriangle(program, vao);
unsigned char negativeSide[4] = {0, 0, 0, 0};
glReadPixels(Gl().Width() / 4, Gl().Height() / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, negativeSide);
return IsGreen(negativeSide);
}
private:
static std::string ShaderLog(GLuint shader) {
GLint length = 0;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
std::vector<char> log(static_cast<size_t>(length > 1 ? length : 1), '\0');
glGetShaderInfoLog(shader, static_cast<GLsizei>(log.size()), nullptr, log.data());
return log.data();
}
std::string m_buildLog;
};
} // namespace
// The state itself: glEnable must be observable through glIsEnabled. This is the cheap half
// of the bug - SetCapability's missing case made the query answer GL_FALSE for a capability
// that had just been enabled without error.
TEST_F(ClipDistanceScenario, EnableIsObservableThroughIsEnabled) {
if (!Ready()) return;
HeadlessGL& gl = Gl();
DisableEveryClipDistance();
EXPECT_EQ(glIsEnabled(GL_CLIP_DISTANCE0), GL_FALSE)
<< "glDisable(GL_CLIP_DISTANCE0) is not observable through glIsEnabled";
glEnable(GL_CLIP_DISTANCE0);
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_EQ(glIsEnabled(GL_CLIP_DISTANCE0), GL_TRUE)
<< "glEnable(GL_CLIP_DISTANCE0) raised no error but glIsEnabled still reports it disabled";
EXPECT_EQ(glIsEnabled(GL_CLIP_DISTANCE1), GL_FALSE)
<< "enabling distance 0 must not enable distance 1 - the eight are independent";
glEnable(GL_CLIP_DISTANCE1);
glDisable(GL_CLIP_DISTANCE0);
EXPECT_EQ(glIsEnabled(GL_CLIP_DISTANCE0), GL_FALSE);
EXPECT_EQ(glIsEnabled(GL_CLIP_DISTANCE1), GL_TRUE);
glDisable(GL_CLIP_DISTANCE1);
EXPECT_EQ(FirstGLError(), 0u);
gl.EndFrame();
}
// The claim: an enabled clip distance removes the fragments where it is negative.
TEST_F(ClipDistanceScenario, AnEnabledClipDistanceRemovesTheNegativeHalf) {
if (!Ready()) return;
HeadlessGL& gl = Gl();
const int width = gl.Width();
const int height = gl.Height();
ASSERT_GE(width, 8);
ASSERT_GE(height, 8);
GLuint vao = 0;
glGenVertexArrays(1, &vao);
const GLuint program = BuildProgram();
ASSERT_NE(program, 0u) << "the gl_ClipDistance program did not build: " << BuildLog();
BindDefaultFramebuffer();
glViewport(0, 0, width, height);
glDisable(GL_SCISSOR_TEST);
glDisable(GL_DEPTH_TEST);
glDisable(GL_CULL_FACE);
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
// Distance 1 is positive by a single pixel at the sampled row, so a stray enable on it
// would put the "kept" probe right on the clip boundary.
DisableEveryClipDistance();
glEnable(GL_CLIP_DISTANCE0);
DrawClippedTriangle(program, vao);
EXPECT_EQ(FirstGLError(), 0u);
unsigned char right[4] = {0, 0, 0, 0};
unsigned char left[4] = {0, 0, 0, 0};
PixelAt(width - 1 - width / 4, height / 2, right);
PixelAt(width / 4, height / 2, left);
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_TRUE(IsGreen(right)) << "the kept half is not painted (" << int(right[0]) << "," << int(right[1])
<< "," << int(right[2]) << ") - the draw itself did not happen, so the clipped "
"half below proves nothing";
EXPECT_TRUE(IsRed(left)) << "gl_ClipDistance[0] is negative on the left half and GL_CLIP_DISTANCE0 is "
"enabled, so those fragments must be clipped away; found ("
<< int(left[0]) << "," << int(left[1]) << "," << int(left[2]) << ")";
glDisable(GL_CLIP_DISTANCE0);
glUseProgram(0);
glBindVertexArray(0);
glDeleteProgram(program);
glDeleteVertexArrays(1, &vao);
gl.EndFrame();
}
// The negative control: the same shader writing the same distances, with the enable off,
// must paint both halves. Without this a backend that clipped everything - or one whose
// draw simply failed - would pass the case above.
TEST_F(ClipDistanceScenario, ADisabledClipDistanceRemovesNothing) {
if (!Ready()) return;
HeadlessGL& gl = Gl();
const int width = gl.Width();
const int height = gl.Height();
ASSERT_GE(width, 8);
ASSERT_GE(height, 8);
GLuint vao = 0;
glGenVertexArrays(1, &vao);
const GLuint program = BuildProgram();
ASSERT_NE(program, 0u) << "the gl_ClipDistance program did not build: " << BuildLog();
BindDefaultFramebuffer();
glViewport(0, 0, width, height);
glDisable(GL_SCISSOR_TEST);
glDisable(GL_DEPTH_TEST);
glDisable(GL_CULL_FACE);
DisableEveryClipDistance();
DrawClippedTriangle(program, vao);
EXPECT_EQ(FirstGLError(), 0u);
unsigned char right[4] = {0, 0, 0, 0};
unsigned char left[4] = {0, 0, 0, 0};
PixelAt(width - 1 - width / 4, height / 2, right);
PixelAt(width / 4, height / 2, left);
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_TRUE(IsGreen(right)) << "with every clip distance disabled the whole triangle must survive";
const bool driverHonoursEnables = IsGreen(left);
glUseProgram(0);
glBindVertexArray(0);
glDeleteProgram(program);
glDeleteVertexArrays(1, &vao);
gl.EndFrame();
if (!driverHonoursEnables) {
GTEST_SKIP() << "renderer " << gl.RendererString()
<< " clips by a DISABLED gl_ClipDistance - it does not implement per-distance enable state "
"(see DriverHonoursPerDistanceEnables). Emulating GL's semantics there needs shader-side "
"masking keyed on the enable mask, which is a separate feature";
}
}
// The eight enables are independent: enabling only distance 1 must clip by distance 1 and
// leave distance 0 alone. A backend that forwarded "any clip distance enabled" as a single
// bit, or that always enables every declared distance (which is what Vulkan does natively),
// passes both cases above and fails this one.
TEST_F(ClipDistanceScenario, TheEnablesAreIndependentPerDistance) {
if (!Ready()) return;
HeadlessGL& gl = Gl();
const int width = gl.Width();
const int height = gl.Height();
ASSERT_GE(width, 8);
ASSERT_GE(height, 8);
GLuint vao = 0;
glGenVertexArrays(1, &vao);
const GLuint program = BuildProgram();
ASSERT_NE(program, 0u) << "the gl_ClipDistance program did not build: " << BuildLog();
BindDefaultFramebuffer();
glViewport(0, 0, width, height);
glDisable(GL_SCISSOR_TEST);
glDisable(GL_DEPTH_TEST);
glDisable(GL_CULL_FACE);
if (!DriverHonoursPerDistanceEnables(program, vao)) {
glUseProgram(0);
glBindVertexArray(0);
glDeleteProgram(program);
glDeleteVertexArrays(1, &vao);
DisableEveryClipDistance();
gl.EndFrame();
GTEST_SKIP() << "renderer " << gl.RendererString()
<< " clips by every declared gl_ClipDistance regardless of the enables, so per-distance "
"independence is not observable here";
}
DisableEveryClipDistance();
glEnable(GL_CLIP_DISTANCE1);
DrawClippedTriangle(program, vao);
EXPECT_EQ(FirstGLError(), 0u);
// Distance 1 is negative on the bottom half, distance 0 on the left half. With only
// distance 1 enabled, the bottom-left must survive (distance 0 is off) and the bottom
// must not.
unsigned char topLeft[4] = {0, 0, 0, 0};
unsigned char bottomRight[4] = {0, 0, 0, 0};
PixelAt(width / 4, height - 1 - height / 4, topLeft);
PixelAt(width - 1 - width / 4, height / 4, bottomRight);
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_TRUE(IsGreen(topLeft)) << "gl_ClipDistance[0] is negative here but GL_CLIP_DISTANCE0 is disabled, so "
"this fragment must survive";
EXPECT_TRUE(IsRed(bottomRight)) << "gl_ClipDistance[1] is negative here and GL_CLIP_DISTANCE1 is enabled, so "
"this fragment must be clipped";
glDisable(GL_CLIP_DISTANCE1);
glUseProgram(0);
glBindVertexArray(0);
glDeleteProgram(program);
glDeleteVertexArrays(1, &vao);
gl.EndFrame();
}
} // namespace MGITest
@@ -0,0 +1,331 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/CopyImageLayeredScenario.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Scenario - glCopyImageSubData MOVES EVERY SLICE IT WAS ASKED FOR, NOT JUST SLICE 0.
//
// KHR-GL43.copy_image.functional_* copies a whole 12-layer region in one call whenever both
// endpoints are layered, i.e. for the four target pairs 2d_array->2d_array, 2d_array->3d,
// 3d->2d_array and 3d->3d. DirectVulkan built its VkImageCopy with baseArrayLayer 0, layerCount 1
// and srcOffset.z 0 no matter what the call asked for, so slice 0 landed correctly and slices 1..N
// were never written - 64 conformance cases (16 compatible format pairs x those 4 pairs) failing
// with "first mismatch at [x, y, 1]", the first texel of the first slice the copy skipped.
//
// The reason one hardcode covered both shapes wrongly is that GL states a layered copy ONE way -
// srcZ/dstZ and srcDepth - while Vulkan states it two ways and picks by image type:
//
// GL_TEXTURE_3D -> VK_IMAGE_TYPE_3D: slices are z, so srcOffset.z/dstOffset.z select them
// and extent.depth counts them; the layer range must stay (0, 1).
// GL_TEXTURE_2D_ARRAY -> VK_IMAGE_TYPE_2D: slices are array layers, so baseArrayLayer selects
// them and layerCount counts them; offset.z stays 0.
//
// A mixed pair is legal (maintenance1, core in Vulkan 1.1) but only when the counts correspond:
// the 3D side's extent.depth has to equal the array side's layerCount. So the four pairs below are
// four DIFFERENT VkImageCopy shapes, not one shape with different arguments, which is why one
// scenario per pair is the coverage that matters here.
//
// Every case also asserts the slices OUTSIDE the copied range still hold their fill. A backend
// that "fixed" the miss by copying the whole image regardless of srcZ/srcDepth would pass a
// slices-landed check and fail this one.
//
// The verification path is an FBO attachment per slice plus glReadPixels, not glGetTexImage: it is
// the readback both backends share, and glFramebufferTextureLayer names an array layer and a 3D
// slice through the same call, so the two texture kinds are read back identically.
//
// DirectGLES is the control - it forwards to the driver's own glCopyImageSubData - so a failure on
// both backends means the scenario is wrong, and a failure on DirectVulkan alone means Magma is.
#include <array>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr int kWidth = 4;
constexpr int kHeight = 4;
// Six is enough for a copy that starts and ends away from both edges of both endpoints
// while still leaving untouched slices on either side to assert against.
constexpr int kSlices = 6;
struct Rgba8 {
GLubyte r = 0, g = 0, b = 0, a = 0;
bool operator==(const Rgba8& other) const {
return r == other.r && g == other.g && b == other.b && a == other.a;
}
};
std::string Describe(const Rgba8& color) {
return "(" + std::to_string(color.r) + ", " + std::to_string(color.g) + ", " + std::to_string(color.b) +
", " + std::to_string(color.a) + ")";
}
// Per-slice constants, uniform within a slice. A uniform fill is deliberate: the defect is
// in which SLICE the copy addresses, and a value that also varied within the slice would
// make the assertions depend on the framebuffer row order as well.
Rgba8 SourceColor(int slice) {
return {static_cast<GLubyte>(10 + slice * 20), static_cast<GLubyte>(40 + slice * 10),
static_cast<GLubyte>(200 - slice * 15), 255};
}
Rgba8 DestinationFill(int slice) {
return {static_cast<GLubyte>(3 + slice), static_cast<GLubyte>(250 - slice * 7),
static_cast<GLubyte>(120 + slice * 5), 255};
}
class CopyImageLayeredScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
if (!CopyImageSubDataUsable()) {
GTEST_SKIP() << "glCopyImageSubData is unavailable on backend " << Gl().BackendName();
}
}
void TearDown() override {
if (!Ready()) return;
for (const GLuint texture : m_textures) {
glDeleteTextures(1, &texture);
}
m_textures.clear();
if (m_fbo != 0) {
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glDeleteFramebuffers(1, &m_fbo);
m_fbo = 0;
}
}
// A trivial 1x1x1 array-to-array copy: it exercises the entry point without depending
// on any of the behaviour under test, so a driver (or a backend function table) that
// simply does not have the call skips instead of failing every case below.
bool CopyImageSubDataUsable() {
GLuint probe[2] = {0, 0};
glGenTextures(2, probe);
for (const GLuint texture : probe) {
glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, 1, 1, 1);
}
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
while (glGetError() != GL_NO_ERROR) {
}
glCopyImageSubData(probe[0], GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, probe[1], GL_TEXTURE_2D_ARRAY, 0, 0, 0,
0, 1, 1, 1);
const bool usable = glGetError() == GL_NO_ERROR;
glDeleteTextures(2, probe);
return usable;
}
// `target` is GL_TEXTURE_2D_ARRAY or GL_TEXTURE_3D; both take glTexStorage3D and
// glTexSubImage3D with the slice on the same axis, which is the whole reason GL can
// copy between them. `levels` > 1 puts a real mip chain behind the level the copy
// names, so the level's own extent - a 3D level's depth included - has to be resolved
// rather than assumed to be the image's.
GLuint MakeTexture(GLenum target, int levels, Rgba8 (*colorForSlice)(int)) {
GLuint texture = 0;
glGenTextures(1, &texture);
m_textures.push_back(texture);
glBindTexture(target, texture);
glTexStorage3D(target, levels, GL_RGBA8, kWidth << (levels - 1), kHeight << (levels - 1),
target == GL_TEXTURE_3D ? (kSlices << (levels - 1)) : kSlices);
glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
// Fill every level, so nothing below can pass by reading a level that was never
// written and happened to hold the expected bytes.
for (int level = 0; level < levels; ++level) {
const int levelWidth = kWidth << (levels - 1 - level);
const int levelHeight = kHeight << (levels - 1 - level);
const int levelSlices =
target == GL_TEXTURE_3D ? (kSlices << (levels - 1 - level)) : kSlices;
for (int slice = 0; slice < levelSlices; ++slice) {
const Rgba8 color = colorForSlice(slice % kSlices);
std::vector<Rgba8> texels(static_cast<size_t>(levelWidth) * levelHeight, color);
glTexSubImage3D(target, level, 0, 0, slice, levelWidth, levelHeight, 1, GL_RGBA,
GL_UNSIGNED_BYTE, texels.data());
}
}
glBindTexture(target, 0);
return texture;
}
// One slice of one level, through an FBO attachment. glFramebufferTextureLayer takes an
// array layer and a 3D slice through the same argument, so both targets read back the
// same way.
Rgba8 ReadSlice(GLuint texture, int level, int slice, int width, int height) {
if (m_fbo == 0) {
glGenFramebuffers(1, &m_fbo);
}
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture, level, slice);
EXPECT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE))
<< "slice " << slice << " of level " << level << " is not attachable";
std::vector<Rgba8> pixels(static_cast<size_t>(width) * height, Rgba8{});
glReadBuffer(GL_COLOR_ATTACHMENT0);
glPixelStorei(GL_PACK_ALIGNMENT, 1);
glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// The fill is uniform within a slice, so any disagreement between texels is itself
// a failure - reported here rather than silently reduced to pixels[0].
for (size_t i = 1; i < pixels.size(); ++i) {
EXPECT_TRUE(pixels[i] == pixels[0])
<< "slice " << slice << " of level " << level << " is not uniform: texel 0 is "
<< Describe(pixels[0]) << ", texel " << i << " is " << Describe(pixels[i]);
}
return pixels[0];
}
// The assertion every case ends with: slices inside [dstZ, dstZ + depth) hold the
// source slice they were fed, and every slice outside it still holds its own fill.
void ExpectCopied(GLuint destination, int level, int width, int height, int sliceCount, int srcZ,
int dstZ, int depth, const char* what) {
for (int slice = 0; slice < sliceCount; ++slice) {
const bool inRange = slice >= dstZ && slice < dstZ + depth;
const Rgba8 expected =
inRange ? SourceColor(srcZ + (slice - dstZ)) : DestinationFill(slice);
const Rgba8 actual = ReadSlice(destination, level, slice, width, height);
EXPECT_TRUE(actual == expected)
<< what << ": destination slice " << slice << (inRange ? " (copied)" : " (untouched)")
<< " is " << Describe(actual) << ", expected " << Describe(expected);
}
}
std::vector<GLuint> m_textures;
GLuint m_fbo = 0;
};
// 2d_array -> 2d_array. Both endpoints put the slices on the layer axis, so BOTH layer
// counts carry the depth and extent.depth must stay 1.
TEST_F(CopyImageLayeredScenario, ArrayToArrayCopiesEverySlice) {
if (!Ready() || IsSkipped()) return;
const GLuint source = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, SourceColor);
const GLuint destination = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, DestinationFill);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
glCopyImageSubData(source, GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, destination, GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0,
kWidth, kHeight, kSlices);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
ExpectCopied(destination, 0, kWidth, kHeight, kSlices, 0, 0, kSlices, "array->array, all slices");
}
// The same pair with the layer ranges offset differently on the two sides: the shape that
// separates "copies more than slice 0" from "copies the RIGHT slices". A backend that read
// the source range but wrote from layer 0 (or vice versa) passes the case above.
TEST_F(CopyImageLayeredScenario, ArrayToArrayHonoursDifferentLayerOffsets) {
if (!Ready() || IsSkipped()) return;
const GLuint source = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, SourceColor);
const GLuint destination = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, DestinationFill);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
constexpr int kSrcZ = 3;
constexpr int kDstZ = 1;
constexpr int kDepth = 2;
glCopyImageSubData(source, GL_TEXTURE_2D_ARRAY, 0, 0, 0, kSrcZ, destination, GL_TEXTURE_2D_ARRAY, 0, 0, 0,
kDstZ, kWidth, kHeight, kDepth);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
ExpectCopied(destination, 0, kWidth, kHeight, kSlices, kSrcZ, kDstZ, kDepth,
"array->array, offset layer ranges");
}
// 3d -> 3d. Neither endpoint has array layers at all: the depth travels on extent.depth and
// the offsets on srcOffset.z/dstOffset.z, with both layer counts pinned to 1.
TEST_F(CopyImageLayeredScenario, VolumeToVolumeHonoursNonZeroZ) {
if (!Ready() || IsSkipped()) return;
const GLuint source = MakeTexture(GL_TEXTURE_3D, 1, SourceColor);
const GLuint destination = MakeTexture(GL_TEXTURE_3D, 1, DestinationFill);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
constexpr int kSrcZ = 1;
constexpr int kDstZ = 3;
constexpr int kDepth = 3;
glCopyImageSubData(source, GL_TEXTURE_3D, 0, 0, 0, kSrcZ, destination, GL_TEXTURE_3D, 0, 0, 0, kDstZ,
kWidth, kHeight, kDepth);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
ExpectCopied(destination, 0, kWidth, kHeight, kSlices, kSrcZ, kDstZ, kDepth, "3d->3d, non-zero z");
}
// The same pair one mip level down. A 3D level's DEPTH halves with its width and height, so
// this is the only case where the slice count the copy may name is not the image's own -
// the bound a layered endpoint is checked against has to come from the level.
TEST_F(CopyImageLayeredScenario, VolumeToVolumeAtNonZeroMipLevel) {
if (!Ready() || IsSkipped()) return;
const GLuint source = MakeTexture(GL_TEXTURE_3D, 2, SourceColor);
const GLuint destination = MakeTexture(GL_TEXTURE_3D, 2, DestinationFill);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
constexpr int kLevel = 1;
constexpr int kSrcZ = 2;
constexpr int kDstZ = 0;
constexpr int kDepth = 4;
glCopyImageSubData(source, GL_TEXTURE_3D, kLevel, 0, 0, kSrcZ, destination, GL_TEXTURE_3D, kLevel, 0, 0,
kDstZ, kWidth, kHeight, kDepth);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
ExpectCopied(destination, kLevel, kWidth, kHeight, kSlices, kSrcZ, kDstZ, kDepth,
"3d->3d at mip level 1");
}
// 2d_array -> 3d. The mixed shape: the source counts its slices as layers, the destination
// as depth, and Vulkan requires extent.depth to equal the source's layerCount.
TEST_F(CopyImageLayeredScenario, ArrayToVolumeCopiesEverySlice) {
if (!Ready() || IsSkipped()) return;
const GLuint source = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, SourceColor);
const GLuint destination = MakeTexture(GL_TEXTURE_3D, 1, DestinationFill);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
constexpr int kSrcZ = 2;
constexpr int kDstZ = 1;
constexpr int kDepth = 4;
glCopyImageSubData(source, GL_TEXTURE_2D_ARRAY, 0, 0, 0, kSrcZ, destination, GL_TEXTURE_3D, 0, 0, 0, kDstZ,
kWidth, kHeight, kDepth);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
ExpectCopied(destination, 0, kWidth, kHeight, kSlices, kSrcZ, kDstZ, kDepth, "2d_array->3d");
}
// 3d -> 2d_array, the mirror image: the depth now has to reach the DESTINATION's layerCount
// while the source states it as extent.depth from a z offset.
TEST_F(CopyImageLayeredScenario, VolumeToArrayCopiesEverySlice) {
if (!Ready() || IsSkipped()) return;
const GLuint source = MakeTexture(GL_TEXTURE_3D, 1, SourceColor);
const GLuint destination = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, DestinationFill);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
constexpr int kSrcZ = 1;
constexpr int kDstZ = 2;
constexpr int kDepth = 4;
glCopyImageSubData(source, GL_TEXTURE_3D, 0, 0, 0, kSrcZ, destination, GL_TEXTURE_2D_ARRAY, 0, 0, 0, kDstZ,
kWidth, kHeight, kDepth);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
ExpectCopied(destination, 0, kWidth, kHeight, kSlices, kSrcZ, kDstZ, kDepth, "3d->2d_array");
}
} // namespace
} // namespace MGITest
@@ -0,0 +1,209 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/CopyImageLevelRangeScenario.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// KHR-GL43.copy_image.non_existent_mipmap, and what it cost.
//
// The CTS case is a pure negative test: two 16x16 textures that have level 0 and
// nothing else, and a glCopyImageSubData naming level 1. The answer is
// GL_INVALID_VALUE (GL 4.6 core 18.3.2 / ARB_copy_image: "srcLevel/dstLevel is not
// a valid level"). MobileGL's frontend only checked the level against
// GL_MAX_TEXTURE_SIZE, so level 1 sailed through into the backends, DirectVulkan
// resolved it into a VkImageCopy subresource on a VkImage that was created with
// exactly one mip level, and the Adreno driver dereferenced the level it was
// promised - SIGSEGV inside vkCmdCopyImage, taking the whole glcts process down
// mid-run. A negative case must never do that.
//
// So the level-1-on-a-one-level-texture rejection is the regression proper, and the
// rest of this file is what keeps the fix honest. A validator that answered
// GL_INVALID_VALUE to every level would satisfy the regression tests alone, so the
// scenarios below pin the BOUNDARY rather than the symptom:
//
// * a texture that really does have two levels must accept a copy at level 1,
// * the same texture must still reject level 2,
// * and a plain level-0 copy must move pixels, which is checked by reading the
// destination back rather than by trusting glGetError.
//
// Both backends are covered because the fix is in the shared frontend: DirectGLES
// forwards to the ES glCopyImageSubData (whose own error lands in the ES context,
// not in MobileGL's, so it never reached the application either) and DirectVulkan
// records the copy itself.
#include <array>
#include <cstring>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr GLsizei kSize = 16;
struct Rgba8 {
GLubyte r, g, b, a;
bool operator==(const Rgba8& other) const {
return r == other.r && g == other.g && b == other.b && a == other.a;
}
};
std::vector<Rgba8> SolidImage(GLsizei width, GLsizei height, Rgba8 color) {
return std::vector<Rgba8>(static_cast<std::size_t>(width) * static_cast<std::size_t>(height), color);
}
class CopyImageLevelRangeScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
DrainErrors();
}
void TearDown() override {
if (!Ready()) return;
DeleteTextures();
if (m_fbo != 0) {
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glDeleteFramebuffers(1, &m_fbo);
m_fbo = 0;
}
DrainErrors();
ScenarioTest::TearDown();
}
static void DrainErrors() {
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
}
}
void DeleteTextures() {
if (m_src != 0) glDeleteTextures(1, &m_src);
if (m_dst != 0) glDeleteTextures(1, &m_dst);
m_src = 0;
m_dst = 0;
}
// One 16x16 RGBA8 texture with `levelCount` levels defined through
// glTexImage2D - the same way the CTS case builds its textures, and
// deliberately NOT glTexStorage2D: an immutable allocation would define the
// whole chain up front and could not express "level 1 does not exist".
GLuint MakeTexture(int levelCount, Rgba8 baseColor) {
GLuint texture = 0;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
for (int level = 0; level < levelCount; ++level) {
const GLsizei extent = kSize >> level;
const std::vector<Rgba8> pixels = SolidImage(extent, extent, baseColor);
glTexImage2D(GL_TEXTURE_2D, level, GL_RGBA8, extent, extent, 0, GL_RGBA, GL_UNSIGNED_BYTE,
pixels.data());
}
// What Utils::makeTextureComplete does in the CTS case: the texture is
// complete for the levels it actually has, not for a chain it does not.
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, levelCount - 1);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glBindTexture(GL_TEXTURE_2D, 0);
return texture;
}
void MakePair(int levelCount) {
DeleteTextures();
m_src = MakeTexture(levelCount, Rgba8{11, 22, 33, 255});
m_dst = MakeTexture(levelCount, Rgba8{200, 100, 50, 255});
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "texture setup with " << levelCount << " level(s)";
}
// The call under test, at whatever levels the caller wants, over a 1x1
// region so the region check can never be what rejects it.
GLenum CopyAt(GLint srcLevel, GLint dstLevel, GLsizei extent = 1) {
DrainErrors();
glCopyImageSubData(m_src, GL_TEXTURE_2D, srcLevel, 0, 0, 0, m_dst, GL_TEXTURE_2D, dstLevel, 0, 0, 0,
extent, extent, 1);
const GLenum error = glGetError();
// A second pending error would mean the entry point queued more than one,
// and the extra would be handed out at an unrelated call site later.
EXPECT_EQ(glGetError(), GL_NO_ERROR) << "the copy recorded more than one error";
return error;
}
Rgba8 ReadBackDestinationLevel0() {
if (m_fbo == 0) glGenFramebuffers(1, &m_fbo);
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_dst, 0);
const GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
if (status != GL_FRAMEBUFFER_COMPLETE) {
ADD_FAILURE() << "readback framebuffer incomplete: " << status;
glBindFramebuffer(GL_FRAMEBUFFER, 0);
return Rgba8{0, 0, 0, 0};
}
Rgba8 texel{0, 0, 0, 0};
glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, &texel);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
return texel;
}
GLuint m_src = 0;
GLuint m_dst = 0;
GLuint m_fbo = 0;
};
// The regression. Level 1 of a texture that has only level 0 is not a level, and
// saying so is the whole job: before the fix this reached DirectVulkan, which
// handed mipLevel=1 to vkCmdCopyImage on a one-level VkImage and died inside the
// Adreno driver.
TEST_F(CopyImageLevelRangeScenario, LevelOneOfASingleLevelTextureIsRejected) {
if (!Ready()) GTEST_SKIP();
MakePair(1);
EXPECT_EQ(CopyAt(1, 0), static_cast<GLenum>(GL_INVALID_VALUE)) << "source level 1";
EXPECT_EQ(CopyAt(0, 1), static_cast<GLenum>(GL_INVALID_VALUE)) << "destination level 1";
EXPECT_EQ(CopyAt(1, 1), static_cast<GLenum>(GL_INVALID_VALUE)) << "both levels 1";
}
// The negative control that makes the test above falsifiable: the same level
// index, on textures that genuinely have it, must be accepted. A validator that
// rejected every non-zero level would pass the regression test and fail here.
TEST_F(CopyImageLevelRangeScenario, LevelOneOfATwoLevelTextureIsAccepted) {
if (!Ready()) GTEST_SKIP();
MakePair(2);
EXPECT_EQ(CopyAt(1, 1), static_cast<GLenum>(GL_NO_ERROR));
}
// And the boundary from the other side: two levels means 0 and 1, not 2.
TEST_F(CopyImageLevelRangeScenario, LevelTwoOfATwoLevelTextureIsRejected) {
if (!Ready()) GTEST_SKIP();
MakePair(2);
EXPECT_EQ(CopyAt(2, 0), static_cast<GLenum>(GL_INVALID_VALUE)) << "source level 2";
EXPECT_EQ(CopyAt(0, 2), static_cast<GLenum>(GL_INVALID_VALUE)) << "destination level 2";
}
// Errors alone cannot tell an accepted copy from a silently dropped one, so the
// ordinary case is checked by reading the destination back: the copy has to move
// the source's texel, not merely decline to complain.
TEST_F(CopyImageLevelRangeScenario, AValidLevelZeroCopyStillMovesPixels) {
if (!Ready()) GTEST_SKIP();
MakePair(1);
ASSERT_EQ(ReadBackDestinationLevel0(), (Rgba8{200, 100, 50, 255})) << "destination before the copy";
EXPECT_EQ(CopyAt(0, 0, kSize), static_cast<GLenum>(GL_NO_ERROR));
EXPECT_EQ(ReadBackDestinationLevel0(), (Rgba8{11, 22, 33, 255})) << "destination after the copy";
}
} // namespace
} // namespace MGITest
@@ -0,0 +1,370 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/DepthStencilReadbackAttachmentShapeScenario.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Scenario - DEPTH/STENCIL READBACK WHEN THE ATTACHMENT IS NOT A PLAIN GL_TEXTURE_2D,
// AND THE DEFAULT FRAMEBUFFER'S ADVERTISED DEPTH/STENCIL FORMAT.
//
// Three shipped defects, all of them invisible to a test that only ever attaches a 2D texture
// or only ever asks the default framebuffer for a colour value.
//
// (1) The ES depth/stencil readback emulation identifies the source format by binding the
// attachment's texture NAME to GL_TEXTURE_2D and asking that target for its internal
// format. A name whose target is GL_TEXTURE_2D_ARRAY (attached by
// glFramebufferTextureLayer) makes the bind answer GL_INVALID_OPERATION and change
// nothing - so the query then truthfully describes whatever texture was already on
// GL_TEXTURE_2D, which on that path is the emulation's own staging scratch. A wrong
// answer that looks like a right one: the staging blit is issued between mismatched
// depth formats, ES rejects it, and the read reports nothing at all.
//
// (2) Adreno answers GL_NONE for GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE on an attachment made
// by glFramebufferTexture (a cube map, attached layered) while still reporting its depth
// and stencil bits correctly. The emulation took OBJECT_TYPE as the sole witness for "is
// there an aspect here at all" and declined the whole read.
//
// (3) DirectGLES never told the frontend what its default framebuffer's depth/stencil format
// actually is, so the placeholder from MG_Impl/Init.cpp - GL_DEPTH32F_STENCIL8 - was what
// every attachment query answered, whatever the surface really had. That is not cosmetic:
// GL blits depth/stencil only between IDENTICAL formats, so an application that reads
// GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE, allocates the buffer it was just told about and
// blits gets GL_INVALID_OPERATION - and a rejected glBlitFramebuffer transfers NOTHING,
// colour bits included. DirectVulkan has published its real format since the swapchain
// work; this is the half that was missing.
//
// Every case poisons its destination with a value the correct answer cannot be, so "the
// backend wrote nothing" fails loudly instead of passing on stale memory. The plain
// GL_TEXTURE_2D case at the end is the built-in control: it shares every line of the readback
// path with the array and cube cases, so its passing is what says a failure above is about the
// attachment's SHAPE and not about depth readback in general.
//
// The scenario name starts with DepthStencilReadback on purpose - that is the filter the
// forced-emulation ctest registration uses (MG_IntegrationTest/CMakeLists.txt), and without
// that registration these cases are unfalsifiable on llvmpipe, which accepts the native ES
// depth reads that the Adreno device does not have.
#include <cmath>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr float kDepthPoison = 0.2f;
constexpr int kStencilPoison = 50;
constexpr float kDepthValue = 0.75f;
constexpr int kStencilValue = 7;
constexpr int kSize = 16;
class DepthStencilReadbackAttachmentShapeScenario : public ScenarioTest {
protected:
float ReadDepthAt(int x, int y) const {
float depth = kDepthPoison;
glReadPixels(x, y, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, &depth);
return depth;
}
int ReadStencilAt(int x, int y) const {
int stencil = kStencilPoison;
glReadPixels(x, y, 1, 1, GL_STENCIL_INDEX, GL_INT, &stencil);
return stencil;
}
// Clears the currently bound framebuffer's depth and stencil to the shared
// reference values, with both write masks explicitly open (glClear honours them,
// and a leftover mask from another scenario in this shared context would look
// exactly like the bug under test).
void ClearDepthStencil() const {
glDepthMask(GL_TRUE);
glStencilMask(0xFFu);
glDisable(GL_SCISSOR_TEST);
glClearDepth(kDepthValue);
glClearStencil(kStencilValue);
glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
}
};
// Fails the calling test if the framebuffer bound at both targets is not complete;
// an incomplete framebuffer would make every read below return the poison for a
// reason that has nothing to do with what is being tested.
::testing::AssertionResult FramebufferIsComplete() {
const GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
if (status == GL_FRAMEBUFFER_COMPLETE) return ::testing::AssertionSuccess();
return ::testing::AssertionFailure() << "framebuffer status 0x" << std::hex << status;
}
} // namespace
// (1) A depth slice of a 2D ARRAY texture, attached with glFramebufferTextureLayer.
// Pre-fix this read back the poison: the format probe answered with the staging scratch's
// GL_DEPTH24_STENCIL8 instead of the array's GL_DEPTH_COMPONENT24, and the mismatched
// staging blit was rejected.
TEST_F(DepthStencilReadbackAttachmentShapeScenario, DepthOfAnArrayLayerAttachmentReadsBack) {
if (!Ready()) return;
HeadlessGL& gl = Gl();
GLuint fbo = 0;
GLuint depthArray = 0;
glGenFramebuffers(1, &fbo);
glGenTextures(1, &depthArray);
glBindTexture(GL_TEXTURE_2D_ARRAY, depthArray);
glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_DEPTH_COMPONENT24, kSize, kSize, 4);
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
// Layer 2, not layer 0: a backend that silently reads the wrong slice would still
// agree with a single-layer texture.
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, depthArray, 0, 2);
glDrawBuffer(GL_NONE);
glReadBuffer(GL_NONE);
EXPECT_EQ(FirstGLError(), 0u);
ASSERT_TRUE(FramebufferIsComplete());
glViewport(0, 0, kSize, kSize);
ClearDepthStencil();
const float depth = ReadDepthAt(kSize / 2, kSize / 2);
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_NEAR(depth, kDepthValue, 1.0f / 4096.0f)
<< "glReadPixels(GL_DEPTH_COMPONENT) of a GL_TEXTURE_2D_ARRAY layer attachment returned " << depth
<< (std::fabs(depth - kDepthPoison) < 1e-6f ? " - the destination was never written at all" : "");
BindDefaultFramebuffer();
glDeleteFramebuffers(1, &fbo);
glDeleteTextures(1, &depthArray);
gl.EndFrame();
}
// (2) A depth cube map, attached whole with glFramebufferTexture - a LAYERED attachment.
// Pre-fix the emulation declined outright, because the driver reports GL_NONE for that
// attachment's OBJECT_TYPE.
TEST_F(DepthStencilReadbackAttachmentShapeScenario, DepthOfALayeredCubeAttachmentReadsBack) {
if (!Ready()) return;
HeadlessGL& gl = Gl();
GLuint fbo = 0;
GLuint depthCube = 0;
glGenFramebuffers(1, &fbo);
glGenTextures(1, &depthCube);
glBindTexture(GL_TEXTURE_CUBE_MAP, depthCube);
glTexStorage2D(GL_TEXTURE_CUBE_MAP, 1, GL_DEPTH_COMPONENT24, kSize, kSize);
glBindTexture(GL_TEXTURE_CUBE_MAP, 0);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, depthCube, 0);
glDrawBuffer(GL_NONE);
glReadBuffer(GL_NONE);
EXPECT_EQ(FirstGLError(), 0u);
ASSERT_TRUE(FramebufferIsComplete());
glViewport(0, 0, kSize, kSize);
ClearDepthStencil();
const float depth = ReadDepthAt(kSize / 2, kSize / 2);
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_NEAR(depth, kDepthValue, 1.0f / 4096.0f)
<< "glReadPixels(GL_DEPTH_COMPONENT) of a layered GL_TEXTURE_CUBE_MAP attachment returned " << depth
<< (std::fabs(depth - kDepthPoison) < 1e-6f ? " - the destination was never written at all" : "");
BindDefaultFramebuffer();
glDeleteFramebuffers(1, &fbo);
glDeleteTextures(1, &depthCube);
gl.EndFrame();
}
// Both aspects of a packed array attachment. The stencil half goes through a different
// sampling mode than the depth half, and only the depth half was covered above.
TEST_F(DepthStencilReadbackAttachmentShapeScenario, PackedArrayLayerAttachmentReadsBackBothAspects) {
if (!Ready()) return;
HeadlessGL& gl = Gl();
GLuint fbo = 0;
GLuint packedArray = 0;
glGenFramebuffers(1, &fbo);
glGenTextures(1, &packedArray);
glBindTexture(GL_TEXTURE_2D_ARRAY, packedArray);
glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_DEPTH24_STENCIL8, kSize, kSize, 3);
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, packedArray, 0, 1);
glDrawBuffer(GL_NONE);
glReadBuffer(GL_NONE);
EXPECT_EQ(FirstGLError(), 0u);
ASSERT_TRUE(FramebufferIsComplete());
glViewport(0, 0, kSize, kSize);
ClearDepthStencil();
const float depth = ReadDepthAt(kSize / 2, kSize / 2);
const int stencil = ReadStencilAt(kSize / 2, kSize / 2);
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_NEAR(depth, kDepthValue, 1.0f / 4096.0f)
<< "depth of a packed GL_TEXTURE_2D_ARRAY layer attachment returned " << depth;
EXPECT_EQ(stencil, kStencilValue)
<< "stencil of a packed GL_TEXTURE_2D_ARRAY layer attachment returned " << stencil
<< (stencil == kStencilPoison ? " - the destination was never written at all" : "");
BindDefaultFramebuffer();
glDeleteFramebuffers(1, &fbo);
glDeleteTextures(1, &packedArray);
gl.EndFrame();
}
// The control: the plain GL_TEXTURE_2D shape, which always worked. If this one ever fails
// alongside the three above, the fault is in depth readback generally rather than in how
// the attachment's format and presence are discovered.
TEST_F(DepthStencilReadbackAttachmentShapeScenario, DepthOfAPlainTexture2DAttachmentReadsBack) {
if (!Ready()) return;
HeadlessGL& gl = Gl();
GLuint fbo = 0;
GLuint depthTex = 0;
glGenFramebuffers(1, &fbo);
glGenTextures(1, &depthTex);
glBindTexture(GL_TEXTURE_2D, depthTex);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_DEPTH_COMPONENT24, kSize, kSize);
glBindTexture(GL_TEXTURE_2D, 0);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthTex, 0);
glDrawBuffer(GL_NONE);
glReadBuffer(GL_NONE);
EXPECT_EQ(FirstGLError(), 0u);
ASSERT_TRUE(FramebufferIsComplete());
glViewport(0, 0, kSize, kSize);
ClearDepthStencil();
const float depth = ReadDepthAt(kSize / 2, kSize / 2);
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_NEAR(depth, kDepthValue, 1.0f / 4096.0f)
<< "the control case failed: even a plain GL_TEXTURE_2D depth attachment read back " << depth;
BindDefaultFramebuffer();
glDeleteFramebuffers(1, &fbo);
glDeleteTextures(1, &depthTex);
gl.EndFrame();
}
// (3) The default framebuffer must describe its depth/stencil truthfully enough that a
// buffer allocated from that description is blit-compatible with it. This is the exact
// sequence KHR-GLxx.framebuffer_blit performs, and the exact reason 22 of its cases died
// on DirectGLES: the frontend answered 32-bit float depth for a 24-bit fixed-point
// surface, so the renderbuffer the caller allocated could never be blitted to.
TEST_F(DepthStencilReadbackAttachmentShapeScenario, DefaultFramebufferDepthStencilFormatIsBlitCompatible) {
if (!Ready()) return;
HeadlessGL& gl = Gl();
const int width = gl.Width();
const int height = gl.Height();
BindDefaultFramebuffer();
GLint depthBits = 0;
GLint stencilBits = 0;
GLint componentType = GL_UNSIGNED_NORMALIZED;
glGetFramebufferAttachmentParameteriv(GL_DRAW_FRAMEBUFFER, GL_DEPTH,
GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE, &depthBits);
glGetFramebufferAttachmentParameteriv(GL_DRAW_FRAMEBUFFER, GL_STENCIL,
GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE, &stencilBits);
glGetFramebufferAttachmentParameteriv(GL_DRAW_FRAMEBUFFER, GL_DEPTH,
GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE, &componentType);
EXPECT_EQ(FirstGLError(), 0u);
if (depthBits <= 0 || stencilBits <= 0) {
GTEST_SKIP() << "this surface has no packed depth/stencil (depth=" << depthBits
<< " stencil=" << stencilBits << "); the blit-compatibility contract needs both";
}
// The one sized format the reported description names. Getting here with the wrong
// answer is the bug: the two candidates are not interchangeable for a blit.
const GLenum reported = (componentType == GL_FLOAT || depthBits > 24) ? GL_DEPTH32F_STENCIL8
: GL_DEPTH24_STENCIL8;
GLuint fbo = 0;
GLuint colorRbo = 0;
GLuint depthRbo = 0;
glGenFramebuffers(1, &fbo);
glGenRenderbuffers(1, &colorRbo);
glGenRenderbuffers(1, &depthRbo);
glBindRenderbuffer(GL_RENDERBUFFER, colorRbo);
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, width, height);
glBindRenderbuffer(GL_RENDERBUFFER, depthRbo);
glRenderbufferStorage(GL_RENDERBUFFER, reported, width, height);
glBindRenderbuffer(GL_RENDERBUFFER, 0);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, colorRbo);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, depthRbo);
EXPECT_EQ(FirstGLError(), 0u);
ASSERT_TRUE(FramebufferIsComplete());
// Put a known depth in the default framebuffer, then blit colour+depth+stencil out of
// it into the buffer that its own description asked for.
BindDefaultFramebuffer();
glViewport(0, 0, width, height);
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
glClearColor(0.0f, 1.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
ClearDepthStencil();
EXPECT_EQ(FirstGLError(), 0u);
glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo);
glBlitFramebuffer(0, 0, width, height, 0, 0, width, height,
GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT, GL_NEAREST);
EXPECT_EQ(FirstGLError(), 0u)
<< "blitting depth/stencil out of the default framebuffer into a buffer allocated from the format "
"the default framebuffer itself reported was rejected - the report and the storage disagree";
glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
unsigned char color[4] = {0, 0, 0, 0};
glReadPixels(width / 2, height / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, color);
const float depth = ReadDepthAt(width / 2, height / 2);
const int stencil = ReadStencilAt(width / 2, height / 2);
EXPECT_EQ(FirstGLError(), 0u);
// The colour bit is the precondition, not the claim: it says this stack can blit out of
// its default framebuffer at all, which has nothing to do with depth/stencil formats.
// DirectVulkan on a surfaceless pbuffer cannot - the whole call, colour included, is a
// no-op there, while the same blit works on a real surface (KHR-GLxx.framebuffer_blit
// exercises exactly it and Magma passes 33/33 on device). Skipping keeps the
// depth/stencil claim below falsifiable instead of drowning it in an unrelated
// harness limitation.
if (int(color[1]) <= 192) {
// GTEST_SKIP() expands to a return, so the teardown below it would never run and this
// scenario would hand the next one a foreign framebuffer plus three leaked objects -
// and this is the path DirectVulkan takes on every headless run, not a rare one.
BindDefaultFramebuffer();
glDeleteFramebuffers(1, &fbo);
glDeleteRenderbuffers(1, &colorRbo);
glDeleteRenderbuffers(1, &depthRbo);
gl.EndFrame();
GTEST_SKIP() << "backend " << gl.BackendName() << " on this surface transferred no colour either (green="
<< int(color[1])
<< "): it cannot blit out of the default framebuffer here, so the depth/stencil half proves "
"nothing. The GL-error assertion above still ran, and it is the format contract";
}
EXPECT_NEAR(depth, kDepthValue, 1.0f / 4096.0f)
<< "depth blitted out of the default framebuffer read back " << depth
<< (std::fabs(depth - kDepthPoison) < 1e-6f ? " - the blit transferred nothing" : "");
EXPECT_EQ(stencil, kStencilValue) << "stencil blitted out of the default framebuffer read back " << stencil;
BindDefaultFramebuffer();
glDeleteFramebuffers(1, &fbo);
glDeleteRenderbuffers(1, &colorRbo);
glDeleteRenderbuffers(1, &depthRbo);
gl.EndFrame();
}
} // namespace MGITest
@@ -0,0 +1,784 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/DepthStencilReadbackMatrixScenario.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Scenario - THE DEPTH/STENCIL READBACK MATRIX: every verb, every source kind.
//
// DepthStencilReadbackScenario pins the default framebuffer. This file pins the rest of
// the surface a depth/stencil read has to cover, because the three verbs and the four
// source kinds do NOT share a code path by accident - they share one on purpose, and a
// change that quietly serves only one of them is exactly what these assertions catch:
//
// verbs glReadPixels(GL_DEPTH_COMPONENT | GL_STENCIL_INDEX | GL_DEPTH_STENCIL),
// glGetTexImage(GL_DEPTH_STENCIL), glCopyTexImage2D followed by a read
// source kinds depth(-stencil) TEXTURE, RENDERBUFFER (not samplable at all),
// MULTISAMPLE renderbuffer (needs a resolve first), default framebuffer
// formats DEPTH24_STENCIL8, DEPTH32F_STENCIL8, DEPTH_COMPONENT16/24/32F,
// STENCIL_INDEX8
// client types GL_FLOAT / GL_UNSIGNED_INT / GL_UNSIGNED_SHORT depth, GL_INT /
// GL_UNSIGNED_BYTE stencil, both packed GL_DEPTH_STENCIL layouts
//
// On DirectGLES none of this exists natively - ES has no depth or stencil readback in
// core - so every assertion here is really an assertion about the shader-sampling
// emulation. The catch is that some ES drivers accept the reads anyway (Mesa does,
// Adreno does not), which would make the emulation dead code on the very stack the
// headless suite runs on. That is what the second ctest registration is for: the same
// scenarios run again with MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION=1, which takes the
// native spellings off the table and leaves only the path the device actually uses.
//
// Every destination is poisoned with a value the correct answer cannot be, so "the
// backend wrote nothing" fails loudly instead of passing on a coincidence - a test that
// only checked "no GL error" would pass against a readback that never touched the buffer,
// which is precisely how this whole cluster hid for so long.
#include <cmath>
#include <cstring>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr float kDepthPoison = 0.2f;
constexpr int kStencilPoison = 50;
constexpr int kWidth = 64;
constexpr int kHeight = 48;
// A depth-stencil pair no clear in these tests produces, packed both ways.
constexpr unsigned int kPacked24_8Poison = 0xAAAAAA33u;
struct D32fS8 {
float depth;
unsigned int stencil;
};
// Everything a source needs to be read: the framebuffer to bind, plus the objects
// to delete afterwards.
struct DepthSource {
GLuint fbo = 0;
GLuint colorTexture = 0;
GLuint depthTexture = 0;
GLuint depthRenderbuffer = 0;
GLuint colorRenderbuffer = 0;
};
void DestroySource(DepthSource& source) {
if (source.fbo != 0) glDeleteFramebuffers(1, &source.fbo);
if (source.colorTexture != 0) glDeleteTextures(1, &source.colorTexture);
if (source.depthTexture != 0) glDeleteTextures(1, &source.depthTexture);
if (source.depthRenderbuffer != 0) glDeleteRenderbuffers(1, &source.depthRenderbuffer);
if (source.colorRenderbuffer != 0) glDeleteRenderbuffers(1, &source.colorRenderbuffer);
source = DepthSource{};
}
GLenum AttachmentPointFor(GLenum internalFormat) {
switch (internalFormat) {
case GL_DEPTH24_STENCIL8:
case GL_DEPTH32F_STENCIL8: return GL_DEPTH_STENCIL_ATTACHMENT;
case GL_STENCIL_INDEX8: return GL_STENCIL_ATTACHMENT;
default: return GL_DEPTH_ATTACHMENT;
}
}
bool FormatHasDepth(GLenum internalFormat) { return internalFormat != GL_STENCIL_INDEX8; }
bool FormatHasStencil(GLenum internalFormat) {
return internalFormat == GL_DEPTH24_STENCIL8 || internalFormat == GL_DEPTH32F_STENCIL8 ||
internalFormat == GL_STENCIL_INDEX8;
}
// A framebuffer whose depth/stencil lives in a TEXTURE. The colour attachment is
// there so a stencil-only or depth-only framebuffer still has something to size it.
DepthSource MakeTextureSource(GLenum internalFormat) {
DepthSource source;
glGenFramebuffers(1, &source.fbo);
glBindFramebuffer(GL_FRAMEBUFFER, source.fbo);
glGenTextures(1, &source.colorTexture);
glBindTexture(GL_TEXTURE_2D, source.colorTexture);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kWidth, kHeight);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, source.colorTexture, 0);
glGenTextures(1, &source.depthTexture);
glBindTexture(GL_TEXTURE_2D, source.depthTexture);
glTexStorage2D(GL_TEXTURE_2D, 1, internalFormat, kWidth, kHeight);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glFramebufferTexture2D(GL_FRAMEBUFFER, AttachmentPointFor(internalFormat), GL_TEXTURE_2D,
source.depthTexture, 0);
return source;
}
// The same, with the depth/stencil in a RENDERBUFFER - which cannot be sampled at
// all, so the readback has no choice but to copy it somewhere samplable first.
// `samples` > 0 makes it multisample, which additionally needs a resolve.
DepthSource MakeRenderbufferSource(GLenum internalFormat, int samples) {
DepthSource source;
glGenFramebuffers(1, &source.fbo);
glBindFramebuffer(GL_FRAMEBUFFER, source.fbo);
glGenRenderbuffers(1, &source.colorRenderbuffer);
glBindRenderbuffer(GL_RENDERBUFFER, source.colorRenderbuffer);
if (samples > 0) {
glRenderbufferStorageMultisample(GL_RENDERBUFFER, samples, GL_RGBA8, kWidth, kHeight);
} else {
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, kWidth, kHeight);
}
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, source.colorRenderbuffer);
glGenRenderbuffers(1, &source.depthRenderbuffer);
glBindRenderbuffer(GL_RENDERBUFFER, source.depthRenderbuffer);
if (samples > 0) {
glRenderbufferStorageMultisample(GL_RENDERBUFFER, samples, internalFormat, kWidth, kHeight);
} else {
glRenderbufferStorage(GL_RENDERBUFFER, internalFormat, kWidth, kHeight);
}
glFramebufferRenderbuffer(GL_FRAMEBUFFER, AttachmentPointFor(internalFormat), GL_RENDERBUFFER,
source.depthRenderbuffer);
glBindRenderbuffer(GL_RENDERBUFFER, 0);
return source;
}
// Clears the bound framebuffer's depth and stencil to known values, with the masks
// and the scissor explicitly out of the way (a leaked scissor from an earlier
// scenario would clip the clear and every assertion after it).
void ClearDepthStencil(GLenum internalFormat, float depth, int stencil) {
glDisable(GL_SCISSOR_TEST);
glViewport(0, 0, kWidth, kHeight);
GLbitfield mask = 0;
if (FormatHasDepth(internalFormat)) {
glDepthMask(GL_TRUE);
glClearDepth(depth);
mask |= GL_DEPTH_BUFFER_BIT;
}
if (FormatHasStencil(internalFormat)) {
glStencilMask(0xFFu);
glClearStencil(stencil);
mask |= GL_STENCIL_BUFFER_BIT;
}
glClear(mask);
}
class DepthStencilReadbackMatrixScenario : public ScenarioTest {
protected:
// Not every ES driver can render to every depth format (DEPTH_COMPONENT32F and
// the multisample counts in particular), and an incomplete framebuffer would
// turn a legitimate "this machine cannot host the source" into a spurious
// failure about the readback.
static bool SourceIsUsable() {
return glCheckFramebufferStatus(GL_FRAMEBUFFER) == GLenum(GL_FRAMEBUFFER_COMPLETE);
}
static std::vector<float> ReadDepthFloat(int x, int y, int width, int height) {
std::vector<float> depth(static_cast<size_t>(width) * height, kDepthPoison);
glReadPixels(x, y, width, height, GL_DEPTH_COMPONENT, GL_FLOAT, depth.data());
return depth;
}
static std::vector<int> ReadStencilInt(int x, int y, int width, int height) {
std::vector<int> stencil(static_cast<size_t>(width) * height, kStencilPoison);
glReadPixels(x, y, width, height, GL_STENCIL_INDEX, GL_INT, stencil.data());
return stencil;
}
// "every value in the region is `expected`" rather than "the middle pixel is":
// a staging blit that lands the wrong rectangle, or a conversion pass with a
// half-texel offset, still gets the centre right.
static void ExpectAllDepth(const std::vector<float>& values, float expected, const char* what) {
size_t bad = 0;
float worst = expected;
for (float value : values) {
if (std::fabs(value - expected) > 1.0f / 4096.0f) {
if (bad == 0) worst = value;
++bad;
}
}
EXPECT_EQ(bad, 0u) << what << ": " << bad << " of " << values.size()
<< " depth values differ from " << expected << "; first bad value " << worst
<< (std::fabs(worst - kDepthPoison) < 1e-6f
? " - which is the poison value, so nothing was written at all"
: "");
}
static void ExpectAllStencil(const std::vector<int>& values, int expected, const char* what) {
size_t bad = 0;
int worst = expected;
for (int value : values) {
if (value != expected) {
if (bad == 0) worst = value;
++bad;
}
}
EXPECT_EQ(bad, 0u) << what << ": " << bad << " of " << values.size()
<< " stencil values differ from " << expected << "; first bad value " << worst
<< (worst == kStencilPoison
? " - which is the poison value, so nothing was written at all"
: "");
}
};
// ---- glReadPixels across the source kinds -----------------------------------
struct SourceCase {
const char* name;
GLenum internalFormat;
int samples;
bool renderbuffer;
};
const SourceCase kSourceCases[] = {
{"texture depth24_stencil8", GL_DEPTH24_STENCIL8, 0, false},
{"texture depth32f_stencil8", GL_DEPTH32F_STENCIL8, 0, false},
{"texture depth_component16", GL_DEPTH_COMPONENT16, 0, false},
{"texture depth_component24", GL_DEPTH_COMPONENT24, 0, false},
{"texture depth_component32f", GL_DEPTH_COMPONENT32F, 0, false},
{"renderbuffer depth24_stencil8", GL_DEPTH24_STENCIL8, 0, true},
{"renderbuffer depth_component24", GL_DEPTH_COMPONENT24, 0, true},
{"renderbuffer stencil_index8", GL_STENCIL_INDEX8, 0, true},
};
} // namespace
TEST_F(DepthStencilReadbackMatrixScenario, EverySourceKindReadsItsClearBack) {
if (!Ready()) return;
int exercised = 0;
for (const SourceCase& testCase : kSourceCases) {
SCOPED_TRACE(testCase.name);
DepthSource source = testCase.renderbuffer
? MakeRenderbufferSource(testCase.internalFormat, testCase.samples)
: MakeTextureSource(testCase.internalFormat);
if (!SourceIsUsable()) {
DestroySource(source);
continue;
}
FirstGLError(); // the storage calls above may have probed an unsupported combination
ClearDepthStencil(testCase.internalFormat, 0.625f, 9);
EXPECT_EQ(FirstGLError(), 0u) << "clearing the source";
if (FormatHasDepth(testCase.internalFormat)) {
const std::vector<float> depth = ReadDepthFloat(0, 0, kWidth, kHeight);
EXPECT_EQ(FirstGLError(), 0u) << "glReadPixels(GL_DEPTH_COMPONENT, GL_FLOAT)";
ExpectAllDepth(depth, 0.625f, testCase.name);
}
if (FormatHasStencil(testCase.internalFormat)) {
const std::vector<int> stencil = ReadStencilInt(0, 0, kWidth, kHeight);
EXPECT_EQ(FirstGLError(), 0u) << "glReadPixels(GL_STENCIL_INDEX, GL_INT)";
ExpectAllStencil(stencil, 9, testCase.name);
}
++exercised;
DestroySource(source);
}
// A machine that hosted none of the sources would report a vacuous pass.
EXPECT_GE(exercised, 4) << "too few depth/stencil source kinds were usable to call this a matrix";
glBindFramebuffer(GL_FRAMEBUFFER, 0);
Gl().EndFrame();
}
// Depth and stencil in two SEPARATE objects, with two different formats, on the same
// framebuffer. Legal GL, and the shape KHR-GL3x.framebuffer_blit builds when its depth
// config and its stencil config are configured independently - so a readback that
// describes "the" depth/stencil source as one thing serves whichever aspect it happened
// to find first and silently abandons the other. Each aspect has to be staged from its
// own attachment, in its own format.
TEST_F(DepthStencilReadbackMatrixScenario, SeparateDepthAndStencilAttachmentsAreBothReadable) {
if (!Ready()) return;
DepthSource source;
glGenFramebuffers(1, &source.fbo);
glBindFramebuffer(GL_FRAMEBUFFER, source.fbo);
glGenRenderbuffers(1, &source.colorRenderbuffer);
glBindRenderbuffer(GL_RENDERBUFFER, source.colorRenderbuffer);
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, kWidth, kHeight);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, source.colorRenderbuffer);
// Depth in a DEPTH_COMPONENT24 renderbuffer...
glGenRenderbuffers(1, &source.depthRenderbuffer);
glBindRenderbuffer(GL_RENDERBUFFER, source.depthRenderbuffer);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, kWidth, kHeight);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, source.depthRenderbuffer);
// ...and stencil in a STENCIL_INDEX8 one of its own.
GLuint stencilRenderbuffer = 0;
glGenRenderbuffers(1, &stencilRenderbuffer);
glBindRenderbuffer(GL_RENDERBUFFER, stencilRenderbuffer);
glRenderbufferStorage(GL_RENDERBUFFER, GL_STENCIL_INDEX8, kWidth, kHeight);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER, stencilRenderbuffer);
glBindRenderbuffer(GL_RENDERBUFFER, 0);
if (!SourceIsUsable()) {
// Separate depth and stencil images are legal GL but many stacks answer
// GL_FRAMEBUFFER_UNSUPPORTED for them; say which, so a skip here is a fact about
// the driver rather than an unexplained hole in the matrix.
const GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
glDeleteRenderbuffers(1, &stencilRenderbuffer);
DestroySource(source);
GTEST_SKIP() << "this driver cannot host separate DEPTH_COMPONENT24 and STENCIL_INDEX8 attachments: "
<< "glCheckFramebufferStatus = 0x" << std::hex << status;
}
FirstGLError();
glDisable(GL_SCISSOR_TEST);
glViewport(0, 0, kWidth, kHeight);
glDepthMask(GL_TRUE);
glStencilMask(0xFFu);
glClearDepth(0.3125);
glClearStencil(17);
glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
ASSERT_EQ(FirstGLError(), 0u);
const std::vector<float> depth = ReadDepthFloat(0, 0, kWidth, kHeight);
EXPECT_EQ(FirstGLError(), 0u) << "reading depth from a separately-attached DEPTH_COMPONENT24";
ExpectAllDepth(depth, 0.3125f, "separate depth attachment");
const std::vector<int> stencil = ReadStencilInt(0, 0, kWidth, kHeight);
EXPECT_EQ(FirstGLError(), 0u) << "reading stencil from a separately-attached STENCIL_INDEX8";
ExpectAllStencil(stencil, 17, "separate stencil attachment");
glDeleteRenderbuffers(1, &stencilRenderbuffer);
DestroySource(source);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
Gl().EndFrame();
}
// A multisample source is never read directly - glReadPixels on a multisampled
// framebuffer is INVALID_OPERATION in GL as much as in ES, and the state layer says so.
// The way multisample depth reaches a reader is a resolve blit into a single-sampled
// framebuffer, which is then read; that pair is
// KHR-GL3x.framebuffer_blit.multisampled_to_singlesampled_blit_depth_config_test, and
// the assertion here is that the resolved depth arrives intact rather than as the
// destination's own clear value.
TEST_F(DepthStencilReadbackMatrixScenario, AResolvedMultisampleDepthReadsBackFromTheDestination) {
if (!Ready()) return;
DepthSource multisampled = MakeRenderbufferSource(GL_DEPTH24_STENCIL8, 4);
if (!SourceIsUsable()) {
DestroySource(multisampled);
GTEST_SKIP() << "this driver cannot host a 4x multisample DEPTH24_STENCIL8 renderbuffer";
}
FirstGLError();
ClearDepthStencil(GL_DEPTH24_STENCIL8, 0.875f, 0);
ASSERT_EQ(FirstGLError(), 0u);
// The destination starts at a depth the resolve must overwrite everywhere.
DepthSource resolved = MakeTextureSource(GL_DEPTH24_STENCIL8);
ASSERT_TRUE(SourceIsUsable());
ClearDepthStencil(GL_DEPTH24_STENCIL8, 0.125f, 0);
ASSERT_EQ(FirstGLError(), 0u);
glBindFramebuffer(GL_READ_FRAMEBUFFER, multisampled.fbo);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, resolved.fbo);
glDisable(GL_SCISSOR_TEST);
glBlitFramebuffer(0, 0, kWidth, kHeight, 0, 0, kWidth, kHeight, GL_DEPTH_BUFFER_BIT, GL_NEAREST);
EXPECT_EQ(FirstGLError(), 0u) << "resolving a multisample depth buffer into a single-sampled one";
glBindFramebuffer(GL_FRAMEBUFFER, resolved.fbo);
const std::vector<float> depth = ReadDepthFloat(0, 0, kWidth, kHeight);
EXPECT_EQ(FirstGLError(), 0u);
ExpectAllDepth(depth, 0.875f, "resolved multisample depth");
DestroySource(resolved);
DestroySource(multisampled);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
Gl().EndFrame();
}
// A read whose rectangle is NOT the whole attachment. The staging copy has to carry
// the requested rect (not the origin) and hand back its rows bottom-up, which a
// full-extent uniform read is a fixed point of and therefore cannot see.
TEST_F(DepthStencilReadbackMatrixScenario, ASubRectangleReadsTheRightBandInTheRightOrder) {
if (!Ready()) return;
DepthSource source = MakeTextureSource(GL_DEPTH24_STENCIL8);
ASSERT_TRUE(SourceIsUsable());
// Bottom half 0.25, top half 0.75, and the stencil banded the other way round so a
// mix-up between the two aspects cannot pass either.
glDisable(GL_SCISSOR_TEST);
glViewport(0, 0, kWidth, kHeight);
glDepthMask(GL_TRUE);
glStencilMask(0xFFu);
glEnable(GL_SCISSOR_TEST);
glScissor(0, 0, kWidth, kHeight / 2);
glClearDepth(0.25);
glClearStencil(11);
glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
glScissor(0, kHeight / 2, kWidth, kHeight - kHeight / 2);
glClearDepth(0.75);
glClearStencil(22);
glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
glDisable(GL_SCISSOR_TEST);
ASSERT_EQ(FirstGLError(), 0u);
// A rect wholly inside the bottom band, offset from the origin in both axes.
const int rectWidth = 8;
const int rectHeight = 4;
const std::vector<float> bottom = ReadDepthFloat(16, 4, rectWidth, rectHeight);
EXPECT_EQ(FirstGLError(), 0u);
ExpectAllDepth(bottom, 0.25f, "sub-rect inside the bottom depth band");
const std::vector<int> bottomStencil = ReadStencilInt(16, 4, rectWidth, rectHeight);
EXPECT_EQ(FirstGLError(), 0u);
ExpectAllStencil(bottomStencil, 11, "sub-rect inside the bottom stencil band");
// And one wholly inside the top band. Reading the mirrored row would answer 0.25.
const std::vector<float> top = ReadDepthFloat(16, kHeight - 4 - rectHeight, rectWidth, rectHeight);
EXPECT_EQ(FirstGLError(), 0u);
ExpectAllDepth(top, 0.75f, "sub-rect inside the top depth band");
// A rect that STRADDLES the boundary pins the row order itself: its first rows must
// be the bottom band and its last rows the top one.
const int straddleHeight = 8;
const std::vector<float> straddle =
ReadDepthFloat(16, kHeight / 2 - straddleHeight / 2, rectWidth, straddleHeight);
EXPECT_EQ(FirstGLError(), 0u);
ASSERT_EQ(straddle.size(), static_cast<size_t>(rectWidth) * straddleHeight);
EXPECT_NEAR(straddle[0], 0.25f, 1.0f / 4096.0f)
<< "the first row of the returned rect must be its BOTTOM row (GL order), which is in the 0.25 band";
EXPECT_NEAR(straddle[straddle.size() - 1], 0.75f, 1.0f / 4096.0f)
<< "the last row of the returned rect must be its TOP row, which is in the 0.75 band";
DestroySource(source);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
Gl().EndFrame();
}
// The packed layouts the packed_depth_stencil family reads its gradients with.
TEST_F(DepthStencilReadbackMatrixScenario, PackedDepthStencilReadPixelsCarriesBothAspects) {
if (!Ready()) return;
struct PackedCase {
const char* name;
GLenum internalFormat;
GLenum type;
};
const PackedCase cases[] = {
{"depth24_stencil8 / GL_UNSIGNED_INT_24_8", GL_DEPTH24_STENCIL8, GL_UNSIGNED_INT_24_8},
{"depth32f_stencil8 / GL_FLOAT_32_UNSIGNED_INT_24_8_REV", GL_DEPTH32F_STENCIL8,
GL_FLOAT_32_UNSIGNED_INT_24_8_REV},
};
int exercised = 0;
for (const PackedCase& testCase : cases) {
SCOPED_TRACE(testCase.name);
DepthSource source = MakeTextureSource(testCase.internalFormat);
if (!SourceIsUsable()) {
DestroySource(source);
continue;
}
FirstGLError();
ClearDepthStencil(testCase.internalFormat, 0.5f, 3);
ASSERT_EQ(FirstGLError(), 0u);
const size_t pixels = static_cast<size_t>(kWidth) * kHeight;
if (testCase.type == GL_UNSIGNED_INT_24_8) {
std::vector<unsigned int> packed(pixels, kPacked24_8Poison);
glReadPixels(0, 0, kWidth, kHeight, GL_DEPTH_STENCIL, testCase.type, packed.data());
EXPECT_EQ(FirstGLError(), 0u);
size_t bad = 0;
for (unsigned int value : packed) {
const float depth = static_cast<float>(value >> 8) / 16777215.0f;
const int stencil = static_cast<int>(value & 0xFFu);
if (std::fabs(depth - 0.5f) > 0.01f || stencil != 3) ++bad;
}
EXPECT_EQ(bad, 0u) << testCase.name << ": " << bad << " of " << pixels
<< " packed words carry the wrong depth or stencil (first word 0x" << std::hex
<< packed[0] << std::dec << ")";
} else {
std::vector<D32fS8> packed(pixels, D32fS8{kDepthPoison, static_cast<unsigned int>(kStencilPoison)});
glReadPixels(0, 0, kWidth, kHeight, GL_DEPTH_STENCIL, testCase.type, packed.data());
EXPECT_EQ(FirstGLError(), 0u);
size_t bad = 0;
for (const D32fS8& value : packed) {
if (std::fabs(value.depth - 0.5f) > 0.01f || (value.stencil & 0xFFu) != 3u) ++bad;
}
EXPECT_EQ(bad, 0u) << testCase.name << ": " << bad << " of " << pixels
<< " packed pairs carry the wrong depth or stencil (first pair depth "
<< packed[0].depth << " stencil " << (packed[0].stencil & 0xFFu) << ")";
}
++exercised;
DestroySource(source);
}
EXPECT_GE(exercised, 1) << "neither packed depth/stencil format was renderable";
glBindFramebuffer(GL_FRAMEBUFFER, 0);
Gl().EndFrame();
}
// glGetTexImage reads a TEXTURE, not the bound framebuffer - a different entry point
// that has to reach the same machinery. This is verify_get_tex_image's shape.
TEST_F(DepthStencilReadbackMatrixScenario, GetTexImageReadsAPackedDepthStencilTexture) {
if (!Ready()) return;
DepthSource source = MakeTextureSource(GL_DEPTH24_STENCIL8);
ASSERT_TRUE(SourceIsUsable());
FirstGLError();
ClearDepthStencil(GL_DEPTH24_STENCIL8, 0.375f, 5);
ASSERT_EQ(FirstGLError(), 0u);
// Read it back through the texture, with the framebuffer that owns it unbound so a
// path that secretly read the framebuffer instead would answer from somewhere else.
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glBindTexture(GL_TEXTURE_2D, source.depthTexture);
const size_t pixels = static_cast<size_t>(kWidth) * kHeight;
std::vector<unsigned int> packed(pixels, kPacked24_8Poison);
glGetTexImage(GL_TEXTURE_2D, 0, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, packed.data());
EXPECT_EQ(FirstGLError(), 0u);
size_t bad = 0;
for (unsigned int value : packed) {
const float depth = static_cast<float>(value >> 8) / 16777215.0f;
if (std::fabs(depth - 0.375f) > 0.01f || (value & 0xFFu) != 5u) ++bad;
}
EXPECT_EQ(bad, 0u) << bad << " of " << pixels
<< " words from glGetTexImage(GL_DEPTH_STENCIL) are wrong (first word 0x" << std::hex
<< packed[0] << std::dec << ")";
glBindTexture(GL_TEXTURE_2D, 0);
DestroySource(source);
Gl().EndFrame();
}
// glCopyTexImage2D out of a depth attachment, then read the copy - verify_copy_tex_image.
TEST_F(DepthStencilReadbackMatrixScenario, CopyTexImageFromADepthAttachmentSurvivesAReadBack) {
if (!Ready()) return;
DepthSource source = MakeTextureSource(GL_DEPTH24_STENCIL8);
ASSERT_TRUE(SourceIsUsable());
FirstGLError();
ClearDepthStencil(GL_DEPTH24_STENCIL8, 0.75f, 6);
ASSERT_EQ(FirstGLError(), 0u);
GLuint copy = 0;
glGenTextures(1, &copy);
glBindTexture(GL_TEXTURE_2D, copy);
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, kWidth, kHeight, 0, GL_DEPTH_STENCIL,
GL_UNSIGNED_INT_24_8, nullptr);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, 0, 0, kWidth, kHeight, 0);
EXPECT_EQ(FirstGLError(), 0u) << "glCopyTexImage2D from a depth/stencil attachment";
glBindFramebuffer(GL_FRAMEBUFFER, 0);
const size_t pixels = static_cast<size_t>(kWidth) * kHeight;
std::vector<unsigned int> packed(pixels, kPacked24_8Poison);
glGetTexImage(GL_TEXTURE_2D, 0, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, packed.data());
EXPECT_EQ(FirstGLError(), 0u);
size_t bad = 0;
for (unsigned int value : packed) {
const float depth = static_cast<float>(value >> 8) / 16777215.0f;
if (std::fabs(depth - 0.75f) > 0.01f) ++bad;
}
EXPECT_EQ(bad, 0u) << bad << " of " << pixels << " copied depth values are wrong (first word 0x" << std::hex
<< packed[0] << std::dec << ")";
glBindTexture(GL_TEXTURE_2D, 0);
glDeleteTextures(1, &copy);
DestroySource(source);
Gl().EndFrame();
}
// The integer client widths, which are a separate conversion each.
TEST_F(DepthStencilReadbackMatrixScenario, DepthAndStencilConvertIntoEveryClientWidth) {
if (!Ready()) return;
DepthSource source = MakeTextureSource(GL_DEPTH24_STENCIL8);
ASSERT_TRUE(SourceIsUsable());
FirstGLError();
ClearDepthStencil(GL_DEPTH24_STENCIL8, 0.5f, 200);
ASSERT_EQ(FirstGLError(), 0u);
const size_t pixels = static_cast<size_t>(kWidth) * kHeight;
std::vector<unsigned int> depthUint(pixels, 0xDEADBEEFu);
glReadPixels(0, 0, kWidth, kHeight, GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, depthUint.data());
EXPECT_EQ(FirstGLError(), 0u) << "glReadPixels(GL_DEPTH_COMPONENT, GL_UNSIGNED_INT)";
// 0.5 of the full 32-bit range, with room for the source's 24-bit quantisation.
EXPECT_NEAR(static_cast<double>(depthUint[0]) / 4294967295.0, 0.5, 0.01)
<< "GL_UNSIGNED_INT depth came back as " << depthUint[0];
std::vector<unsigned short> depthUshort(pixels, 0xBEEFu);
glReadPixels(0, 0, kWidth, kHeight, GL_DEPTH_COMPONENT, GL_UNSIGNED_SHORT, depthUshort.data());
EXPECT_EQ(FirstGLError(), 0u) << "glReadPixels(GL_DEPTH_COMPONENT, GL_UNSIGNED_SHORT)";
EXPECT_NEAR(static_cast<double>(depthUshort[0]) / 65535.0, 0.5, 0.01)
<< "GL_UNSIGNED_SHORT depth came back as " << depthUshort[0];
// A stencil index is written unconverted into whichever width was asked for, so 200
// must survive intact in all of them - it is also large enough that a signed byte
// would wrap, which is the point of choosing it.
std::vector<unsigned char> stencilByte(pixels, static_cast<unsigned char>(kStencilPoison));
glReadPixels(0, 0, kWidth, kHeight, GL_STENCIL_INDEX, GL_UNSIGNED_BYTE, stencilByte.data());
EXPECT_EQ(FirstGLError(), 0u) << "glReadPixels(GL_STENCIL_INDEX, GL_UNSIGNED_BYTE)";
EXPECT_EQ(static_cast<int>(stencilByte[0]), 200);
std::vector<int> stencilInt(pixels, kStencilPoison);
glReadPixels(0, 0, kWidth, kHeight, GL_STENCIL_INDEX, GL_INT, stencilInt.data());
EXPECT_EQ(FirstGLError(), 0u) << "glReadPixels(GL_STENCIL_INDEX, GL_INT)";
EXPECT_EQ(stencilInt[0], 200);
DestroySource(source);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
Gl().EndFrame();
}
// The PACK pixel-store parameters apply to a depth read exactly as they do to a colour
// one, and the gap regions they create must be left alone.
TEST_F(DepthStencilReadbackMatrixScenario, DepthReadbackHonoursThePackPixelStoreParameters) {
if (!Ready()) return;
DepthSource source = MakeTextureSource(GL_DEPTH_COMPONENT24);
ASSERT_TRUE(SourceIsUsable());
FirstGLError();
ClearDepthStencil(GL_DEPTH_COMPONENT24, 0.5f, 0);
ASSERT_EQ(FirstGLError(), 0u);
const int rectWidth = 4;
const int rectHeight = 3;
const int rowLength = 8;
const int skipPixels = 2;
const int skipRows = 1;
constexpr float kGap = -7.0f;
std::vector<float> destination(static_cast<size_t>(rowLength) * (skipRows + rectHeight) + 16, kGap);
glPixelStorei(GL_PACK_ROW_LENGTH, rowLength);
glPixelStorei(GL_PACK_SKIP_PIXELS, skipPixels);
glPixelStorei(GL_PACK_SKIP_ROWS, skipRows);
glPixelStorei(GL_PACK_ALIGNMENT, 4);
glReadPixels(0, 0, rectWidth, rectHeight, GL_DEPTH_COMPONENT, GL_FLOAT, destination.data());
const unsigned int readError = FirstGLError();
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
glPixelStorei(GL_PACK_SKIP_PIXELS, 0);
glPixelStorei(GL_PACK_SKIP_ROWS, 0);
glPixelStorei(GL_PACK_ALIGNMENT, 4);
EXPECT_EQ(readError, 0u);
size_t written = 0;
size_t gapsTouched = 0;
for (size_t index = 0; index < destination.size(); ++index) {
const long row = static_cast<long>(index) / rowLength - skipRows;
const long column = static_cast<long>(index) % rowLength - skipPixels;
const bool inRect = row >= 0 && row < rectHeight && column >= 0 && column < rectWidth;
if (inRect) {
if (std::fabs(destination[index] - 0.5f) <= 1.0f / 4096.0f) ++written;
} else if (destination[index] != kGap) {
++gapsTouched;
}
}
EXPECT_EQ(written, static_cast<size_t>(rectWidth) * rectHeight)
<< "only " << written << " of " << (rectWidth * rectHeight)
<< " destination pixels landed where GL_PACK_ROW_LENGTH/SKIP_* put them";
EXPECT_EQ(gapsTouched, 0u) << gapsTouched << " bytes outside the packed rectangle were overwritten";
DestroySource(source);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
Gl().EndFrame();
}
// The readback borrows the application's context for a full-screen pass. Everything it
// touches has to come back, or the next draw inherits it - which is how an emulation
// that "works" takes the rest of the renderer down with it.
TEST_F(DepthStencilReadbackMatrixScenario, ReadbackLeavesNoGLStateBehind) {
if (!Ready()) return;
DepthSource source = MakeTextureSource(GL_DEPTH24_STENCIL8);
ASSERT_TRUE(SourceIsUsable());
FirstGLError();
ClearDepthStencil(GL_DEPTH24_STENCIL8, 0.5f, 4);
// A deliberately awkward state: nothing here is what an emulation pass would want,
// so anything it forgets to put back shows up below.
GLuint scratchTexture = 0;
glGenTextures(1, &scratchTexture);
glBindTexture(GL_TEXTURE_2D, scratchTexture);
glActiveTexture(GL_TEXTURE3);
glBindTexture(GL_TEXTURE_2D, scratchTexture);
glEnable(GL_SCISSOR_TEST);
glScissor(3, 5, 7, 11);
glEnable(GL_CULL_FACE);
glEnable(GL_BLEND);
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_GEQUAL);
glDepthMask(GL_FALSE);
glEnable(GL_STENCIL_TEST);
glStencilFunc(GL_NOTEQUAL, 0x5, 0x0Fu);
glStencilOp(GL_INCR, GL_DECR, GL_INVERT);
glStencilMask(0x3Cu);
glColorMask(GL_FALSE, GL_TRUE, GL_FALSE, GL_TRUE);
glViewport(2, 3, 5, 7);
ASSERT_EQ(FirstGLError(), 0u);
const std::vector<float> depth = ReadDepthFloat(0, 0, kWidth, kHeight);
const std::vector<int> stencil = ReadStencilInt(0, 0, kWidth, kHeight);
EXPECT_EQ(FirstGLError(), 0u);
ExpectAllDepth(depth, 0.5f, "state-preservation case depth");
ExpectAllStencil(stencil, 4, "state-preservation case stencil");
GLint viewport[4] = {0, 0, 0, 0};
GLint scissorBox[4] = {0, 0, 0, 0};
GLboolean colorMask[4] = {GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE};
GLint depthFunc = 0;
GLboolean depthMask = GL_TRUE;
GLint stencilFunc = 0, stencilRef = 0, stencilValueMask = 0, stencilWriteMask = 0;
GLint stencilFail = 0, stencilPassDepthFail = 0, stencilPassDepthPass = 0;
GLint activeTexture = 0, boundTexture = 0;
glGetIntegerv(GL_VIEWPORT, viewport);
glGetIntegerv(GL_SCISSOR_BOX, scissorBox);
glGetBooleanv(GL_COLOR_WRITEMASK, colorMask);
glGetIntegerv(GL_DEPTH_FUNC, &depthFunc);
glGetBooleanv(GL_DEPTH_WRITEMASK, &depthMask);
glGetIntegerv(GL_STENCIL_FUNC, &stencilFunc);
glGetIntegerv(GL_STENCIL_REF, &stencilRef);
glGetIntegerv(GL_STENCIL_VALUE_MASK, &stencilValueMask);
glGetIntegerv(GL_STENCIL_WRITEMASK, &stencilWriteMask);
glGetIntegerv(GL_STENCIL_FAIL, &stencilFail);
glGetIntegerv(GL_STENCIL_PASS_DEPTH_FAIL, &stencilPassDepthFail);
glGetIntegerv(GL_STENCIL_PASS_DEPTH_PASS, &stencilPassDepthPass);
glGetIntegerv(GL_ACTIVE_TEXTURE, &activeTexture);
glGetIntegerv(GL_TEXTURE_BINDING_2D, &boundTexture);
EXPECT_EQ(viewport[0], 2);
EXPECT_EQ(viewport[1], 3);
EXPECT_EQ(viewport[2], 5);
EXPECT_EQ(viewport[3], 7);
EXPECT_EQ(scissorBox[0], 3);
EXPECT_EQ(scissorBox[1], 5);
EXPECT_EQ(scissorBox[2], 7);
EXPECT_EQ(scissorBox[3], 11);
EXPECT_EQ(glIsEnabled(GL_SCISSOR_TEST), GLboolean(GL_TRUE));
EXPECT_EQ(glIsEnabled(GL_CULL_FACE), GLboolean(GL_TRUE));
EXPECT_EQ(glIsEnabled(GL_BLEND), GLboolean(GL_TRUE));
EXPECT_EQ(glIsEnabled(GL_DEPTH_TEST), GLboolean(GL_TRUE));
EXPECT_EQ(glIsEnabled(GL_STENCIL_TEST), GLboolean(GL_TRUE));
EXPECT_EQ(colorMask[0], GLboolean(GL_FALSE));
EXPECT_EQ(colorMask[1], GLboolean(GL_TRUE));
EXPECT_EQ(colorMask[2], GLboolean(GL_FALSE));
EXPECT_EQ(colorMask[3], GLboolean(GL_TRUE));
EXPECT_EQ(depthFunc, GLint(GL_GEQUAL));
EXPECT_EQ(depthMask, GLboolean(GL_FALSE));
EXPECT_EQ(stencilFunc, GLint(GL_NOTEQUAL));
EXPECT_EQ(stencilRef, 0x5);
EXPECT_EQ(stencilValueMask, 0x0F);
EXPECT_EQ(stencilWriteMask, 0x3C);
EXPECT_EQ(stencilFail, GLint(GL_INCR));
EXPECT_EQ(stencilPassDepthFail, GLint(GL_DECR));
EXPECT_EQ(stencilPassDepthPass, GLint(GL_INVERT));
EXPECT_EQ(activeTexture, GLint(GL_TEXTURE3));
EXPECT_EQ(boundTexture, GLint(scratchTexture))
<< "the readback left a scratch texture on the application's texture unit";
EXPECT_EQ(FirstGLError(), 0u);
// Put the awkward state back so the next scenario in this process starts clean.
glDisable(GL_SCISSOR_TEST);
glDisable(GL_CULL_FACE);
glDisable(GL_BLEND);
glDisable(GL_DEPTH_TEST);
glDisable(GL_STENCIL_TEST);
glDepthFunc(GL_LESS);
glDepthMask(GL_TRUE);
glStencilFunc(GL_ALWAYS, 0, 0xFFFFFFFFu);
glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
glStencilMask(0xFFFFFFFFu);
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
glBindTexture(GL_TEXTURE_2D, 0);
glActiveTexture(GL_TEXTURE0);
glDeleteTextures(1, &scratchTexture);
DestroySource(source);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glViewport(0, 0, Gl().Width(), Gl().Height());
Gl().EndFrame();
}
} // namespace MGITest
@@ -58,12 +58,12 @@ namespace MGITest {
class DepthStencilReadbackScenario : public ScenarioTest {
protected:
// DirectGLES reads depth and stencil back through the ES driver, which has no
// guaranteed path for either (GL_NV_read_depth / GL_NV_read_stencil are optional and
// absent on both the Adreno device and Mesa's ES). That gap is tracked separately as
// the packed_depth_stencil cluster and needs a shader-sampling emulation, not this
// change; asserting it here would only pin a known-missing feature.
bool BackendReadsDepthStencil() const { return Gl().BackendName() == "DirectVulkan"; }
// Both backends now answer these reads. DirectGLES has no native ES path for
// either aspect (GL_NV_read_depth / GL_NV_read_stencil are optional and absent on
// both the Adreno device and Mesa's ES), so it stages the attachment into a
// scratch depth texture and samples it into a colour target; the assertions below
// are the same either way, which is the point.
bool BackendReadsDepthStencil() const { return true; }
float ReadDepthAt(int x, int y) const {
float depth = kDepthPoison;
@@ -0,0 +1,720 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/DoublePrecisionScenario.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Scenario - GLSL DOUBLES, RUN AT SINGLE PRECISION.
//
// No mobile GPU has 64-bit floats. Adreno and Mali both report shaderFloat64 == VK_FALSE, so
// Magma cannot build a module that declares the Float64 capability, and ESSL has no fp64 type
// at all, so SPIRV-Cross refuses the module outright on Espryt ("FP64 not supported in ES
// profile") and the program never reaches the driver. MobileGL therefore narrows every 64-bit
// float in a shader to 32 bits (ShaderTranspiler::DemoteFloat64Pass) rather than declining the
// shader: `double` compiles and runs everywhere, at float precision.
//
// The narrowing is only half a contract. The other half is the API side: the global UBO is
// laid out by reflecting the DEMOTED module, so glUniform*d has to store a float where the
// shader reads a float, glGetUniform*v has to read one back, and a dmat4's columns are now
// std140-padded like any other matrix's. Every one of those is a byte offset that fails
// silently - the uniform simply reads as something else - so the cases below set values
// through the API and have the SHADER report what it saw.
//
// What is deliberately NOT asserted: that the values are exact to double precision. They are
// not, and cannot be. Every expectation here is the float value of the double that was set,
// which is the whole point.
#include <cmath>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
// Doubles in every shape the demotion has to handle - a scalar, a vector, a matrix
// whose column stride changes, an array whose element stride changes - all reported
// through one float SSBO so a single readback says which one moved.
constexpr const char* kComputeSource = R"(#version 430 core
layout(local_size_x = 1) in;
uniform double uScalar;
uniform dvec3 uVector;
uniform dmat4 uMatrix;
uniform double uArray[3];
layout(std430, binding = 0) buffer Output {
float g_out[];
};
void main() {
g_out[0] = float(uScalar);
g_out[1] = float(uVector.x);
g_out[2] = float(uVector.y);
g_out[3] = float(uVector.z);
// Column-major [column][row]. Off-diagonal entries catch a column-stride mistake that a
// diagonal-only check reads straight past.
g_out[4] = float(uMatrix[0][0]);
g_out[5] = float(uMatrix[0][3]);
g_out[6] = float(uMatrix[3][0]);
g_out[7] = float(uMatrix[3][3]);
g_out[8] = float(uArray[0]);
g_out[9] = float(uArray[1]);
g_out[10] = float(uArray[2]);
// Arithmetic on doubles, including an implicit float->double conversion and a literal
// with the fp64 suffix: this is what an application actually writes, and it is the part
// that has to survive the conversion folding.
double accumulated = uScalar * 2.0lf + 1.5;
g_out[11] = float(accumulated);
}
)";
constexpr int kOutputSlots = 12;
class DoublePrecisionScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
m_program = CompileComputeProgram(kComputeSource);
ASSERT_NE(m_program, 0u) << m_buildLog;
glGenBuffers(1, &m_output);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_output);
const std::vector<float> zeroes(kOutputSlots, 0.0f);
glBufferData(GL_SHADER_STORAGE_BUFFER, kOutputSlots * sizeof(float), zeroes.data(),
GL_DYNAMIC_DRAW);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_output);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
}
void TearDown() override {
if (!Ready()) return;
if (m_shapeOutput != 0) glDeleteBuffers(1, &m_shapeOutput);
if (m_shapeProgram != 0) glDeleteProgram(m_shapeProgram);
if (m_output != 0) glDeleteBuffers(1, &m_output);
if (m_program != 0) glDeleteProgram(m_program);
}
unsigned int CompileComputeProgram(const char* source) {
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
glShaderSource(shader, 1, &source, nullptr);
glCompileShader(shader);
GLint compiled = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (compiled == GL_FALSE) {
char log[2048] = {};
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
m_buildLog = std::string("compute shader did not compile: ") + log;
glDeleteShader(shader);
return 0;
}
const GLuint program = glCreateProgram();
glAttachShader(program, shader);
glLinkProgram(program);
glDeleteShader(shader);
GLint linked = 0;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
if (linked == GL_FALSE) {
char log[2048] = {};
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
m_buildLog = std::string("compute program did not link: ") + log;
glDeleteProgram(program);
return 0;
}
return program;
}
std::vector<float> Dispatch() {
glUseProgram(m_program);
glDispatchCompute(1, 1, 1);
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
std::vector<float> values(kOutputSlots, -1.0f);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_output);
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, kOutputSlots * sizeof(float), values.data());
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
glUseProgram(0);
return values;
}
unsigned int m_program = 0;
unsigned int m_output = 0;
unsigned int m_shapeProgram = 0;
unsigned int m_shapeOutput = 0;
std::string m_buildLog;
};
// Every double-typed uniform shape GLSL has, all thirteen of them, in one program - the
// shape of KHR-GL43.compute_shader.fp64-case2. The scalar and the square matrices are
// covered by the cases above; what only a set like this reaches is the NON-SQUARE
// matrices, whose column stride and total size both change when the demotion turns a
// 64-bit column into a 32-bit one, and whose members therefore move every uniform
// declared after them.
//
// The shader reports every component separately rather than one pass/fail flag, because
// "the readback is wrong" is not a diagnosis: a wrong column stride, a wrong member
// offset and a wrong narrowing all fail the same single comparison, and only the
// component map says which.
// No #version here on purpose: it is handed over as a separate source string, the way
// the CTS case hands it over.
constexpr const char* kAllDoubleShapesSource = R"(
layout(local_size_x = 1) in;
uniform double g_0;
uniform dvec2 g_1;
uniform dvec3 g_2;
uniform dvec4 g_3;
uniform dmat2 g_4;
uniform dmat2x3 g_5;
uniform dmat2x4 g_6;
uniform dmat3x2 g_7;
uniform dmat3 g_8;
uniform dmat3x4 g_9;
uniform dmat4x2 g_10;
uniform dmat4x3 g_11;
uniform dmat4 g_12;
layout(std430, binding = 0) buffer Output {
float g_out[];
};
void main() {
g_out[0] = float(g_0);
for (int i = 0; i < 2; ++i) g_out[1 + i] = float(g_1[i]);
for (int i = 0; i < 3; ++i) g_out[3 + i] = float(g_2[i]);
for (int i = 0; i < 4; ++i) g_out[6 + i] = float(g_3[i]);
for (int c = 0; c < 2; ++c) for (int r = 0; r < 2; ++r) g_out[10 + c * 2 + r] = float(g_4[c][r]);
for (int c = 0; c < 2; ++c) for (int r = 0; r < 3; ++r) g_out[14 + c * 3 + r] = float(g_5[c][r]);
for (int c = 0; c < 2; ++c) for (int r = 0; r < 4; ++r) g_out[20 + c * 4 + r] = float(g_6[c][r]);
for (int c = 0; c < 3; ++c) for (int r = 0; r < 2; ++r) g_out[28 + c * 2 + r] = float(g_7[c][r]);
for (int c = 0; c < 3; ++c) for (int r = 0; r < 3; ++r) g_out[34 + c * 3 + r] = float(g_8[c][r]);
for (int c = 0; c < 3; ++c) for (int r = 0; r < 4; ++r) g_out[43 + c * 4 + r] = float(g_9[c][r]);
for (int c = 0; c < 4; ++c) for (int r = 0; r < 2; ++r) g_out[55 + c * 2 + r] = float(g_10[c][r]);
for (int c = 0; c < 4; ++c) for (int r = 0; r < 3; ++r) g_out[63 + c * 3 + r] = float(g_11[c][r]);
for (int c = 0; c < 4; ++c) for (int r = 0; r < 4; ++r) g_out[75 + c * 4 + r] = float(g_12[c][r]);
}
)";
// The values the CTS case sets, spelled the way it spells them - column-major, and small
// enough that every one is exact in a float. Nothing here is a precision question; a
// component that comes back wrong came back from the wrong bytes.
constexpr double kG0 = 1.0;
constexpr double kG1[2] = {2.0, 3.0};
constexpr double kG2[3] = {4.0, 5.0, 6.0};
constexpr double kG3[4] = {7.0, 8.0, 9.0, 10.0};
constexpr double kG4[4] = {11.0, 12.0, 13.0, 14.0};
constexpr double kG5[6] = {15.0, 16.0, 17.0, 18.0, 19.0, 20.0};
constexpr double kG6[8] = {21.0, 22.0, 23.0, 24.0, 25.0, 26.0, 27.0, 28.0};
constexpr double kG7[6] = {29.0, 30.0, 31.0, 32.0, 33.0, 34.0};
constexpr double kG8[9] = {35.0, 36.0, 37.0, 38.0, 39.0, 40.0, 41.0, 42.0, 43.0};
constexpr double kG9[12] = {44.0, 45.0, 46.0, 47.0, 48.0, 49.0, 50.0, 51.0, 52.0, 53.0, 54.0, 55.0};
constexpr double kG10[8] = {56.0, 57.0, 58.0, 59.0, 60.0, 61.0, 62.0, 63.0};
constexpr double kG11[12] = {63.0, 64.0, 65.0, 66.0, 67.0, 68.0, 69.0, 70.0, 71.0, 27.0, 73.0, 74.0};
constexpr double kG12[16] = {75.0, 76.0, 77.0, 78.0, 79.0, 80.0, 81.0, 82.0,
83.0, 84.0, 85.0, 86.0, 87.0, 88.0, 89.0, 90.0};
struct DoubleShape {
const char* name;
int base;
int columns; // 1 for the scalar and the vectors
int rows; // component count for the scalar and the vectors
const double* values;
};
constexpr DoubleShape kDoubleShapes[] = {
{"g_0 double", 0, 1, 1, &kG0}, {"g_1 dvec2", 1, 1, 2, kG1},
{"g_2 dvec3", 3, 1, 3, kG2}, {"g_3 dvec4", 6, 1, 4, kG3},
{"g_4 dmat2", 10, 2, 2, kG4}, {"g_5 dmat2x3", 14, 2, 3, kG5},
{"g_6 dmat2x4", 20, 2, 4, kG6}, {"g_7 dmat3x2", 28, 3, 2, kG7},
{"g_8 dmat3", 34, 3, 3, kG8}, {"g_9 dmat3x4", 43, 3, 4, kG9},
{"g_10 dmat4x2", 55, 4, 2, kG10}, {"g_11 dmat4x3", 63, 4, 3, kG11},
{"g_12 dmat4", 75, 4, 4, kG12},
};
constexpr int kAllShapeSlots = 91;
// The conformance case's own shader, kept verbatim down to the literal suffixes and the
// unnamed, unqualified storage block - except that each comparison sets its OWN bit
// instead of collapsing all thirteen into one flag. That single flag is the whole reason
// the case was unexplained for a wave: it says "something is wrong" and nothing else.
//
// Verbatim matters here. Reading the components out one at a time (the case above)
// passes; whatever fails does so through the shape the conformance case actually
// writes - whole-matrix comparison against a constructor, a storage block with no
// layout qualifier and no instance name, values reached with constant indices.
constexpr const char* kCtsShapedSource = R"(
layout(local_size_x = 1) in;
buffer Result {
int g_result;
};
uniform double g_0;
uniform dvec2 g_1;
uniform dvec3 g_2;
uniform dvec4 g_3;
uniform dmat2 g_4;
uniform dmat2x3 g_5;
uniform dmat2x4 g_6;
uniform dmat3x2 g_7;
uniform dmat3 g_8;
uniform dmat3x4 g_9;
uniform dmat4x2 g_10;
uniform dmat4x3 g_11;
uniform dmat4 g_12;
void main() {
g_result = 0;
if (g_0 != 1.0LF) g_result |= 1;
if (g_1 != dvec2(2.0LF, 3.0LF)) g_result |= 2;
if (g_2 != dvec3(4.0LF, 5.0LF, 6.0LF)) g_result |= 4;
if (g_3 != dvec4(7.0LF, 8.0LF, 9.0LF, 10.0LF)) g_result |= 8;
if (g_4 != dmat2(11.0LF, 12.0LF, 13.0LF, 14.0LF)) g_result |= 16;
if (g_5 != dmat2x3(15.0LF, 16.0LF, 17.0LF, 18.0LF, 19.0LF, 20.0LF)) g_result |= 32;
if (g_6 != dmat2x4(21.0LF, 22.0LF, 23.0LF, 24.0LF, 25.0LF, 26.0LF, 27.0LF, 28.0LF)) g_result |= 64;
if (g_7 != dmat3x2(29.0LF, 30.0LF, 31.0LF, 32.0LF, 33.0LF, 34.0LF)) g_result |= 128;
if (g_8 != dmat3(35.0LF, 36.0LF, 37.0LF, 38.0LF, 39.0LF, 40.0LF, 41.0LF, 42.0LF, 43.0LF)) g_result |= 256;
if (g_9 != dmat3x4(44.0LF, 45.0LF, 46.0LF, 47.0LF, 48.0LF, 49.0LF, 50.0LF, 51.0LF, 52.0LF, 53.0LF, 54.0LF, 55.0LF)) g_result |= 512;
if (g_10 != dmat4x2(56.0, 57.0, 58.0, 59.0, 60.0, 61.0, 62.0, 63.0)) g_result |= 1024;
if (g_11 != dmat4x3(63.0, 64.0, 65.0, 66.0, 67.0, 68.0, 69.0, 70.0, 71.0, 27.0, 73, 74.0)) g_result |= 2048;
if (g_12 != dmat4(75.0, 76.0, 77.0, 78.0, 79.0, 80.0, 81.0, 82.0, 83.0, 84.0, 85.0, 86.0, 87.0, 88.0, 89.0, 90.0)) g_result |= 4096;
}
)";
TEST_F(DoublePrecisionScenario, ADoubleUniformReachesTheShaderAtFloatPrecision) {
if (!Ready()) return;
glUseProgram(m_program);
const GLint scalar = glGetUniformLocation(m_program, "uScalar");
ASSERT_GE(scalar, 0);
// 0.1 has no exact float (or double) representation, so this only passes if the
// value really travelled through the demoted slot rather than being read out of
// some other four bytes.
glUniform1d(scalar, 0.1);
glUseProgram(0);
const std::vector<float> values = Dispatch();
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
EXPECT_FLOAT_EQ(values[0], static_cast<float>(0.1));
EXPECT_FLOAT_EQ(values[11], static_cast<float>(static_cast<float>(0.1) * 2.0f + 1.5f))
<< "arithmetic on the demoted value, including the folded fp64 literal";
}
TEST_F(DoublePrecisionScenario, EveryDoubleShapeLandsInItsOwnSlot) {
if (!Ready()) return;
glUseProgram(m_program);
const GLint scalar = glGetUniformLocation(m_program, "uScalar");
const GLint vector = glGetUniformLocation(m_program, "uVector");
const GLint matrix = glGetUniformLocation(m_program, "uMatrix");
const GLint array0 = glGetUniformLocation(m_program, "uArray[0]");
const GLint array2 = glGetUniformLocation(m_program, "uArray[2]");
ASSERT_GE(scalar, 0);
ASSERT_GE(vector, 0);
ASSERT_GE(matrix, 0);
ASSERT_GE(array0, 0);
ASSERT_GE(array2, 0);
glUniform1d(scalar, 5.0);
const GLdouble vectorValue[3] = {11.0, 12.0, 13.0};
glUniform3dv(vector, 1, vectorValue);
// Column-major, and every entry distinct so a transposed or mis-strided write
// cannot land on a value that happens to match.
GLdouble matrixValue[16] = {};
for (int i = 0; i < 16; ++i) matrixValue[i] = 100.0 + i;
glUniformMatrix4dv(matrix, 1, GL_FALSE, matrixValue);
const GLdouble arrayValue[3] = {71.0, 72.0, 73.0};
glUniform1dv(array0, 3, arrayValue);
glUseProgram(0);
const std::vector<float> values = Dispatch();
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
EXPECT_FLOAT_EQ(values[0], 5.0f) << "scalar double";
EXPECT_FLOAT_EQ(values[1], 11.0f) << "dvec3 .x";
EXPECT_FLOAT_EQ(values[2], 12.0f) << "dvec3 .y";
EXPECT_FLOAT_EQ(values[3], 13.0f) << "dvec3 .z";
EXPECT_FLOAT_EQ(values[4], 100.0f) << "dmat4 [0][0]";
EXPECT_FLOAT_EQ(values[5], 103.0f) << "dmat4 [0][3] - within the first column";
EXPECT_FLOAT_EQ(values[6], 112.0f) << "dmat4 [3][0] - column stride";
EXPECT_FLOAT_EQ(values[7], 115.0f) << "dmat4 [3][3]";
EXPECT_FLOAT_EQ(values[8], 71.0f) << "double array element 0";
EXPECT_FLOAT_EQ(values[9], 72.0f) << "double array element 1 - element stride";
EXPECT_FLOAT_EQ(values[10], 73.0f) << "double array element 2";
}
TEST_F(DoublePrecisionScenario, TheTransposeFlagStillTransposes) {
if (!Ready()) return;
glUseProgram(m_program);
const GLint matrix = glGetUniformLocation(m_program, "uMatrix");
ASSERT_GE(matrix, 0);
GLdouble matrixValue[16] = {};
for (int i = 0; i < 16; ++i) matrixValue[i] = 100.0 + i;
glUniformMatrix4dv(matrix, 1, GL_TRUE, matrixValue);
glUseProgram(0);
const std::vector<float> values = Dispatch();
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
// Transposed, so [column][row] now reads the source's [row][column].
EXPECT_FLOAT_EQ(values[4], 100.0f) << "dmat4 [0][0] is on the diagonal either way";
EXPECT_FLOAT_EQ(values[5], 112.0f) << "dmat4 [0][3] after transpose";
EXPECT_FLOAT_EQ(values[6], 103.0f) << "dmat4 [3][0] after transpose";
EXPECT_FLOAT_EQ(values[7], 115.0f) << "dmat4 [3][3] is on the diagonal either way";
}
TEST_F(DoublePrecisionScenario, TheUniformIsStillReportedAsADouble) {
if (!Ready()) return;
// The demotion is an implementation detail of how the value is STORED. What the
// shader source declared is what the application asked about, so the reflection
// keeps answering GL_DOUBLE* - an application that switches on the type and calls
// glUniform*d has to keep working, and it is the glUniform*d path that is correct
// for these uniforms.
struct Expectation {
const char* name;
GLenum type;
GLint size;
};
const Expectation expectations[] = {
{"uScalar", GL_DOUBLE, 1},
{"uVector", GL_DOUBLE_VEC3, 1},
{"uMatrix", GL_DOUBLE_MAT4, 1},
{"uArray[0]", GL_DOUBLE, 3},
};
GLint activeUniforms = 0;
glGetProgramiv(m_program, GL_ACTIVE_UNIFORMS, &activeUniforms);
ASSERT_GT(activeUniforms, 0);
for (const Expectation& expectation : expectations) {
bool found = false;
for (GLint index = 0; index < activeUniforms; ++index) {
char name[128] = {};
GLsizei length = 0;
GLint size = 0;
GLenum type = 0;
glGetActiveUniform(m_program, static_cast<GLuint>(index), sizeof(name) - 1, &length, &size,
&type, name);
if (std::string(name, static_cast<size_t>(length)) != expectation.name) continue;
found = true;
EXPECT_EQ(type, expectation.type) << expectation.name;
EXPECT_EQ(size, expectation.size) << expectation.name;
break;
}
EXPECT_TRUE(found) << "glGetActiveUniform never reported " << expectation.name;
}
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
}
TEST_F(DoublePrecisionScenario, GetUniformdvReadsBackWhatWasStored) {
if (!Ready()) return;
glUseProgram(m_program);
const GLint scalar = glGetUniformLocation(m_program, "uScalar");
const GLint vector = glGetUniformLocation(m_program, "uVector");
const GLint matrix = glGetUniformLocation(m_program, "uMatrix");
ASSERT_GE(scalar, 0);
ASSERT_GE(vector, 0);
ASSERT_GE(matrix, 0);
glUniform1d(scalar, 0.1);
const GLdouble vectorValue[3] = {11.5, 12.5, 13.5};
glUniform3dv(vector, 1, vectorValue);
GLdouble matrixValue[16] = {};
for (int i = 0; i < 16; ++i) matrixValue[i] = 100.0 + i;
glUniformMatrix4dv(matrix, 1, GL_FALSE, matrixValue);
glUseProgram(0);
// The readback has to undo exactly what the write did - the same std140 column
// padding, the same 4-byte components - or a dmat4 comes back with its columns
// shifted and nothing else in the API would say so.
GLdouble readScalar = 0.0;
glGetUniformdv(m_program, scalar, &readScalar);
EXPECT_DOUBLE_EQ(readScalar, static_cast<double>(static_cast<float>(0.1)))
<< "the value is what a float can hold, not the double that was passed in";
GLdouble readVector[3] = {};
glGetUniformdv(m_program, vector, readVector);
EXPECT_DOUBLE_EQ(readVector[0], 11.5);
EXPECT_DOUBLE_EQ(readVector[1], 12.5);
EXPECT_DOUBLE_EQ(readVector[2], 13.5);
GLdouble readMatrix[16] = {};
glGetUniformdv(m_program, matrix, readMatrix);
for (int i = 0; i < 16; ++i) {
EXPECT_DOUBLE_EQ(readMatrix[i], 100.0 + i) << "dmat4 component " << i;
}
// The float query sees the same storage through the type it is actually stored as.
GLfloat readFloat = 0.0f;
glGetUniformfv(m_program, scalar, &readFloat);
EXPECT_FLOAT_EQ(readFloat, static_cast<float>(0.1));
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
}
TEST_F(DoublePrecisionScenario, ADoubleUniformKeepsItsDeclaredInitializer) {
if (!Ready()) return;
// A declared initializer is seeded straight into the uniform shadow at link, and the
// seeding used to skip 64-bit floats outright ("no 32-bit shadow encoding") - which
// was true before the demotion and silently left every such uniform reading zero.
const char* source = R"(#version 430 core
layout(local_size_x = 1) in;
uniform double uSeeded = 2.5lf;
uniform dvec3 uSeededVector = dvec3(4.0lf, 5.0lf, 6.0lf);
layout(std430, binding = 0) buffer Output {
float g_out[];
};
void main() {
g_out[0] = float(uSeeded);
g_out[1] = float(uSeededVector.x);
g_out[2] = float(uSeededVector.y);
g_out[3] = float(uSeededVector.z);
}
)";
const GLuint program = CompileComputeProgram(source);
ASSERT_NE(program, 0u) << m_buildLog;
glUseProgram(program);
glDispatchCompute(1, 1, 1);
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
std::vector<float> values(4, -1.0f);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_output);
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, 4 * sizeof(float), values.data());
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
glUseProgram(0);
glDeleteProgram(program);
EXPECT_FLOAT_EQ(values[0], 2.5f) << "scalar double initializer";
EXPECT_FLOAT_EQ(values[1], 4.0f) << "dvec3 initializer .x";
EXPECT_FLOAT_EQ(values[2], 5.0f) << "dvec3 initializer .y";
EXPECT_FLOAT_EQ(values[3], 6.0f) << "dvec3 initializer .z";
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
}
TEST_F(DoublePrecisionScenario, EveryDoubleUniformShapeArrivesWhereTheShaderReadsIt) {
if (!Ready()) return;
// Built the way the CTS case builds it, because every step of that build has been a
// bug here at least once: the source arrives as TWO strings (the version directive
// and the body), the shader is attached before it has a source and deleted while
// still attached, and the program is linked twice.
m_shapeProgram = glCreateProgram();
ASSERT_NE(m_shapeProgram, 0u);
{
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
glAttachShader(m_shapeProgram, shader);
glDeleteShader(shader);
const char* const sources[2] = {"#version 430 core\n", kAllDoubleShapesSource};
glShaderSource(shader, 2, sources, nullptr);
glCompileShader(shader);
GLint compiled = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (compiled == GL_FALSE) {
char log[2048] = {};
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
FAIL() << "compute shader did not compile: " << log;
}
}
glLinkProgram(m_shapeProgram);
{
GLint linkedOnce = 0;
glGetProgramiv(m_shapeProgram, GL_LINK_STATUS, &linkedOnce);
if (linkedOnce == GL_FALSE) {
char log[2048] = {};
glGetProgramInfoLog(m_shapeProgram, sizeof(log) - 1, nullptr, log);
FAIL() << "compute program did not link: " << log;
}
}
glGenBuffers(1, &m_shapeOutput);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_shapeOutput);
const std::vector<float> zeroes(kAllShapeSlots, 0.0f);
glBufferData(GL_SHADER_STORAGE_BUFFER, kAllShapeSlots * sizeof(float), zeroes.data(), GL_DYNAMIC_DRAW);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_shapeOutput);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
const auto location = [&](const char* name) { return glGetUniformLocation(m_shapeProgram, name); };
// Pass one sets through glProgramUniform*, pass two through glUniform* after a
// re-link - the two entry-point families the CTS case exercises, and two different
// routes into the same uniform storage.
const auto setWithProgramUniform = [&]() {
glProgramUniform1d(m_shapeProgram, location("g_0"), kG0);
glProgramUniform2d(m_shapeProgram, location("g_1"), kG1[0], kG1[1]);
glProgramUniform3d(m_shapeProgram, location("g_2"), kG2[0], kG2[1], kG2[2]);
glProgramUniform4d(m_shapeProgram, location("g_3"), kG3[0], kG3[1], kG3[2], kG3[3]);
glProgramUniformMatrix2dv(m_shapeProgram, location("g_4"), 1, GL_FALSE, kG4);
glProgramUniformMatrix2x3dv(m_shapeProgram, location("g_5"), 1, GL_FALSE, kG5);
glProgramUniformMatrix2x4dv(m_shapeProgram, location("g_6"), 1, GL_FALSE, kG6);
glProgramUniformMatrix3x2dv(m_shapeProgram, location("g_7"), 1, GL_FALSE, kG7);
glProgramUniformMatrix3dv(m_shapeProgram, location("g_8"), 1, GL_FALSE, kG8);
glProgramUniformMatrix3x4dv(m_shapeProgram, location("g_9"), 1, GL_FALSE, kG9);
glProgramUniformMatrix4x2dv(m_shapeProgram, location("g_10"), 1, GL_FALSE, kG10);
glProgramUniformMatrix4x3dv(m_shapeProgram, location("g_11"), 1, GL_FALSE, kG11);
glProgramUniformMatrix4dv(m_shapeProgram, location("g_12"), 1, GL_FALSE, kG12);
};
// Deliberately does NOT re-issue glUseProgram: the CTS case leaves the program
// current across the re-link and writes into it from there, so this is the path
// where a re-link has to keep the current program's uniform storage addressable.
const auto setWithUniform = [&]() {
glUniform1d(location("g_0"), kG0);
glUniform2d(location("g_1"), kG1[0], kG1[1]);
glUniform3d(location("g_2"), kG2[0], kG2[1], kG2[2]);
glUniform4d(location("g_3"), kG3[0], kG3[1], kG3[2], kG3[3]);
glUniformMatrix2dv(location("g_4"), 1, GL_FALSE, kG4);
glUniformMatrix2x3dv(location("g_5"), 1, GL_FALSE, kG5);
glUniformMatrix2x4dv(location("g_6"), 1, GL_FALSE, kG6);
glUniformMatrix3x2dv(location("g_7"), 1, GL_FALSE, kG7);
glUniformMatrix3dv(location("g_8"), 1, GL_FALSE, kG8);
glUniformMatrix3x4dv(location("g_9"), 1, GL_FALSE, kG9);
glUniformMatrix4x2dv(location("g_10"), 1, GL_FALSE, kG10);
glUniformMatrix4x3dv(location("g_11"), 1, GL_FALSE, kG11);
glUniformMatrix4dv(location("g_12"), 1, GL_FALSE, kG12);
};
const auto dispatchAndRead = [&]() {
glUseProgram(m_shapeProgram);
glDispatchCompute(1, 1, 1);
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
std::vector<float> values(kAllShapeSlots, -1.0f);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_shapeOutput);
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, kAllShapeSlots * sizeof(float), values.data());
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
// The program stays current on purpose - see setWithUniform.
return values;
};
const auto expectEverything = [](const std::vector<float>& values, const char* pass) {
for (const DoubleShape& shape : kDoubleShapes) {
for (int c = 0; c < shape.columns; ++c) {
for (int r = 0; r < shape.rows; ++r) {
const int component = c * shape.rows + r;
EXPECT_FLOAT_EQ(values[shape.base + component],
static_cast<float>(shape.values[component]))
<< pass << ": " << shape.name << " column " << c << " row " << r;
}
}
}
};
setWithProgramUniform();
expectEverything(dispatchAndRead(), "glProgramUniform*");
// A re-link zeroes every uniform, so pass two proves its own writes rather than
// reading pass one's bytes back.
glLinkProgram(m_shapeProgram);
GLint linked = 0;
glGetProgramiv(m_shapeProgram, GL_LINK_STATUS, &linked);
ASSERT_EQ(linked, GL_TRUE);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_shapeOutput);
glBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, kAllShapeSlots * sizeof(float), zeroes.data());
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
setWithUniform();
expectEverything(dispatchAndRead(), "glUniform* after re-link");
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
}
TEST_F(DoublePrecisionScenario, TheConformanceUniformShaderAgreesWithEveryValueItWasGiven) {
if (!Ready()) return;
m_shapeProgram = glCreateProgram();
ASSERT_NE(m_shapeProgram, 0u);
{
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
glAttachShader(m_shapeProgram, shader);
glDeleteShader(shader);
const char* const sources[2] = {"#version 430 core\n", kCtsShapedSource};
glShaderSource(shader, 2, sources, nullptr);
glCompileShader(shader);
GLint compiled = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (compiled == GL_FALSE) {
char log[2048] = {};
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
FAIL() << "compute shader did not compile: " << log;
}
}
glLinkProgram(m_shapeProgram);
GLint linked = 0;
glGetProgramiv(m_shapeProgram, GL_LINK_STATUS, &linked);
if (linked == GL_FALSE) {
char log[2048] = {};
glGetProgramInfoLog(m_shapeProgram, sizeof(log) - 1, nullptr, log);
FAIL() << "compute program did not link: " << log;
}
glGenBuffers(1, &m_shapeOutput);
const GLint seed = 123;
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_shapeOutput);
glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(seed), &seed, GL_STATIC_DRAW);
const auto location = [&](const char* name) { return glGetUniformLocation(m_shapeProgram, name); };
glProgramUniform1d(m_shapeProgram, location("g_0"), kG0);
glProgramUniform2d(m_shapeProgram, location("g_1"), kG1[0], kG1[1]);
glProgramUniform3d(m_shapeProgram, location("g_2"), kG2[0], kG2[1], kG2[2]);
glProgramUniform4d(m_shapeProgram, location("g_3"), kG3[0], kG3[1], kG3[2], kG3[3]);
glProgramUniformMatrix2dv(m_shapeProgram, location("g_4"), 1, GL_FALSE, kG4);
glProgramUniformMatrix2x3dv(m_shapeProgram, location("g_5"), 1, GL_FALSE, kG5);
glProgramUniformMatrix2x4dv(m_shapeProgram, location("g_6"), 1, GL_FALSE, kG6);
glProgramUniformMatrix3x2dv(m_shapeProgram, location("g_7"), 1, GL_FALSE, kG7);
glProgramUniformMatrix3dv(m_shapeProgram, location("g_8"), 1, GL_FALSE, kG8);
glProgramUniformMatrix3x4dv(m_shapeProgram, location("g_9"), 1, GL_FALSE, kG9);
glProgramUniformMatrix4x2dv(m_shapeProgram, location("g_10"), 1, GL_FALSE, kG10);
glProgramUniformMatrix4x3dv(m_shapeProgram, location("g_11"), 1, GL_FALSE, kG11);
glProgramUniformMatrix4dv(m_shapeProgram, location("g_12"), 1, GL_FALSE, kG12);
glUseProgram(m_shapeProgram);
glDispatchCompute(1, 1, 1);
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
GLint disagreements = -1;
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, sizeof(disagreements), &disagreements);
for (int bit = 0; bit < 13; ++bit) {
EXPECT_EQ(disagreements & (1 << bit), 0)
<< kDoubleShapes[bit].name << " did not compare equal to the value it was given";
}
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
}
TEST_F(DoublePrecisionScenario, TheFp64ExtensionIsNotAdvertised) {
if (!Ready()) return;
// The shader above compiled, linked and ran without the extension string, which is
// the point: an application does not need GL_ARB_gpu_shader_fp64 advertised to USE
// doubles here. What the string additionally promises is 64-bit precision, and that
// is the one thing the demotion cannot deliver - so it stays off unless
// MOBILEGL_ADVERTISE_FP64 asks for it, and an application that branches on the
// string keeps taking its float path.
GLint extensionCount = 0;
glGetIntegerv(GL_NUM_EXTENSIONS, &extensionCount);
ASSERT_GT(extensionCount, 0);
bool advertised = false;
for (GLint i = 0; i < extensionCount; ++i) {
const char* name = reinterpret_cast<const char*>(glGetStringi(GL_EXTENSIONS, static_cast<GLuint>(i)));
if (name != nullptr && std::string(name) == "GL_ARB_gpu_shader_fp64") advertised = true;
}
EXPECT_FALSE(advertised);
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
}
TEST_F(DoublePrecisionScenario, A64BitVertexFormatIsDeclinedOnEveryBackend) {
if (!Ready()) return;
// The demotion leaves no 64-bit shader input to feed, so there is nothing a 64-bit
// vertex FETCH could be fetched into - on either backend, and no longer only on the
// ones whose device lacks shaderFloat64. Declined loudly rather than accepted and
// drawn as garbage; the matching POST row says the same thing at startup.
GLuint vao = 0;
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
while (glGetError() != GL_NO_ERROR) {}
glVertexAttribLFormat(0, 3, GL_DOUBLE, 0);
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_INVALID_OPERATION));
glBindVertexArray(0);
glDeleteVertexArrays(1, &vao);
while (glGetError() != GL_NO_ERROR) {}
}
} // namespace
} // namespace MGITest
@@ -0,0 +1,348 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/DrawParametersScenario.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// gl_BaseVertex / gl_BaseInstance / gl_DrawID (GL_ARB_shader_draw_parameters),
// read straight out of the shader that a draw command produced.
//
// Neither backend has these builtins for free, and each is wrong in its own way
// when nobody watches:
//
// * DirectVulkan HAS a BaseVertex builtin, but Vulkan's carries the draw's
// firstVertex on a NON-INDEXED draw where GL's is defined to be zero ("the
// value passed to the baseVertex parameter, or zero for a command with no
// such parameter"). Only the indexed meaning of the two agrees. Every
// DrawArrays form therefore takes the ZeroBaseVertex program variant.
// * DirectGLES has no such builtins at all: ESSL knows none of them, so the
// transpiler demotes each one to a uniform the draw paths feed. A uniform
// nobody writes keeps whatever the previous draw left in it - which is what
// made gl_BaseVertex report a stale base vertex, and what made
// gl_BaseInstance read an unbound storage buffer on a plain glDrawArrays.
//
// The shader paints the three values, so a draw that carries the wrong ones
// paints the wrong colour rather than merely disagreeing with an expectation
// somewhere. The framebuffer is cleared to WHITE and no case expects 255 in any
// channel, so "the draw did not happen" can never be mistaken for a pass.
#include <cstdint>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glext.h>
namespace MGITest {
namespace {
// #version 450: glslang only declares the ARB builtins from 440 up.
//
// Each value is painted at 8 units per count, not 1: the errors these builtins
// actually have are OFF BY ONE (a sub-draw that never got its own gl_DrawID reads
// the previous one's, a base vertex that arrives one command late), and at one unit
// per count no readback tolerance can tell those from rounding.
//
// And biased by two counts, so that ZERO is not the clamp floor. Five of these cases
// expect zero, and an unbiased encoding would let every negative value - the shape a
// sign or rebase mistake produces - clamp to the same black and pass.
constexpr const char* kVertexSource = R"(#version 450 core
#extension GL_ARB_shader_draw_parameters : require
layout(location = 0) in vec2 aPos;
flat out vec3 vParams;
void main() {
vParams = (vec3(gl_BaseVertexARB, gl_BaseInstanceARB, gl_DrawIDARB) * 8.0 + 16.0) / 255.0;
gl_Position = vec4(aPos, 0.0, 1.0);
}
)";
constexpr const char* kFragmentSource = R"(#version 450 core
flat in vec3 vParams;
out vec4 oColor;
void main() {
oColor = vec4(vParams, 1.0);
}
)";
struct Vertex {
float x, y;
};
// 3 dummy vertices, then the left half of the viewport as two triangles,
// then the right half. Nothing here is symmetric by accident:
//
// * the padding makes a draw that ignores `first` / baseVertex paint a
// degenerate triangle (i.e. nothing) instead of the right picture;
// * the two halves let one multi-draw show TWO different gl_DrawID
// values in one readback.
//
// Indices 3..14 together cover the whole viewport, which is what the
// single-draw cases use.
constexpr int kPad = 3;
constexpr int kLeftFirst = kPad; // 3
constexpr int kRightFirst = kPad + 6; // 9
constexpr int kHalfCount = 6;
std::vector<Vertex> SceneVertices() {
std::vector<Vertex> vertices(static_cast<std::size_t>(kPad), Vertex{0.0f, 0.0f});
const float bounds[2][2] = {{-1.0f, 0.0f}, {0.0f, 1.0f}};
for (const auto& half : bounds) {
const float x0 = half[0];
const float x1 = half[1];
vertices.push_back({x0, -1.0f});
vertices.push_back({x1, -1.0f});
vertices.push_back({x1, 1.0f});
vertices.push_back({x0, -1.0f});
vertices.push_back({x1, 1.0f});
vertices.push_back({x0, 1.0f});
}
return vertices;
}
// GL's DrawArraysIndirectCommand / DrawElementsIndirectCommand, spelled out
// so a test can write one without depending on a GL header's struct.
struct ArraysCommand {
std::uint32_t count, instanceCount, first, baseInstance;
};
struct ElementsCommand {
std::uint32_t count, instanceCount, firstIndex;
std::int32_t baseVertex;
std::uint32_t baseInstance;
};
class DrawParametersScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
std::string error;
m_program = CompileProgram(kVertexSource, kFragmentSource, &error);
ASSERT_NE(m_program, 0u) << error;
const std::vector<Vertex> vertices = SceneVertices();
glGenVertexArrays(1, &m_vao);
glBindVertexArray(m_vao);
glGenBuffers(1, &m_vbo);
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(vertices.size() * sizeof(Vertex)),
vertices.data(), GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), reinterpret_cast<const void*>(0));
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "scene setup left a GL error behind";
}
void TearDown() override {
if (!Ready()) return;
for (GLuint* buffer : {&m_ebo, &m_indirect, &m_parameter, &m_vbo}) {
if (*buffer != 0) glDeleteBuffers(1, buffer);
*buffer = 0;
}
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
if (m_program != 0) glDeleteProgram(m_program);
}
template <typename T>
void FillBuffer(GLuint& name, GLenum target, const std::vector<T>& data) {
if (name == 0) glGenBuffers(1, &name);
glBindBuffer(target, name);
glBufferData(target, static_cast<GLsizeiptr>(data.size() * sizeof(T)), data.data(), GL_STATIC_DRAW);
}
// Clears to white, runs `draw` and reads the frame back.
template <typename DrawFn>
Image Render(DrawFn&& draw) {
BindDefaultFramebuffer();
glViewport(0, 0, HeadlessGL::Get().Width(), HeadlessGL::Get().Height());
ClearTo(1.0f, 1.0f, 1.0f, 1.0f);
glUseProgram(m_program);
glBindVertexArray(m_vao);
draw();
return ReadPixels(HeadlessGL::Get().Width(), HeadlessGL::Get().Height());
}
// The three builtins as the shader saw them, at a point in one half of
// the viewport. `half` is 0 for the left half and 1 for the right.
struct DrawParams {
int baseVertex = -1, baseInstance = -1, drawId = -1;
};
// Decodes the biased 8-units-per-count encoding back to the integer the
// shader saw. Rounding to the nearest step absorbs any UNORM slop; adjacent
// values stay eight units apart, so an off-by-one still reads as one, and a
// negative value lands below the bias and decodes negative rather than
// clamping into a legitimate zero.
static DrawParams ParamsAt(const Image& image, int half) {
const int x = image.Width() * (1 + 2 * half) / 4;
const Rgba8 pixel = image.At(x, image.Height() / 2);
const auto decode = [](std::uint8_t channel) {
return (static_cast<int>(channel) - 16 + 4) / 8;
};
return {decode(pixel.r), decode(pixel.g), decode(pixel.b)};
}
static void ExpectParams(const Image& image, int half, const DrawParams& expected,
const std::string& what) {
const DrawParams actual = ParamsAt(image, half);
EXPECT_EQ(actual.baseVertex, expected.baseVertex)
<< what << ": gl_BaseVertex (half " << half << ")";
EXPECT_EQ(actual.baseInstance, expected.baseInstance)
<< what << ": gl_BaseInstance (half " << half << ")";
EXPECT_EQ(actual.drawId, expected.drawId) << what << ": gl_DrawID (half " << half << ")";
}
GLuint m_program = 0;
GLuint m_vao = 0;
GLuint m_vbo = 0;
GLuint m_ebo = 0;
GLuint m_indirect = 0;
GLuint m_parameter = 0;
};
// ---- the non-indexed forms: gl_BaseVertex is zero, `first` or not ----
// Vulkan's BaseVertex would answer 3 here (the draw's firstVertex); GL's
// must answer 0, because glDrawArrays has no baseVertex parameter at all.
TEST_F(DrawParametersScenario, DrawArraysReportsAZeroBaseVertexDespiteItsFirst) {
if (!Ready()) return;
const Image image = Render([&] { glDrawArrays(GL_TRIANGLES, kLeftFirst, 2 * kHalfCount); });
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
ExpectParams(image, 0, {0, 0, 0}, "glDrawArrays(first=3)");
ExpectParams(image, 1, {0, 0, 0}, "glDrawArrays(first=3)");
}
TEST_F(DrawParametersScenario, DrawArraysInstancedBaseInstanceReportsItsBaseInstance) {
if (!Ready()) return;
const Image image = Render([&] {
glDrawArraysInstancedBaseInstance(GL_TRIANGLES, kLeftFirst, 2 * kHalfCount, 1, 5);
});
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
ExpectParams(image, 0, {0, 5, 0}, "glDrawArraysInstancedBaseInstance(baseInstance=5)");
}
// The base instance of one draw must not survive into the next one. This is
// the shape that broke on DirectGLES: the emulation uniform is per-program
// state, so a draw that never writes it inherits the last writer's value.
TEST_F(DrawParametersScenario, APlainDrawAfterABaseInstancedOneSeesZeroAgain) {
if (!Ready()) return;
const Image image = Render([&] {
glDrawArraysInstancedBaseInstance(GL_TRIANGLES, kLeftFirst, 2 * kHalfCount, 1, 7);
glDrawArrays(GL_TRIANGLES, kLeftFirst, 2 * kHalfCount);
});
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
ExpectParams(image, 0, {0, 0, 0}, "plain glDrawArrays after a base-instanced draw");
}
// ---- the indexed forms: gl_BaseVertex IS the base vertex ----
TEST_F(DrawParametersScenario, DrawElementsBaseVertexReportsItsBaseVertex) {
if (!Ready()) return;
std::vector<std::uint32_t> indices;
for (std::uint32_t i = 0; i < 2 * kHalfCount; ++i) indices.push_back(i);
FillBuffer(m_ebo, GL_ELEMENT_ARRAY_BUFFER, indices);
const Image image = Render([&] {
glDrawElementsBaseVertex(GL_TRIANGLES, 2 * kHalfCount, GL_UNSIGNED_INT,
reinterpret_cast<const void*>(0), kLeftFirst);
});
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
ExpectParams(image, 0, {kLeftFirst, 0, 0}, "glDrawElementsBaseVertex(basevertex=3)");
ExpectParams(image, 1, {kLeftFirst, 0, 0}, "glDrawElementsBaseVertex(basevertex=3)");
}
// ... and is zero again for the command that has none, including after one
// that did: the same leak the base instance has, on the other builtin. The
// preceding draw MUST carry a non-zero base vertex or this case proves nothing -
// one index run reaches the geometry through the base vertex, the second through
// its own indices, so the two draws paint the same picture with different
// gl_BaseVertex and only the second one's value survives in the framebuffer.
TEST_F(DrawParametersScenario, DrawElementsAfterABaseVertexDrawReportsZeroAgain) {
if (!Ready()) return;
std::vector<std::uint32_t> indices;
for (std::uint32_t i = 0; i < 2 * kHalfCount; ++i) indices.push_back(i);
for (std::uint32_t i = 0; i < 2 * kHalfCount; ++i) indices.push_back(i + kLeftFirst);
FillBuffer(m_ebo, GL_ELEMENT_ARRAY_BUFFER, indices);
const auto rebasedRun = reinterpret_cast<const void*>(2 * kHalfCount * sizeof(std::uint32_t));
const Image image = Render([&] {
glDrawElementsBaseVertex(GL_TRIANGLES, 2 * kHalfCount, GL_UNSIGNED_INT,
reinterpret_cast<const void*>(0), kLeftFirst);
glDrawElements(GL_TRIANGLES, 2 * kHalfCount, GL_UNSIGNED_INT, rebasedRun);
});
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
ExpectParams(image, 0, {0, 0, 0}, "glDrawElements after a base-vertex draw");
ExpectParams(image, 1, {0, 0, 0}, "glDrawElements after a base-vertex draw");
}
// ---- the multi-draw forms: one gl_DrawID per sub-draw ----
TEST_F(DrawParametersScenario, MultiDrawArraysNumbersItsSubDraws) {
if (!Ready()) return;
const GLint firsts[2] = {kLeftFirst, kRightFirst};
const GLsizei counts[2] = {kHalfCount, kHalfCount};
const Image image = Render([&] { glMultiDrawArrays(GL_TRIANGLES, firsts, counts, 2); });
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
ExpectParams(image, 0, {0, 0, 0}, "glMultiDrawArrays sub-draw 0");
ExpectParams(image, 1, {0, 0, 1}, "glMultiDrawArrays sub-draw 1");
}
// Every field of an indexed indirect command at once: its own gl_DrawID, the
// baseVertex word (which the CPU reads out of the command) and the
// baseInstance word (which DirectGLES reads through a storage-buffer view of
// the very same buffer).
TEST_F(DrawParametersScenario, MultiDrawElementsIndirectCarriesEveryCommandsParameters) {
if (!Ready()) return;
std::vector<std::uint32_t> indices;
for (std::uint32_t i = 0; i < kHalfCount; ++i) indices.push_back(i);
FillBuffer(m_ebo, GL_ELEMENT_ARRAY_BUFFER, indices);
const std::vector<ElementsCommand> commands = {
{kHalfCount, 1, 0, kLeftFirst, 0},
{kHalfCount, 1, 0, kRightFirst, 4},
};
FillBuffer(m_indirect, GL_DRAW_INDIRECT_BUFFER, commands);
const Image image = Render([&] {
glMultiDrawElementsIndirect(GL_TRIANGLES, GL_UNSIGNED_INT, reinterpret_cast<const void*>(0), 2,
sizeof(ElementsCommand));
});
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
ExpectParams(image, 0, {kLeftFirst, 0, 0}, "indirect command 0");
ExpectParams(image, 1, {kRightFirst, 4, 1}, "indirect command 1");
}
// glMultiDrawArraysIndirectCount was missing from the DirectGLES backend
// table entirely, so the frontend answered INVALID_OPERATION for every call
// while GL_ARB_indirect_parameters was advertised. The parameter buffer here
// holds a count SMALLER than maxdrawcount, so a path that ignores it draws a
// third command over the top of the second and changes the right half.
TEST_F(DrawParametersScenario, MultiDrawArraysIndirectCountObeysItsParameterBuffer) {
if (!Ready()) return;
const std::vector<ArraysCommand> commands = {
{kHalfCount, 1, kLeftFirst, 0},
{kHalfCount, 1, kRightFirst, 6},
{kHalfCount, 1, kRightFirst, 9},
};
FillBuffer(m_indirect, GL_DRAW_INDIRECT_BUFFER, commands);
const std::vector<std::uint32_t> parameters = {2};
FillBuffer(m_parameter, GL_PARAMETER_BUFFER, parameters);
const Image image = Render([&] {
glMultiDrawArraysIndirectCount(GL_TRIANGLES, reinterpret_cast<const void*>(0), 0, 3,
sizeof(ArraysCommand));
});
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
ExpectParams(image, 0, {0, 0, 0}, "counted indirect command 0");
ExpectParams(image, 1, {0, 6, 1}, "counted indirect command 1");
}
} // namespace
} // namespace MGITest
@@ -0,0 +1,314 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ImageFormatQualifierScenario.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Scenario - AN IMAGE UNIFORM THAT DECLARES NO FORMAT.
//
// Desktop GLSL 4.2 lets a writeonly image declaration omit its format layout qualifier:
//
// writeonly uniform uimage2D uni_image; // legal desktop GLSL
//
// GLSL ES has no such relaxation; every image uniform must carry one, and Adreno says so as "all
// images have to define layout format", which fails the whole program. That is what took the
// compute half of KHR-GL4x.packed_depth_stencil.stencil_texturing.
//
// The only qualifier that is CORRECT to substitute is whatever glBindImageTexture named for the
// unit that uniform addresses - GL requires the qualifier, the bind format and the texture's
// internal format to belong to one format class - so the format is not knowable when the shader
// is compiled, only when it is drawn with. Espryt therefore BAKES it into the program it
// generates and keys that program on the (unit, format) pairs it baked
// (BackendProgramObjectImpl::ImageUnitFormatsStillMatch, MG_Backend/DirectGLES).
//
// Three separate things follow from "the program is built against live binding state", and each
// one is a case below:
//
// 1. the format reaches the shader at all, so the store lands where the texture is (Writes);
// 2. binding a DIFFERENT format to the same unit rebuilds the program, rather than reusing one
// compiled against the old format (RebindToADifferentFormatRebuilds);
// 3. an image bound for the FIRST time after the link works, i.e. the program built against
// "nothing bound yet" is not the one the dispatch runs (FirstBindAfterLinkRebuilds).
//
// Magma needs none of this - Vulkan takes an Unknown-format storage image given
// shaderStorageImageWriteWithoutFormat, and the view format is resolved from the same bind state
// at descriptor time - so every case here runs on both backends and must agree, which is what
// makes the ES-only machinery falsifiable rather than merely exercised.
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr int kExtent = 4;
// The image unit is deliberately NOT 0 and the uniform declares no binding, so the unit
// has to travel through glUniform1i and be baked into the ESSL alongside the format -
// the two bakes share a rebuild key and a bug in either shows up as the wrong texel.
constexpr GLint kImageUnit = 1;
// KHR-GL4x.packed_depth_stencil.stencil_texturing's own image declaration, verbatim.
const char* kStoreSource = R"(#version 430 core
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
writeonly uniform uimage2D uni_image;
void main()
{
imageStore(uni_image, ivec2(gl_GlobalInvocationID.xy), uvec4(gl_GlobalInvocationID.x + 100u, 0u, 0u, 0u));
}
)";
class ImageFormatQualifierScenario : public ScenarioTest {
protected:
void TearDown() override {
if (!Ready()) return;
glUseProgram(0);
for (GLuint p : m_programs) glDeleteProgram(p);
for (GLuint t : m_textures) glDeleteTextures(1, &t);
m_programs.clear();
m_textures.clear();
GLint maxImageUnits = 0;
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
for (GLint unit = 0; unit < maxImageUnits; ++unit) {
glBindImageTexture(static_cast<GLuint>(unit), 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_R32UI);
}
while (glGetError() != GL_NO_ERROR) {
}
}
bool ImagesAreUsable() const {
GLint maxImageUnits = 0;
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
GLint maxComputeImageUniforms = 0;
glGetIntegerv(GL_MAX_COMPUTE_IMAGE_UNIFORMS, &maxComputeImageUniforms);
while (glGetError() != GL_NO_ERROR) {
}
return maxImageUnits > kImageUnit && maxComputeImageUniforms >= 1;
}
GLuint MakeComputeProgram(const std::string& source) {
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
const char* text = source.c_str();
glShaderSource(shader, 1, &text, nullptr);
glCompileShader(shader);
GLint compiled = GL_FALSE;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (compiled == GL_FALSE) {
char log[4096] = {};
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
ADD_FAILURE() << "the compute shader did not compile: " << log;
glDeleteShader(shader);
return 0;
}
const GLuint program = glCreateProgram();
m_programs.push_back(program);
glAttachShader(program, shader);
glLinkProgram(program);
glDeleteShader(shader);
GLint linked = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
if (linked == GL_FALSE) {
char log[4096] = {};
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
ADD_FAILURE() << "the compute program did not link: " << log;
return 0;
}
return program;
}
GLuint MakeTexture(GLenum internalFormat) {
GLuint texture = 0;
glGenTextures(1, &texture);
m_textures.push_back(texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexStorage2D(GL_TEXTURE_2D, 1, internalFormat, kExtent, kExtent);
if (const GLenum error = FirstGLError()) {
ADD_FAILURE() << "allocating storage errored with " << GLErrorName(error);
return 0;
}
// Seeded to a value no dispatch writes, so "the store never happened" and "the
// store wrote the right thing" cannot be confused.
const std::vector<GLuint> zeros(static_cast<std::size_t>(kExtent) * kExtent * 4u, 0u);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kExtent, kExtent,
internalFormat == GL_RGBA32UI ? GL_RGBA_INTEGER : GL_RED_INTEGER, GL_UNSIGNED_INT,
zeros.data());
while (glGetError() != GL_NO_ERROR) {
}
return texture;
}
// Texel (x, 0) of the texture's red channel, read back through the GL frontend rather
// than through a second image uniform: a defect in the format bake would be shared by
// a reader declared the same way and could cancel itself out.
GLuint ReadRedTexel(GLuint texture, GLenum internalFormat, int x) {
const bool rgba = internalFormat == GL_RGBA32UI;
std::vector<GLuint> texels(static_cast<std::size_t>(kExtent) * kExtent * (rgba ? 4u : 1u),
0xFFFFFFFFu);
glBindTexture(GL_TEXTURE_2D, texture);
glGetTexImage(GL_TEXTURE_2D, 0, rgba ? GL_RGBA_INTEGER : GL_RED_INTEGER, GL_UNSIGNED_INT,
texels.data());
if (const GLenum error = FirstGLError()) {
ADD_FAILURE() << "reading the image back errored with " << GLErrorName(error);
return 0xFFFFFFFFu;
}
return texels[static_cast<std::size_t>(x) * (rgba ? 4u : 1u)];
}
void DispatchStore(GLuint program, GLuint texture, GLenum internalFormat) {
glBindImageTexture(static_cast<GLuint>(kImageUnit), texture, 0, GL_FALSE, 0, GL_WRITE_ONLY,
internalFormat);
ASSERT_EQ(FirstGLError(), 0u) << "glBindImageTexture errored";
glUseProgram(program);
const GLint location = glGetUniformLocation(program, "uni_image");
ASSERT_GE(location, 0) << "the image uniform was not reflected";
glUniform1i(location, kImageUnit);
ASSERT_EQ(FirstGLError(), 0u) << "assigning the image unit errored";
glDispatchCompute(kExtent, 1, 1);
glMemoryBarrier(GL_ALL_BARRIER_BITS);
EXPECT_EQ(FirstGLError(), 0u) << "the dispatch leaked a GL error";
glUseProgram(0);
}
std::vector<GLuint> m_programs;
std::vector<GLuint> m_textures;
};
// The defect itself. Without the bake the ES driver refuses the program outright and the
// texture keeps its seed - which is also exactly what a silently no-op dispatch looks
// like, and why the seed is a value no store writes.
TEST_F(ImageFormatQualifierScenario, AFormatlessWriteonlyImageWrites) {
if (!Ready()) GTEST_SKIP() << "no GL context";
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
const GLuint program = MakeComputeProgram(kStoreSource);
const GLuint texture = MakeTexture(GL_R32UI);
if (program == 0 || texture == 0) return;
DispatchStore(program, texture, GL_R32UI);
for (int x = 0; x < kExtent; ++x) {
EXPECT_EQ(ReadRedTexel(texture, GL_R32UI, x), static_cast<GLuint>(x) + 100u)
<< "texel " << x << " of a format-less writeonly image did not take the store";
}
}
// The rebuild key. The SAME program is dispatched twice with a different format bound to
// its unit; a build keyed only on the link (or only on the image UNIT) would reuse the
// r32ui program for the rgba32ui texture, and the second half would come back seeded.
//
// What the SOFTWARE lanes cannot falsify: with the key disabled this case still passes on
// Mesa, because the reused r32ui declaration writes the red channel of an RGBA32UI image
// anyway - a format-class mismatch GL leaves undefined and that driver happens to absorb.
// FirstBindAfterLinkRebuilds below is the case that fails there, because the reused
// program was built with no format at all and never compiled. Both are kept: this one is
// the shape a strict driver is entitled to reject, and it is the shape the device runs.
TEST_F(ImageFormatQualifierScenario, RebindToADifferentFormatRebuilds) {
if (!Ready()) GTEST_SKIP() << "no GL context";
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
const GLuint program = MakeComputeProgram(kStoreSource);
const GLuint first = MakeTexture(GL_R32UI);
const GLuint second = MakeTexture(GL_RGBA32UI);
if (program == 0 || first == 0 || second == 0) return;
DispatchStore(program, first, GL_R32UI);
for (int x = 0; x < kExtent; ++x) {
ASSERT_EQ(ReadRedTexel(first, GL_R32UI, x), static_cast<GLuint>(x) + 100u)
<< "the first format must work before the rebind can be blamed for anything";
}
DispatchStore(program, second, GL_RGBA32UI);
for (int x = 0; x < kExtent; ++x) {
EXPECT_EQ(ReadRedTexel(second, GL_RGBA32UI, x), static_cast<GLuint>(x) + 100u)
<< "texel " << x << ": the program was not rebuilt for the newly bound format";
}
// ...and back, so the rebuild is not a one-way door: returning to a format the
// program was once built against must build for it again, not resurrect a cache row.
const GLuint third = MakeTexture(GL_R32UI);
if (third == 0) return;
DispatchStore(program, third, GL_R32UI);
for (int x = 0; x < kExtent; ++x) {
EXPECT_EQ(ReadRedTexel(third, GL_R32UI, x), static_cast<GLuint>(x) + 100u)
<< "texel " << x << ": going back to the first format did not rebuild";
}
}
// Nothing is bound to the unit when the program links, so whatever the first build sees
// is not the format the dispatch needs. glBindImageTexture must not itself trigger a
// build - it is an entry point, and building there is the constraint
// glShaderStorageBlockBinding is held to as well - so the rebuild has to happen at the
// next dispatch preparation instead. This case fails either way round: no rebuild, or a
// build attempted from the entry point before the state settles.
TEST_F(ImageFormatQualifierScenario, FirstBindAfterLinkRebuilds) {
if (!Ready()) GTEST_SKIP() << "no GL context";
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
const GLuint program = MakeComputeProgram(kStoreSource);
if (program == 0) return;
// Use it once with NOTHING bound to the unit, which is what makes the backend build
// against an empty binding. The dispatch writes nowhere and must not error.
glUseProgram(program);
const GLint location = glGetUniformLocation(program, "uni_image");
ASSERT_GE(location, 0);
glUniform1i(location, kImageUnit);
glDispatchCompute(kExtent, 1, 1);
glMemoryBarrier(GL_ALL_BARRIER_BITS);
EXPECT_EQ(FirstGLError(), 0u) << "dispatching with an unbound image unit must not error";
glUseProgram(0);
const GLuint texture = MakeTexture(GL_R32UI);
if (texture == 0) return;
DispatchStore(program, texture, GL_R32UI);
for (int x = 0; x < kExtent; ++x) {
EXPECT_EQ(ReadRedTexel(texture, GL_R32UI, x), static_cast<GLuint>(x) + 100u)
<< "texel " << x << ": the first bind after the link did not reach the shader";
}
}
// A DECLARED format is authoritative and the bake must never touch it - including when
// the texture behind the unit has a different (but class-compatible) internal format,
// which GL explicitly allows. If the bake ever overrode a declaration, this is the case
// that would go wrong while every other one stayed green.
TEST_F(ImageFormatQualifierScenario, ADeclaredFormatStillWins) {
if (!Ready()) GTEST_SKIP() << "no GL context";
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
const GLuint program = MakeComputeProgram(R"(#version 430 core
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
layout (r32ui) writeonly uniform uimage2D uni_image;
void main()
{
imageStore(uni_image, ivec2(gl_GlobalInvocationID.xy), uvec4(gl_GlobalInvocationID.x + 100u, 0u, 0u, 0u));
}
)");
const GLuint texture = MakeTexture(GL_R32UI);
if (program == 0 || texture == 0) return;
DispatchStore(program, texture, GL_R32UI);
for (int x = 0; x < kExtent; ++x) {
EXPECT_EQ(ReadRedTexel(texture, GL_R32UI, x), static_cast<GLuint>(x) + 100u)
<< "texel " << x << ": a declared format stopped working";
}
}
} // namespace
} // namespace MGITest
@@ -0,0 +1,550 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ImageTargetKindScenario.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Scenario - ONE IMAGE TARGET KIND AT A TIME, THROUGH A COMPUTE DISPATCH.
//
// KHR-GL44.multi_bind.dispatch_bind_image_textures decomposed. That conformance case declares
// ELEVEN image uniforms of eleven different target kinds in one compute shader, binds a texture
// of the matching kind to each unit, sums one texel from every one of them and compares the sum
// against N*(N-1)/2. It is a single pass/fail bit over eleven independent mechanisms: if any one
// of them is wrong - or merely fails to compile - the case fails and says nothing about which.
// That is what it did here, on both backends, for two waves.
//
// So the eleven are pulled apart into one case each. Each case declares ONE image uniform, binds
// ONE texture and checks the value that comes back, so a failure names the target kind and the
// direction. What the conformance case does with eleven at once, AllKindsInOneProgram at the
// bottom still does - a defect that only appears when several kinds share a program is invisible
// to the single-kind cases by construction.
//
// The shape is deliberately the conformance case's own, not a cleaner equivalent:
//
// * r32ui / GL_R32UI throughout, 6x6x6 storage, one level, texel (0,0,0) read;
// * `layout (location = N, r32ui) readonly uniform` - an explicit uniform LOCATION, not a
// binding, with the image unit then assigned by glUniform1i. That combination is the one ES
// cannot express directly, because ES forbids glUniform1i on an image uniform and the unit
// has to be baked into the generated ESSL (RebindImageUniformsToFrontendUnits);
// * `layout (std140, ...) buffer` for the result block - legal, but unusual enough that a
// frontend could plausibly mishandle it. Mirroring it means a green scenario cannot be green
// for a reason the conformance case excludes;
// * glBindImageTexture with layered = GL_TRUE, which is what glBindImageTextures is specified
// to pass, and which is where a target kind whose layeredness a backend does not recognise
// goes wrong.
//
// MULTISAMPLE is the one kind that is not merely an emulation problem, and the conformance case
// already knows it: it reads GL_MAX_IMAGE_SAMPLES and, when that is zero, substitutes a plain 2D
// texture and a plain uimage2D for both multisample entries. MobileGL reports zero, so the
// conformance case never asks it for a multisample image at all. The two cases below are kept
// and skip on that same query, so the coverage is already written the day a backend advertises
// them - and so the skip is a standing record of WHY the conformance case passes without them.
#include <algorithm>
#include <cstddef>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
// The conformance case's own dimensions: one level, 6 on every axis (which is also
// exactly one cube's worth for a cube array), and a single texel read at the origin.
constexpr int kExtent = 6;
constexpr GLuint kFilledValue = 7u;
constexpr GLuint kStoredValue = 13u;
// Everything that differs between the eleven kinds, in one row.
struct TargetKind {
const char* name; // this scenario's name for it, which failure messages carry
GLenum target; // the GL texture target
const char* imageType; // the GLSL image uniform type
const char* coord; // the coordinate expression imageLoad/imageStore takes
bool multisample; // needs GL_MAX_IMAGE_SAMPLES > 0
bool buffer; // storage comes from a buffer object, not TexStorage
};
constexpr TargetKind kKind1D{"1D", GL_TEXTURE_1D, "uimage1D", "0", false, false};
constexpr TargetKind kKind1DArray{"1DArray", GL_TEXTURE_1D_ARRAY, "uimage1DArray", "ivec2(0, 0)", false,
false};
constexpr TargetKind kKind2D{"2D", GL_TEXTURE_2D, "uimage2D", "ivec2(0, 0)", false, false};
constexpr TargetKind kKind2DArray{"2DArray", GL_TEXTURE_2D_ARRAY, "uimage2DArray", "ivec3(0, 0, 0)", false,
false};
constexpr TargetKind kKind3D{"3D", GL_TEXTURE_3D, "uimage3D", "ivec3(0, 0, 0)", false, false};
constexpr TargetKind kKindBuffer{"Buffer", GL_TEXTURE_BUFFER, "uimageBuffer", "0", false, true};
constexpr TargetKind kKindCube{"Cube", GL_TEXTURE_CUBE_MAP, "uimageCube", "ivec3(0, 0, 0)", false, false};
constexpr TargetKind kKindCubeArray{"CubeArray", GL_TEXTURE_CUBE_MAP_ARRAY, "uimageCubeArray",
"ivec3(0, 0, 0)", false, false};
constexpr TargetKind kKindRect{"Rect", GL_TEXTURE_RECTANGLE, "uimage2DRect", "ivec2(0, 0)", false, false};
constexpr TargetKind kKind2DMS{"2DMS", GL_TEXTURE_2D_MULTISAMPLE, "uimage2DMS", "ivec2(0, 0)", true, false};
constexpr TargetKind kKind2DMSArray{"2DMSArray", GL_TEXTURE_2D_MULTISAMPLE_ARRAY, "uimage2DMSArray",
"ivec3(0, 0, 0)", true, false};
// A multisample image load/store takes the sample index as an extra argument; no other
// kind does. Keeping that in one place stops the two spellings drifting apart.
std::string LoadExpression(const TargetKind& kind, const std::string& name) {
return "imageLoad(" + name + ", " + kind.coord + (kind.multisample ? ", 0)" : ")");
}
std::string StoreStatement(const TargetKind& kind, const std::string& name, const char* value) {
return "imageStore(" + name + ", " + kind.coord + (kind.multisample ? ", 0, uvec4(" : ", uvec4(") +
value + ", 0, 0, 0));";
}
const char* kComputePrologue = "#version 440 core\n"
"\n"
"layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;\n"
"\n";
const char* kResultBlock = "layout (std140, binding = 0) buffer SSB {\n"
" uint sum;\n"
"} ssb;\n"
"\n";
// The conformance case's shader, narrowed to a single image.
std::string SingleLoadSource(const TargetKind& kind) {
return std::string(kComputePrologue) + "layout (location = 0, r32ui) readonly uniform " + kind.imageType +
" i0;\n" + kResultBlock + "void main()\n{\n uvec4 v = " + LoadExpression(kind, "i0") +
";\n ssb.sum = v.r;\n}\n";
}
// The other direction. Written as its own program rather than a read-write one so that a
// backend which gets the store right and the load wrong (or the reverse) is not able to
// cancel its own defect out.
std::string SingleStoreSource(const TargetKind& kind) {
return std::string(kComputePrologue) + "layout (location = 0, r32ui) writeonly uniform " +
kind.imageType + " i0;\n\nvoid main()\n{\n " + StoreStatement(kind, "i0", "13u") + "\n}\n";
}
class ImageTargetKindScenario : public ScenarioTest {
protected:
void TearDown() override {
if (!Ready()) return;
glUseProgram(0);
for (GLuint p : m_programs) glDeleteProgram(p);
for (GLuint t : m_textures) glDeleteTextures(1, &t);
for (GLuint b : m_buffers) glDeleteBuffers(1, &b);
m_programs.clear();
m_textures.clear();
m_buffers.clear();
// Leave no image unit bound. These scenarios share one context, and a stale image
// binding is exactly the kind of state that makes the NEXT scenario's failure
// impossible to reproduce on its own.
GLint maxImageUnits = 0;
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
for (GLint unit = 0; unit < maxImageUnits; ++unit) {
glBindImageTexture(static_cast<GLuint>(unit), 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_R32UI);
}
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, 0);
while (glGetError() != GL_NO_ERROR) {
}
}
bool ImagesAreUsable() const {
GLint maxImageUnits = 0;
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
GLint maxComputeImageUniforms = 0;
glGetIntegerv(GL_MAX_COMPUTE_IMAGE_UNIFORMS, &maxComputeImageUniforms);
while (glGetError() != GL_NO_ERROR) {
}
return maxImageUnits >= 1 && maxComputeImageUniforms >= 1;
}
// The conformance case's own multisample gate, asked the same way it asks it.
bool MultisampleImagesAreUsable() const {
GLint maxImageSamples = 0;
glGetIntegerv(GL_MAX_IMAGE_SAMPLES, &maxImageSamples);
while (glGetError() != GL_NO_ERROR) {
}
return maxImageSamples > 0;
}
GLuint MakeComputeProgram(const std::string& source) {
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
const char* text = source.c_str();
glShaderSource(shader, 1, &text, nullptr);
glCompileShader(shader);
GLint compiled = GL_FALSE;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (compiled == GL_FALSE) {
char log[4096] = {};
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
ADD_FAILURE() << "the compute shader did not compile: " << log << "\nsource:\n" << source;
glDeleteShader(shader);
return 0;
}
const GLuint program = glCreateProgram();
m_programs.push_back(program);
glAttachShader(program, shader);
glLinkProgram(program);
glDeleteShader(shader);
GLint linked = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
if (linked == GL_FALSE) {
char log[4096] = {};
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
ADD_FAILURE() << "the compute program did not link: " << log << "\nsource:\n" << source;
return 0;
}
return program;
}
// Storage plus a full fill with `value`, in the spelling each target kind needs.
// Returns 0 - having already reported - when the target could not be created.
GLuint MakeTexture(const TargetKind& kind, bool fill, GLuint value = kFilledValue) {
const std::vector<GLuint> texels(static_cast<std::size_t>(kExtent) * kExtent * kExtent, value);
if (kind.buffer) {
GLuint buffer = 0;
glGenBuffers(1, &buffer);
m_buffers.push_back(buffer);
glBindBuffer(GL_TEXTURE_BUFFER, buffer);
glBufferData(GL_TEXTURE_BUFFER, static_cast<GLsizeiptr>(texels.size() * sizeof(GLuint)),
fill ? texels.data() : nullptr, GL_DYNAMIC_COPY);
GLuint texture = 0;
glGenTextures(1, &texture);
m_textures.push_back(texture);
glBindTexture(GL_TEXTURE_BUFFER, texture);
glTexBuffer(GL_TEXTURE_BUFFER, GL_R32UI, buffer);
if (const GLenum error = FirstGLError()) {
ADD_FAILURE() << kind.name << ": creating the texture buffer errored with "
<< GLErrorName(error);
return 0;
}
return texture;
}
GLuint texture = 0;
glGenTextures(1, &texture);
m_textures.push_back(texture);
glBindTexture(kind.target, texture);
switch (kind.target) {
case GL_TEXTURE_1D:
glTexStorage1D(kind.target, 1, GL_R32UI, kExtent);
break;
case GL_TEXTURE_2D:
case GL_TEXTURE_RECTANGLE:
case GL_TEXTURE_1D_ARRAY:
case GL_TEXTURE_CUBE_MAP:
glTexStorage2D(kind.target, 1, GL_R32UI, kExtent, kExtent);
break;
case GL_TEXTURE_2D_ARRAY:
case GL_TEXTURE_3D:
case GL_TEXTURE_CUBE_MAP_ARRAY:
glTexStorage3D(kind.target, 1, GL_R32UI, kExtent, kExtent, kExtent);
break;
case GL_TEXTURE_2D_MULTISAMPLE:
glTexStorage2DMultisample(kind.target, 1, GL_R32UI, kExtent, kExtent, GL_FALSE);
break;
case GL_TEXTURE_2D_MULTISAMPLE_ARRAY:
glTexStorage3DMultisample(kind.target, 1, GL_R32UI, kExtent, kExtent, kExtent, GL_FALSE);
break;
default:
ADD_FAILURE() << kind.name << ": no storage spelling for target 0x" << std::hex << kind.target;
return 0;
}
if (const GLenum error = FirstGLError()) {
ADD_FAILURE() << kind.name << ": allocating storage errored with " << GLErrorName(error);
return 0;
}
// A multisample texture has no TexSubImage - the conformance case fills it with a
// compute pass, which is what the store cases below do.
if (!fill || kind.multisample) return texture;
switch (kind.target) {
case GL_TEXTURE_1D:
glTexSubImage1D(kind.target, 0, 0, kExtent, GL_RED_INTEGER, GL_UNSIGNED_INT, texels.data());
break;
case GL_TEXTURE_2D:
case GL_TEXTURE_RECTANGLE:
case GL_TEXTURE_1D_ARRAY:
glTexSubImage2D(kind.target, 0, 0, 0, kExtent, kExtent, GL_RED_INTEGER, GL_UNSIGNED_INT,
texels.data());
break;
case GL_TEXTURE_CUBE_MAP:
for (int face = 0; face < 6; ++face) {
glTexSubImage2D(static_cast<GLenum>(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face), 0, 0, 0, kExtent,
kExtent, GL_RED_INTEGER, GL_UNSIGNED_INT, texels.data());
}
break;
case GL_TEXTURE_2D_ARRAY:
case GL_TEXTURE_3D:
case GL_TEXTURE_CUBE_MAP_ARRAY:
glTexSubImage3D(kind.target, 0, 0, 0, 0, kExtent, kExtent, kExtent, GL_RED_INTEGER,
GL_UNSIGNED_INT, texels.data());
break;
default:
break;
}
if (const GLenum error = FirstGLError()) {
ADD_FAILURE() << kind.name << ": uploading texels errored with " << GLErrorName(error);
return 0;
}
return texture;
}
// A 4-byte `buffer` block bound to base 0, which is where every case puts its answer.
GLuint MakeResultBuffer() {
GLuint ssbo = 0;
glGenBuffers(1, &ssbo);
m_buffers.push_back(ssbo);
const GLuint zero = 0u;
glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo);
glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(GLuint), &zero, GL_DYNAMIC_COPY);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, ssbo);
return ssbo;
}
GLuint ReadResult(GLuint ssbo) {
glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo);
GLuint value = 0xFFFFFFFFu;
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, sizeof(GLuint), &value);
return value;
}
// Fill a texture of `kind`, read texel (0,0,0) of it through an image uniform in a
// compute dispatch, and require the value back.
void RunLoadCase(const TargetKind& kind) {
const GLuint program = MakeComputeProgram(SingleLoadSource(kind));
if (program == 0) return;
const GLuint texture = MakeTexture(kind, true);
if (texture == 0) return;
const GLuint ssbo = MakeResultBuffer();
glBindImageTexture(0, texture, 0, GL_TRUE, 0, GL_READ_ONLY, GL_R32UI);
ASSERT_EQ(FirstGLError(), 0u) << kind.name << ": glBindImageTexture errored";
glUseProgram(program);
// The unit, by LOCATION - the conformance case's own redundant-but-legal
// assignment, and the one ES cannot take at the API level.
glUniform1i(0, 0);
ASSERT_EQ(FirstGLError(), 0u) << kind.name << ": assigning the image unit errored";
glDispatchCompute(1, 1, 1);
glMemoryBarrier(GL_ALL_BARRIER_BITS);
EXPECT_EQ(FirstGLError(), 0u) << kind.name << ": the dispatch leaked a GL error";
EXPECT_EQ(ReadResult(ssbo), kFilledValue)
<< kind.name << ": the compute dispatch did not read the value the texture was filled with";
glUseProgram(0);
}
// The other direction: store through an image uniform, then read the same texel back
// through a SECOND program, so a defect cannot cancel itself out.
void RunStoreCase(const TargetKind& kind) {
const GLuint storeProgram = MakeComputeProgram(SingleStoreSource(kind));
const GLuint loadProgram = MakeComputeProgram(SingleLoadSource(kind));
if (storeProgram == 0 || loadProgram == 0) return;
const GLuint texture = MakeTexture(kind, false);
if (texture == 0) return;
const GLuint ssbo = MakeResultBuffer();
glBindImageTexture(0, texture, 0, GL_TRUE, 0, GL_READ_WRITE, GL_R32UI);
ASSERT_EQ(FirstGLError(), 0u) << kind.name << ": glBindImageTexture errored";
glUseProgram(storeProgram);
glUniform1i(0, 0);
glDispatchCompute(1, 1, 1);
glMemoryBarrier(GL_ALL_BARRIER_BITS);
EXPECT_EQ(FirstGLError(), 0u) << kind.name << ": the storing dispatch leaked a GL error";
glUseProgram(loadProgram);
glUniform1i(0, 0);
glDispatchCompute(1, 1, 1);
glMemoryBarrier(GL_ALL_BARRIER_BITS);
EXPECT_EQ(FirstGLError(), 0u) << kind.name << ": the loading dispatch leaked a GL error";
EXPECT_EQ(ReadResult(ssbo), kStoredValue)
<< kind.name << ": the value stored through the image did not come back";
glUseProgram(0);
}
std::vector<GLuint> m_programs;
std::vector<GLuint> m_textures;
std::vector<GLuint> m_buffers;
};
} // namespace
// ---- the load direction, one target kind per case -----------------------
//
// Exactly what the conformance case does with each of its eleven uniforms, but alone, so a
// failure names the kind.
#define MGL_DEFINE_LOAD_CASE(CaseName, Kind) \
TEST_F(ImageTargetKindScenario, Loads##CaseName) { \
if (!Ready()) return; \
if (!ImagesAreUsable()) GTEST_SKIP() << "no compute image uniforms"; \
if ((Kind).multisample && !MultisampleImagesAreUsable()) { \
GTEST_SKIP() << "GL_MAX_IMAGE_SAMPLES is 0, so the conformance case substitutes a plain 2D image " \
"here and never asks for a multisample one"; \
} \
RunLoadCase(Kind); \
}
#define MGL_DEFINE_STORE_CASE(CaseName, Kind) \
TEST_F(ImageTargetKindScenario, Stores##CaseName) { \
if (!Ready()) return; \
if (!ImagesAreUsable()) GTEST_SKIP() << "no compute image uniforms"; \
if ((Kind).multisample && !MultisampleImagesAreUsable()) { \
GTEST_SKIP() << "GL_MAX_IMAGE_SAMPLES is 0, so the conformance case substitutes a plain 2D image " \
"here and never asks for a multisample one"; \
} \
RunStoreCase(Kind); \
}
MGL_DEFINE_LOAD_CASE(Texture1D, kKind1D)
MGL_DEFINE_LOAD_CASE(Texture1DArray, kKind1DArray)
MGL_DEFINE_LOAD_CASE(Texture2D, kKind2D)
MGL_DEFINE_LOAD_CASE(Texture2DArray, kKind2DArray)
MGL_DEFINE_LOAD_CASE(Texture3D, kKind3D)
MGL_DEFINE_LOAD_CASE(TextureBuffer, kKindBuffer)
MGL_DEFINE_LOAD_CASE(TextureCube, kKindCube)
MGL_DEFINE_LOAD_CASE(TextureCubeArray, kKindCubeArray)
MGL_DEFINE_LOAD_CASE(TextureRectangle, kKindRect)
MGL_DEFINE_LOAD_CASE(Texture2DMultisample, kKind2DMS)
MGL_DEFINE_LOAD_CASE(Texture2DMultisampleArray, kKind2DMSArray)
MGL_DEFINE_STORE_CASE(Texture1D, kKind1D)
MGL_DEFINE_STORE_CASE(Texture1DArray, kKind1DArray)
MGL_DEFINE_STORE_CASE(Texture2D, kKind2D)
MGL_DEFINE_STORE_CASE(Texture2DArray, kKind2DArray)
MGL_DEFINE_STORE_CASE(Texture3D, kKind3D)
MGL_DEFINE_STORE_CASE(TextureBuffer, kKindBuffer)
MGL_DEFINE_STORE_CASE(TextureCube, kKindCube)
MGL_DEFINE_STORE_CASE(TextureCubeArray, kKindCubeArray)
MGL_DEFINE_STORE_CASE(TextureRectangle, kKindRect)
MGL_DEFINE_STORE_CASE(Texture2DMultisample, kKind2DMS)
MGL_DEFINE_STORE_CASE(Texture2DMultisampleArray, kKind2DMSArray)
#undef MGL_DEFINE_LOAD_CASE
#undef MGL_DEFINE_STORE_CASE
// ---- and all of them at once -------------------------------------------
//
// The conformance case's actual shape. The single-kind cases above cannot see a defect that
// needs several kinds in one program - a binding remap that only collides when two image
// types share a descriptor set, a per-kind rewrite that is not idempotent across declarations
// - and that class of defect is precisely what "each kind passes alone but the case still
// fails" would mean.
//
// Each unit is filled with its own DISTINCT value rather than a shared one, so a shortfall
// names WHICH kind is missing rather than merely how many are: with one shared value, "three
// kinds read zero" and "one kind read zero" differ only by a multiple, and any two kinds are
// interchangeable in the total. A sum still cannot see two kinds SWAPPING - addition is
// commutative, and the conformance case has exactly the same blind spot - but the single-kind
// cases above pin each kind to its own texture already, so a swap cannot hide there.
TEST_F(ImageTargetKindScenario, AllKindsInOneProgram) {
if (!Ready()) return;
if (!ImagesAreUsable()) GTEST_SKIP() << "no compute image uniforms";
// The two kinds this whole scenario file exists for come FIRST, and that ordering is
// load-bearing rather than cosmetic. The list has to be truncated to the device's image
// unit count, and the guaranteed minimum is small - ES 3.1 promises only four compute
// image uniforms - so a list in the conformance case's own order would put imageBuffer
// at index five and drop it on exactly the devices most likely to get it wrong. A test
// that quietly stops covering its own subject is worse than one that fails.
const bool multisample = MultisampleImagesAreUsable();
std::vector<TargetKind> kinds{kKind1DArray, kKindBuffer, kKind2D, kKind1D, kKind2DArray,
kKind3D, kKindCube, kKindRect, kKindCubeArray};
if (multisample) {
kinds.push_back(kKind2DMS);
kinds.push_back(kKind2DMSArray);
}
GLint maxComputeImageUniforms = 0;
glGetIntegerv(GL_MAX_COMPUTE_IMAGE_UNIFORMS, &maxComputeImageUniforms);
GLint maxImageUnits = 0;
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
while (glGetError() != GL_NO_ERROR) {
}
const std::size_t count =
std::min<std::size_t>(kinds.size(), static_cast<std::size_t>(std::max(0, std::min(maxComputeImageUniforms,
maxImageUnits))));
if (count == 0) GTEST_SKIP() << "no image units";
// Named, not silently dropped: `expected` is computed over whatever survives, so a
// truncated run is self-consistently green and would otherwise never say what it stopped
// covering.
if (count < kinds.size()) {
std::string dropped;
for (std::size_t i = count; i < kinds.size(); ++i) {
if (!dropped.empty()) dropped += ", ";
dropped += kinds[i].name;
}
RecordProperty("dropped_image_target_kinds", dropped);
GTEST_LOG_(INFO) << "only " << count << " image units, so these kinds are not covered by the "
<< "combined case: " << dropped;
}
kinds.resize(count);
std::string declarations;
std::string sum;
for (std::size_t i = 0; i < kinds.size(); ++i) {
const std::string name = "i" + std::to_string(i);
declarations += "layout (location = " + std::to_string(i) + ", r32ui) readonly uniform " +
kinds[i].imageType + " " + name + ";\n";
if (!sum.empty()) sum += " + ";
sum += LoadExpression(kinds[i], name);
}
const std::string source = std::string(kComputePrologue) + declarations + kResultBlock +
"void main()\n{\n uvec4 v = " + sum + ";\n ssb.sum = v.r;\n}\n";
const GLuint program = MakeComputeProgram(source);
if (program == 0) return;
// Powers of two, so the shortfall's bit pattern names exactly which kinds read zero -
// no other subset of the values can sum to the same total. Eleven kinds at most, so the
// largest is 1 << 10 and the sum cannot approach a uint's range.
GLuint expected = 0;
for (std::size_t i = 0; i < kinds.size(); ++i) {
const GLuint value = 1u << i;
const GLuint texture = MakeTexture(kinds[i], true, value);
if (texture == 0) return;
expected += value;
glBindImageTexture(static_cast<GLuint>(i), texture, 0, GL_TRUE, 0, GL_READ_ONLY, GL_R32UI);
ASSERT_EQ(FirstGLError(), 0u) << kinds[i].name << ": glBindImageTexture errored";
}
const GLuint ssbo = MakeResultBuffer();
glUseProgram(program);
for (std::size_t i = 0; i < kinds.size(); ++i) {
glUniform1i(static_cast<GLint>(i), static_cast<GLint>(i));
}
ASSERT_EQ(FirstGLError(), 0u) << "assigning the image units errored";
glDispatchCompute(1, 1, 1);
glMemoryBarrier(GL_ALL_BARRIER_BITS);
EXPECT_EQ(FirstGLError(), 0u) << "the dispatch leaked a GL error";
const GLuint actual = ReadResult(ssbo);
std::string missing;
for (std::size_t i = 0; i < kinds.size(); ++i) {
if ((actual & (1u << i)) == 0u) {
if (!missing.empty()) missing += ", ";
missing += kinds[i].name;
}
}
EXPECT_EQ(actual, expected)
<< "the sum over " << kinds.size()
<< " image target kinds is wrong; each kind contributes its own bit, and these read "
"zero: "
<< (missing.empty() ? "(none - so some kind read a value it was never given)" : missing);
glUseProgram(0);
}
} // namespace MGITest
@@ -0,0 +1,264 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/VertexArrayEnableDisableScenario.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// KHR-GL45.direct_state_access.vertex_arrays_enable_disable_attributes, rebuilt.
//
// The case is small and does one unusual thing twice: it turns half of
// GL_MAX_VERTEX_ATTRIBS attribute arrays on and the other half off with
// glEnableVertexArrayAttrib / glDisableVertexArrayAttrib on a vertex array object
// that is NOT bound (it binds the default one first, on purpose), draws one point
// through a program that reads exactly the enabled half, and checks the sum those
// arrays produced. Then it swaps which half is enabled, draws again through a
// SECOND program, and checks the other sum.
//
// Both draws capture into ONE four-byte transform feedback buffer, allocated once
// with immutable storage and read back with glMapBuffer - so anything that only
// works on the first capture span through a buffer fails the second check while
// leaving the first one green.
//
// It is reassembled here rather than shortened because every one of those details
// is a candidate: the unbound-VAO enables, the two-program swap, the integer
// attributes fetched with glVertexAttribIPointer at a stride wider than one
// element, the second capture span, and the fact that the sums differ ONLY in
// which arrays contributed (a fetch that ignored the enable state, or one that
// read the wrong element, lands on a different number, not on garbage).
#include <cstdio>
#include <cstring>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
GLuint CompileShader(GLenum type, const std::string& source, std::string* log) {
const GLuint shader = glCreateShader(type);
const char* text = source.c_str();
glShaderSource(shader, 1, &text, nullptr);
glCompileShader(shader);
GLint status = GL_FALSE;
glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
if (status == GL_FALSE) {
GLint length = 0;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
glGetShaderInfoLog(shader, length + 1, nullptr, buffer.data());
if (log != nullptr) *log = buffer.data();
glDeleteShader(shader);
return 0;
}
return shader;
}
// Declares and sums the even (parity 0) or odd (parity 1) attributes only, with the
// locations assigned by glBindAttribLocation rather than a layout qualifier - which is
// what the CTS case does, and which makes the attribute set the program reads a link
// property rather than a source one.
GLuint BuildSumProgram(int parity, int attributeCount, std::string* log) {
std::string declarations;
std::string copies = " sum = 0;\n";
for (int i = parity; i < attributeCount; i += 2) {
declarations += "in int a_" + std::to_string(i) + ";\n";
copies += " sum += a_" + std::to_string(i) + ";\n";
}
// `flat` where the CTS case has none: an integral shader output cannot be
// interpolated, so a driver is within its rights to reject the unqualified form
// even with no matching fragment input. The capture reads the same value either
// way, and the qualifier keeps this scenario portable off llvmpipe.
const std::string vertexSource = "#version 450\n\n" + declarations +
"flat out int sum;\n\nvoid main()\n{\n" + copies + "}\n";
const std::string fragmentSource = R"(#version 450
out vec4 color;
void main()
{
color = vec4(1.0);
}
)";
const GLuint vertexShader = CompileShader(GL_VERTEX_SHADER, vertexSource, log);
if (vertexShader == 0) return 0;
const GLuint fragmentShader = CompileShader(GL_FRAGMENT_SHADER, fragmentSource, log);
if (fragmentShader == 0) {
glDeleteShader(vertexShader);
return 0;
}
const GLuint program = glCreateProgram();
glAttachShader(program, vertexShader);
glAttachShader(program, fragmentShader);
const char* varying = "sum";
glTransformFeedbackVaryings(program, 1, &varying, GL_INTERLEAVED_ATTRIBS);
for (int i = parity; i < attributeCount; i += 2) {
const std::string name = "a_" + std::to_string(i);
glBindAttribLocation(program, static_cast<GLuint>(i), name.c_str());
}
glLinkProgram(program);
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
GLint status = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &status);
if (status == GL_FALSE) {
GLint length = 0;
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
glGetProgramInfoLog(program, length + 1, nullptr, buffer.data());
if (log != nullptr) *log = buffer.data();
glDeleteProgram(program);
return 0;
}
return program;
}
class VertexArrayEnableDisableScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &m_attributeCount);
ASSERT_GE(m_attributeCount, 16);
std::string log;
m_even = BuildSumProgram(0, m_attributeCount, &log);
ASSERT_NE(m_even, 0u) << "even program failed to build: " << log;
m_odd = BuildSumProgram(1, m_attributeCount, &log);
ASSERT_NE(m_odd, 0u) << "odd program failed to build: " << log;
// One element per attribute, read as one vertex whose stride spans them all.
glGenVertexArrays(1, &m_vao);
glBindVertexArray(m_vao);
glGenBuffers(1, &m_vbo);
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
std::vector<GLint> reference(static_cast<std::size_t>(m_attributeCount));
for (int i = 0; i < m_attributeCount; ++i) reference[static_cast<std::size_t>(i)] = i;
glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(reference.size() * sizeof(GLint)),
reference.data(), GL_STATIC_DRAW);
for (int i = 0; i < m_attributeCount; ++i) {
glVertexAttribIPointer(static_cast<GLuint>(i), 1, GL_INT,
static_cast<GLsizei>(sizeof(GLint) * m_attributeCount),
reinterpret_cast<const void*>(static_cast<std::size_t>(i) * sizeof(GLint)));
}
glBindBuffer(GL_ARRAY_BUFFER, 0);
// Immutable storage, allocated once, read back with glMapBuffer - the capture
// buffer is never respecified between the two spans.
glGenBuffers(1, &m_xfb);
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, m_xfb);
glBufferStorage(GL_TRANSFORM_FEEDBACK_BUFFER, sizeof(GLint), nullptr, GL_MAP_READ_BIT);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, m_xfb);
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "capture buffer setup";
}
void TearDown() override {
if (!Ready()) return;
glUseProgram(0);
glBindVertexArray(0);
if (m_xfb != 0) glDeleteBuffers(1, &m_xfb);
if (m_vbo != 0) glDeleteBuffers(1, &m_vbo);
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
if (m_even != 0) glDeleteProgram(m_even);
if (m_odd != 0) glDeleteProgram(m_odd);
ScenarioTest::TearDown();
}
// Enables one parity's arrays and disables the other's, THROUGH THE OBJECT NAME
// while a different vertex array object is bound.
void TurnOnAttributes(int enabledParity) {
glBindVertexArray(0);
for (int i = 0; i < m_attributeCount; ++i) {
if (i % 2 == enabledParity % 2) {
glEnableVertexArrayAttrib(m_vao, static_cast<GLuint>(i));
} else {
glDisableVertexArrayAttrib(m_vao, static_cast<GLuint>(i));
}
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "attribute " << i << ", parity " << enabledParity;
}
glBindVertexArray(m_vao);
}
int ExpectedSum(int parity) const {
int sum = 0;
for (int i = parity; i < m_attributeCount; i += 2) sum += i;
return sum;
}
// One capture span, read back the way the CTS case does.
int DrawAndRead(int parity) {
glUseProgram(parity == 0 ? m_even : m_odd);
glBindVertexArray(m_vao);
glBeginTransformFeedback(GL_POINTS);
glDrawArrays(GL_POINTS, 0, 1);
glEndTransformFeedback();
const void* mapped = glMapBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, GL_READ_ONLY);
if (mapped == nullptr) {
ADD_FAILURE() << "glMapBuffer returned null for parity " << parity;
return -1;
}
GLint result = -1;
std::memcpy(&result, mapped, sizeof(result));
glUnmapBuffer(GL_TRANSFORM_FEEDBACK_BUFFER);
return result;
}
GLint m_attributeCount = 16;
GLuint m_even = 0;
GLuint m_odd = 0;
GLuint m_vao = 0;
GLuint m_vbo = 0;
GLuint m_xfb = 0;
};
// The case verbatim: even half on, draw, check; odd half on, draw, check.
TEST_F(VertexArrayEnableDisableScenario, EitherHalfOfTheAttributesInTurn) {
if (!Ready()) GTEST_SKIP();
TurnOnAttributes(0);
EXPECT_EQ(DrawAndRead(0), ExpectedSum(0)) << "even attributes";
TurnOnAttributes(1);
EXPECT_EQ(DrawAndRead(1), ExpectedSum(1)) << "odd attributes";
EXPECT_EQ(glGetError(), GL_NO_ERROR);
}
// The first span on its own, so a failure of the case above can be read as "the second
// span" rather than "the enables".
TEST_F(VertexArrayEnableDisableScenario, TheEvenHalfAlone) {
if (!Ready()) GTEST_SKIP();
TurnOnAttributes(0);
EXPECT_EQ(DrawAndRead(0), ExpectedSum(0));
EXPECT_EQ(glGetError(), GL_NO_ERROR);
}
// And the odd half as the FIRST span, which separates "the odd program/arrays are
// wrong" from "the second span is wrong".
TEST_F(VertexArrayEnableDisableScenario, TheOddHalfAlone) {
if (!Ready()) GTEST_SKIP();
TurnOnAttributes(1);
EXPECT_EQ(DrawAndRead(1), ExpectedSum(1));
EXPECT_EQ(glGetError(), GL_NO_ERROR);
}
} // namespace
} // namespace MGITest
@@ -0,0 +1,676 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/VertexAttribBindingScenario.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// ARB_vertex_attrib_binding: the separate format/binding state the GL 4.3 vertex
// input model is made of, read back out of the draw that consumed it.
//
// Every scenario here captures the vertex shader's inputs with transform feedback
// under GL_RASTERIZER_DISCARD, which is what the KHR-GL43.vertex_attrib_binding
// cases do: the captured record IS the fetched vertex, so "the binding state did
// not reach the draw" and "the draw fetched the wrong bytes" are distinguishable
// from each other and from "the capture did not run" (the buffer is pre-filled
// with a poison value).
#include <cstdio>
#include <cstring>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr float kPoison = -1234.0f;
GLuint CompileShader(GLenum type, const std::string& source, std::string* log) {
const GLuint shader = glCreateShader(type);
const char* text = source.c_str();
glShaderSource(shader, 1, &text, nullptr);
glCompileShader(shader);
GLint status = GL_FALSE;
glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
if (status == GL_FALSE) {
GLint length = 0;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
glGetShaderInfoLog(shader, length + 1, nullptr, buffer.data());
if (log != nullptr) *log = buffer.data();
glDeleteShader(shader);
return 0;
}
return shader;
}
// A vertex-only capture program, exactly how the CTS builds one: the varying
// names are declared before the link and the fragment stage is absent because
// the draw runs under GL_RASTERIZER_DISCARD.
GLuint BuildCaptureProgram(const std::string& vertexSource, const std::vector<const char*>& xfbVaryings,
std::string* log) {
const GLuint vertexShader = CompileShader(GL_VERTEX_SHADER, vertexSource, log);
if (vertexShader == 0) return 0;
const GLuint program = glCreateProgram();
glAttachShader(program, vertexShader);
if (!xfbVaryings.empty()) {
glTransformFeedbackVaryings(program, static_cast<GLsizei>(xfbVaryings.size()), xfbVaryings.data(),
GL_INTERLEAVED_ATTRIBS);
}
glLinkProgram(program);
glDeleteShader(vertexShader);
GLint status = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &status);
if (status == GL_FALSE) {
GLint length = 0;
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
glGetProgramInfoLog(program, length + 1, nullptr, buffer.data());
if (log != nullptr) *log = buffer.data();
glDeleteProgram(program);
return 0;
}
return program;
}
// Four float inputs at locations 0..3, captured as four vec4s per vertex.
// Locations the test does not feed keep their current-attribute value, which
// every scenario sets to a known constant first.
std::string CaptureVertexSource() {
return R"(#version 430 core
layout(location = 0) in vec4 vs_in_attrib0;
layout(location = 1) in vec4 vs_in_attrib1;
layout(location = 2) in vec4 vs_in_attrib2;
layout(location = 3) in vec4 vs_in_attrib3;
out StageData {
vec4 attrib0;
vec4 attrib1;
vec4 attrib2;
vec4 attrib3;
} vs_out;
void main() {
vs_out.attrib0 = vs_in_attrib0;
vs_out.attrib1 = vs_in_attrib1;
vs_out.attrib2 = vs_in_attrib2;
vs_out.attrib3 = vs_in_attrib3;
}
)";
}
std::vector<const char*> CaptureVaryingNames() {
return {"StageData.attrib0", "StageData.attrib1", "StageData.attrib2", "StageData.attrib3"};
}
// Runs `vertexCount` x `instanceCount` points through the capture program and
// returns the interleaved floats (16 per point: four vec4s).
std::vector<float> CapturePoints(GLuint program, GLuint xfbBuffer, int vertexCount, int instanceCount) {
const std::size_t floats = static_cast<std::size_t>(vertexCount) * instanceCount * 16;
std::vector<float> poison(floats, kPoison);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast<GLsizeiptr>(floats * sizeof(float)), poison.data(),
GL_DYNAMIC_DRAW);
glEnable(GL_RASTERIZER_DISCARD);
glUseProgram(program);
glBeginTransformFeedback(GL_POINTS);
glDrawArraysInstanced(GL_POINTS, 0, vertexCount, instanceCount);
glEndTransformFeedback();
glDisable(GL_RASTERIZER_DISCARD);
std::vector<float> data(floats, kPoison);
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, static_cast<GLsizeiptr>(floats * sizeof(float)),
data.data());
glUseProgram(0);
return data;
}
// As CapturePoints, but through the baseInstance entry point, and on a capture buffer
// of its own.
//
// Kept separate from CapturePoints rather than defaulting a parameter, so that every
// existing caller stays on the draw command that carries no baseInstance at all: the
// negative control is then a DIFFERENT command rather than the same one passed a zero.
//
// The buffer per capture is a leftover. baseInstance was the first thing here that
// needed several captures in ONE test, and at the time a second capture into the same
// buffer object came back empty on DirectVulkan - respecifying a buffer whose bytes the
// backend had handed the frontend a pointer into replaced the storage under that
// pointer, so the capture wrote one store and the readback read another. That is fixed
// and pinned by XfbCaptureBufferReuseScenario, which owns the shape now; a buffer per
// capture is simply the cheapest thing that still isolates these three draws from each
// other.
std::vector<float> CaptureOwnBufferBaseInstance(GLuint program, int vertexCount, int instanceCount,
GLuint baseInstance, bool useBaseInstanceCommand) {
const std::size_t floats = static_cast<std::size_t>(vertexCount) * instanceCount * 16;
std::vector<float> poison(floats, kPoison);
GLuint xfbBuffer = 0;
glGenBuffers(1, &xfbBuffer);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast<GLsizeiptr>(floats * sizeof(float)), poison.data(),
GL_DYNAMIC_DRAW);
glEnable(GL_RASTERIZER_DISCARD);
glUseProgram(program);
glBeginTransformFeedback(GL_POINTS);
if (useBaseInstanceCommand) {
glDrawArraysInstancedBaseInstance(GL_POINTS, 0, vertexCount, instanceCount, baseInstance);
} else {
glDrawArraysInstanced(GL_POINTS, 0, vertexCount, instanceCount);
}
glEndTransformFeedback();
glDisable(GL_RASTERIZER_DISCARD);
std::vector<float> data(floats, kPoison);
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, static_cast<GLsizeiptr>(floats * sizeof(float)),
data.data());
glUseProgram(0);
glDeleteBuffers(1, &xfbBuffer);
return data;
}
// point p, attribute a, component c
float At(const std::vector<float>& data, int point, int attrib, int component) {
const std::size_t index = static_cast<std::size_t>(point) * 16 + attrib * 4 + component;
return index < data.size() ? data[index] : kPoison;
}
void ResetCurrentAttribs() {
for (GLuint i = 0; i < 4; ++i) {
glVertexAttrib4f(i, 0.0f, 0.0f, 0.0f, 0.0f);
}
}
::testing::AssertionResult Vec4Is(const std::vector<float>& data, int point, int attrib, float x, float y,
float z, float w) {
const float gx = At(data, point, attrib, 0);
const float gy = At(data, point, attrib, 1);
const float gz = At(data, point, attrib, 2);
const float gw = At(data, point, attrib, 3);
const float tolerance = 0.01f;
auto close = [tolerance](float a, float b) { return (a - b) < tolerance && (b - a) < tolerance; };
if (close(gx, x) && close(gy, y) && close(gz, z) && close(gw, w)) {
return ::testing::AssertionSuccess();
}
return ::testing::AssertionFailure()
<< "point " << point << " attribute " << attrib << " is (" << gx << ", " << gy << ", " << gz << ", "
<< gw << "), expected (" << x << ", " << y << ", " << z << ", " << w << ")";
}
} // namespace
class VertexAttribBindingScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
m_program = BuildCaptureProgram(CaptureVertexSource(), CaptureVaryingNames(), &m_log);
ASSERT_NE(m_program, 0u) << "capture program did not link: " << m_log;
glGenVertexArrays(1, &m_vao);
glGenBuffers(1, &m_xfbo);
glBindVertexArray(m_vao);
}
void TearDown() override {
if (!Ready()) return;
glBindVertexArray(0);
glDeleteVertexArrays(1, &m_vao);
glDeleteBuffers(1, &m_xfbo);
glDeleteProgram(m_program);
}
GLuint m_program = 0;
GLuint m_vao = 0;
GLuint m_xfbo = 0;
std::string m_log;
};
// glVertexAttribFormat + glBindVertexBuffer + glVertexAttribBinding, in the order
// the CTS uses (buffer first, then format, then binding), must feed the draw.
TEST_F(VertexAttribBindingScenario, FormatAndBindingFeedTheDraw) {
if (!Ready()) GTEST_SKIP();
ResetCurrentAttribs();
const float vertices[] = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f};
GLuint vbo = 0;
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexBuffer(0, vbo, 0, 12);
glVertexAttribFormat(1, 3, GL_FLOAT, GL_FALSE, 0);
glVertexAttribBinding(1, 0);
glEnableVertexAttribArray(1);
const std::vector<float> data = CapturePoints(m_program, m_xfbo, 2, 1);
EXPECT_TRUE(Vec4Is(data, 0, 1, 1.0f, 2.0f, 3.0f, 1.0f));
EXPECT_TRUE(Vec4Is(data, 1, 1, 4.0f, 5.0f, 6.0f, 1.0f));
// An attribute nothing configured still reports its current value.
EXPECT_TRUE(Vec4Is(data, 0, 0, 0.0f, 0.0f, 0.0f, 0.0f));
glDisableVertexAttribArray(1);
glDeleteBuffers(1, &vbo);
}
// The reverse order - format and binding declared before any buffer exists on the
// binding point - has to resolve to the same thing once glBindVertexBuffer lands.
TEST_F(VertexAttribBindingScenario, FormatBeforeBufferStillResolves) {
if (!Ready()) GTEST_SKIP();
ResetCurrentAttribs();
const float vertices[] = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f};
GLuint vbo = 0;
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glVertexAttribBinding(2, 3);
glVertexAttribFormat(2, 2, GL_FLOAT, GL_FALSE, 4);
glEnableVertexAttribArray(2);
glBindVertexBuffer(3, vbo, 0, 12);
const std::vector<float> data = CapturePoints(m_program, m_xfbo, 2, 1);
EXPECT_TRUE(Vec4Is(data, 0, 2, 2.0f, 3.0f, 0.0f, 1.0f));
EXPECT_TRUE(Vec4Is(data, 1, 2, 5.0f, 6.0f, 0.0f, 1.0f));
glDisableVertexAttribArray(2);
glDeleteBuffers(1, &vbo);
}
// GL 4.6 core 10.3.1: a binding point's stride is the byte distance between
// consecutive elements, and zero means every vertex reads the SAME element. That
// is the opposite of glVertexAttribPointer's stride 0, which means "tightly
// packed" - the two spellings must not be collapsed into one another.
TEST_F(VertexAttribBindingScenario, BindingStrideZeroRepeatsOneElement) {
if (!Ready()) GTEST_SKIP();
ResetCurrentAttribs();
const float vertices[] = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f};
GLuint vbo = 0;
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glVertexAttribFormat(0, 4, GL_FLOAT, GL_FALSE, 0);
glVertexAttribBinding(0, 5);
glBindVertexBuffer(5, vbo, 16, 0);
glEnableVertexAttribArray(0);
const std::vector<float> data = CapturePoints(m_program, m_xfbo, 2, 1);
EXPECT_TRUE(Vec4Is(data, 0, 0, 5.0f, 6.0f, 7.0f, 8.0f));
EXPECT_TRUE(Vec4Is(data, 1, 0, 5.0f, 6.0f, 7.0f, 8.0f));
glDisableVertexAttribArray(0);
glDeleteBuffers(1, &vbo);
}
// The pointer API keeps its own meaning of stride 0 (tightly packed) even though
// it is defined in terms of the binding model - the negative control for the
// scenario above.
TEST_F(VertexAttribBindingScenario, PointerStrideZeroStaysTightlyPacked) {
if (!Ready()) GTEST_SKIP();
ResetCurrentAttribs();
const float vertices[] = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f};
GLuint vbo = 0;
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, nullptr);
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
const std::vector<float> data = CapturePoints(m_program, m_xfbo, 2, 1);
EXPECT_TRUE(Vec4Is(data, 0, 0, 1.0f, 2.0f, 3.0f, 4.0f));
EXPECT_TRUE(Vec4Is(data, 1, 0, 5.0f, 6.0f, 7.0f, 8.0f));
glDisableVertexAttribArray(0);
glDeleteBuffers(1, &vbo);
}
// glVertexBindingDivisor is per BINDING POINT: it has to reach every attribute
// pointed at that binding, and the instance step must honour the divisor rather
// than advancing once per instance.
TEST_F(VertexAttribBindingScenario, BindingDivisorAppliesToEveryAttributeOnThePoint) {
if (!Ready()) GTEST_SKIP();
ResetCurrentAttribs();
const float vertices[] = {10.0f, 20.0f, 30.0f, 40.0f};
GLuint vbo = 0;
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glVertexAttribFormat(0, 1, GL_FLOAT, GL_FALSE, 0);
glVertexAttribFormat(1, 1, GL_FLOAT, GL_FALSE, 4);
glVertexAttribBinding(0, 4);
glVertexAttribBinding(1, 4);
glBindVertexBuffer(4, vbo, 0, 8);
glVertexBindingDivisor(4, 2);
glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
// The divisor is per binding point, so it has to be visible on BOTH attributes
// pointed at it - and this query is what separates "the frontend never resolved
// it" from "the backend did not apply it".
GLint divisor = -1;
glGetVertexAttribiv(0, GL_VERTEX_ATTRIB_ARRAY_DIVISOR, &divisor);
EXPECT_EQ(divisor, 2);
divisor = -1;
glGetVertexAttribiv(1, GL_VERTEX_ATTRIB_ARRAY_DIVISOR, &divisor);
EXPECT_EQ(divisor, 2);
// 1 vertex x 4 instances, divisor 2: instances 0,1 read element 0 and
// instances 2,3 read element 1.
const std::vector<float> data = CapturePoints(m_program, m_xfbo, 1, 4);
EXPECT_TRUE(Vec4Is(data, 0, 0, 10.0f, 0.0f, 0.0f, 1.0f));
EXPECT_TRUE(Vec4Is(data, 1, 0, 10.0f, 0.0f, 0.0f, 1.0f));
EXPECT_TRUE(Vec4Is(data, 2, 0, 30.0f, 0.0f, 0.0f, 1.0f));
EXPECT_TRUE(Vec4Is(data, 3, 0, 30.0f, 0.0f, 0.0f, 1.0f));
EXPECT_TRUE(Vec4Is(data, 0, 1, 20.0f, 0.0f, 0.0f, 1.0f));
EXPECT_TRUE(Vec4Is(data, 2, 1, 40.0f, 0.0f, 0.0f, 1.0f));
glDisableVertexAttribArray(0);
glDisableVertexAttribArray(1);
glDeleteBuffers(1, &vbo);
}
// baseInstance moves the ELEMENT the instanced arrays start at. DirectGLES has no
// ES entry point that says so on the drivers we ship against (GL_EXT_base_instance
// is absent on Adreno), so it folds the shift into the attribute's own offset - and
// the thing that made this worth pinning is that the value used to reach the shader
// uniform for gl_BaseInstance and NEVER the fetch, so a draw could report a base
// instance it had not actually read from.
//
// The three draws are the point. Zero first as a negative control, so a backend that
// simply ignored baseInstance could not pass on the middle draw alone; and zero AGAIN
// last, because the shift is emitted into per-attribute state the VAO twin memoises -
// leaving it applied would make every subsequent ordinary draw fetch from the wrong
// element, which is a far worse bug than the one being fixed.
TEST_F(VertexAttribBindingScenario, BaseInstanceMovesTheInstancedArraysStartElement) {
if (!Ready()) GTEST_SKIP();
ResetCurrentAttribs();
const float instanceData[] = {10.0f, 20.0f, 30.0f, 40.0f};
GLuint vbo = 0;
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(instanceData), instanceData, GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glVertexAttribFormat(0, 1, GL_FLOAT, GL_FALSE, 0);
glVertexAttribBinding(0, 0);
glBindVertexBuffer(0, vbo, 0, 4);
glVertexBindingDivisor(0, 1);
glEnableVertexAttribArray(0);
const std::vector<float> atZero = CaptureOwnBufferBaseInstance(m_program, 1, 2, 0, true);
EXPECT_TRUE(Vec4Is(atZero, 0, 0, 10.0f, 0.0f, 0.0f, 1.0f));
EXPECT_TRUE(Vec4Is(atZero, 1, 0, 20.0f, 0.0f, 0.0f, 1.0f));
const std::vector<float> atTwo = CaptureOwnBufferBaseInstance(m_program, 1, 2, 2, true);
EXPECT_TRUE(Vec4Is(atTwo, 0, 0, 30.0f, 0.0f, 0.0f, 1.0f));
EXPECT_TRUE(Vec4Is(atTwo, 1, 0, 40.0f, 0.0f, 0.0f, 1.0f));
// Nothing about the vertex array changed between these two draws, so only a
// backend that actively un-shifts on a baseInstance change gets back to 10/20.
const std::vector<float> backToZero = CaptureOwnBufferBaseInstance(m_program, 1, 2, 0, true);
EXPECT_TRUE(Vec4Is(backToZero, 0, 0, 10.0f, 0.0f, 0.0f, 1.0f));
EXPECT_TRUE(Vec4Is(backToZero, 1, 0, 20.0f, 0.0f, 0.0f, 1.0f));
// And a draw command with no baseInstance parameter at all must be unaffected by
// the one that came before it.
const std::vector<float> plain = CaptureOwnBufferBaseInstance(m_program, 1, 2, 0, false);
EXPECT_TRUE(Vec4Is(plain, 0, 0, 10.0f, 0.0f, 0.0f, 1.0f));
EXPECT_TRUE(Vec4Is(plain, 1, 0, 20.0f, 0.0f, 0.0f, 1.0f));
glDisableVertexAttribArray(0);
glDeleteBuffers(1, &vbo);
}
// baseInstance is defined against the instanced arrays only: an array with divisor 0
// advances per VERTEX and its start element is "first", which baseInstance does not
// touch. An emulation that shifted by offset without checking the divisor would move
// this one too, and nothing in the case above would notice.
TEST_F(VertexAttribBindingScenario, BaseInstanceLeavesPerVertexArraysWhereTheyWere) {
if (!Ready()) GTEST_SKIP();
ResetCurrentAttribs();
const float perVertex[] = {1.0f, 2.0f, 3.0f, 4.0f};
const float perInstance[] = {10.0f, 20.0f, 30.0f, 40.0f};
GLuint buffers[2] = {0, 0};
glGenBuffers(2, buffers);
glBindBuffer(GL_ARRAY_BUFFER, buffers[0]);
glBufferData(GL_ARRAY_BUFFER, sizeof(perVertex), perVertex, GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, buffers[1]);
glBufferData(GL_ARRAY_BUFFER, sizeof(perInstance), perInstance, GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glVertexAttribFormat(0, 1, GL_FLOAT, GL_FALSE, 0);
glVertexAttribBinding(0, 0);
glBindVertexBuffer(0, buffers[0], 0, 4);
glVertexBindingDivisor(0, 0);
glEnableVertexAttribArray(0);
glVertexAttribFormat(1, 1, GL_FLOAT, GL_FALSE, 0);
glVertexAttribBinding(1, 1);
glBindVertexBuffer(1, buffers[1], 0, 4);
glVertexBindingDivisor(1, 1);
glEnableVertexAttribArray(1);
// 2 vertices x 2 instances, baseInstance 2. Points come out instance-major.
const std::vector<float> data = CaptureOwnBufferBaseInstance(m_program, 2, 2, 2, true);
EXPECT_TRUE(Vec4Is(data, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f));
EXPECT_TRUE(Vec4Is(data, 1, 0, 2.0f, 0.0f, 0.0f, 1.0f));
EXPECT_TRUE(Vec4Is(data, 2, 0, 1.0f, 0.0f, 0.0f, 1.0f));
EXPECT_TRUE(Vec4Is(data, 3, 0, 2.0f, 0.0f, 0.0f, 1.0f));
EXPECT_TRUE(Vec4Is(data, 0, 1, 30.0f, 0.0f, 0.0f, 1.0f));
EXPECT_TRUE(Vec4Is(data, 1, 1, 30.0f, 0.0f, 0.0f, 1.0f));
EXPECT_TRUE(Vec4Is(data, 2, 1, 40.0f, 0.0f, 0.0f, 1.0f));
EXPECT_TRUE(Vec4Is(data, 3, 1, 40.0f, 0.0f, 0.0f, 1.0f));
glDisableVertexAttribArray(0);
glDisableVertexAttribArray(1);
glDeleteBuffers(2, buffers);
}
// Two attributes on one binding point at different relative offsets, plus a
// binding offset: the fetch address is binding offset + relative offset, and the
// relative offset must not leak into the binding's own offset.
TEST_F(VertexAttribBindingScenario, RelativeOffsetComposesWithBindingOffset) {
if (!Ready()) GTEST_SKIP();
ResetCurrentAttribs();
const float vertices[] = {0.0f, 0.0f, 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f};
GLuint vbo = 0;
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glVertexAttribFormat(0, 2, GL_FLOAT, GL_FALSE, 0);
glVertexAttribFormat(1, 1, GL_FLOAT, GL_FALSE, 8);
glVertexAttribBinding(0, 1);
glVertexAttribBinding(1, 1);
glBindVertexBuffer(1, vbo, 8, 12);
glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
const std::vector<float> data = CapturePoints(m_program, m_xfbo, 2, 1);
EXPECT_TRUE(Vec4Is(data, 0, 0, 1.0f, 2.0f, 0.0f, 1.0f));
EXPECT_TRUE(Vec4Is(data, 0, 1, 3.0f, 0.0f, 0.0f, 1.0f));
EXPECT_TRUE(Vec4Is(data, 1, 0, 4.0f, 5.0f, 0.0f, 1.0f));
EXPECT_TRUE(Vec4Is(data, 1, 1, 6.0f, 0.0f, 0.0f, 1.0f));
glDisableVertexAttribArray(0);
glDisableVertexAttribArray(1);
glDeleteBuffers(1, &vbo);
}
// The KHR-GL43.vertex_attrib_binding.basic-input* capture program verbatim: a
// 16-element vec4 input ARRAY at location 0, copied element by element into a
// 16-element array inside an output interface block, all 16 members captured.
// Every one of the 17 basic-input* cases is built on it, so a backend that cannot
// produce this program fails all of them with "the draw captured zeros" and no
// other symptom.
TEST_F(VertexAttribBindingScenario, InputArrayCaptureProgramFeedsTheDraw) {
if (!Ready()) GTEST_SKIP();
const std::string vs = R"(#version 430 core
layout(location = 0) in vec4 vs_in_attrib[16];
out StageData {
vec4 attrib[16];
} vs_out;
void main() {
for (int i = 0; i < vs_in_attrib.length(); ++i) {
vs_out.attrib[i] = vs_in_attrib[i];
}
}
)";
std::vector<std::string> names;
for (int i = 0; i < 16; ++i) names.push_back("StageData.attrib[" + std::to_string(i) + "]");
std::vector<const char*> varyings;
for (const auto& n : names) varyings.push_back(n.c_str());
std::string log;
const GLuint program = BuildCaptureProgram(vs, varyings, &log);
ASSERT_NE(program, 0u) << "capture program did not link: " << log;
for (GLuint i = 0; i < 16; ++i) glVertexAttrib4f(i, 0.0f, 0.0f, 0.0f, 0.0f);
const float vertices[] = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f};
GLuint vbo = 0;
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexBuffer(0, vbo, 0, 12);
glVertexAttribFormat(1, 3, GL_FLOAT, GL_FALSE, 0);
glVertexAttribBinding(1, 0);
glEnableVertexAttribArray(1);
// 16 vec4s per point rather than the 4 the shared helper assumes.
constexpr std::size_t kFloatsPerPoint = 64;
std::vector<float> poison(kFloatsPerPoint * 2, kPoison);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, m_xfbo);
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast<GLsizeiptr>(poison.size() * sizeof(float)),
poison.data(), GL_DYNAMIC_DRAW);
glEnable(GL_RASTERIZER_DISCARD);
glUseProgram(program);
glBeginTransformFeedback(GL_POINTS);
glDrawArrays(GL_POINTS, 0, 2);
glEndTransformFeedback();
glDisable(GL_RASTERIZER_DISCARD);
std::vector<float> data(poison.size(), kPoison);
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0,
static_cast<GLsizeiptr>(data.size() * sizeof(float)), data.data());
glUseProgram(0);
// Element 0 of the array has no enabled array behind it, so it must deliver the
// current generic attribute value set above - including its w, which is 0 here and
// NOT the 1 an unwritten vec4 input defaults to.
EXPECT_FLOAT_EQ(data[0], 0.0f);
EXPECT_FLOAT_EQ(data[3], 0.0f);
// attribute 1 of point 0 and of point 1.
EXPECT_FLOAT_EQ(data[4], 1.0f);
EXPECT_FLOAT_EQ(data[5], 2.0f);
EXPECT_FLOAT_EQ(data[6], 3.0f);
EXPECT_FLOAT_EQ(data[7], 1.0f);
EXPECT_FLOAT_EQ(data[kFloatsPerPoint + 4], 4.0f);
EXPECT_FLOAT_EQ(data[kFloatsPerPoint + 5], 5.0f);
EXPECT_FLOAT_EQ(data[kFloatsPerPoint + 6], 6.0f);
EXPECT_FLOAT_EQ(data[kFloatsPerPoint + 7], 1.0f);
glDisableVertexAttribArray(1);
glDeleteBuffers(1, &vbo);
glDeleteProgram(program);
}
// Same program, but every one of the 16 elements is asked for a DIFFERENT current value.
//
// An input array occupies one location per element (GL 4.6 core 11.1.1), so `in vec4 a[16]`
// at location 0 is active on 0..15 - and the whole location span is what a backend reads to
// decide which attributes need their current value pushed. Reflection used to record the
// span of the ELEMENT type only, so a 16-element array claimed exactly one location: every
// element above the first silently read the (0,0,0,1) an unwritten input defaults to instead
// of the value glVertexAttrib4f had set. The test above could not see it, because the only
// element it reads a current value from is element 0 - the one location the array did claim.
TEST_F(VertexAttribBindingScenario, EveryInputArrayElementGetsItsOwnCurrentValue) {
if (!Ready()) GTEST_SKIP();
const std::string vs = R"(#version 430 core
layout(location = 0) in vec4 vs_in_attrib[16];
out StageData {
vec4 attrib[16];
} vs_out;
void main() {
for (int i = 0; i < vs_in_attrib.length(); ++i) {
vs_out.attrib[i] = vs_in_attrib[i];
}
}
)";
std::vector<std::string> names;
for (int i = 0; i < 16; ++i) names.push_back("StageData.attrib[" + std::to_string(i) + "]");
std::vector<const char*> varyings;
for (const auto& n : names) varyings.push_back(n.c_str());
std::string log;
const GLuint program = BuildCaptureProgram(vs, varyings, &log);
ASSERT_NE(program, 0u) << "capture program did not link: " << log;
// Distinct in every component, and never (0,0,0,1): the value an element that was
// skipped would report has to be distinguishable from every value that was asked for.
for (GLuint i = 0; i < 16; ++i) {
const float base = static_cast<float>(i) + 1.0f;
glVertexAttrib4f(i, base, base + 100.0f, base + 200.0f, base + 300.0f);
}
constexpr std::size_t kFloatsPerPoint = 64;
std::vector<float> poison(kFloatsPerPoint, kPoison);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, m_xfbo);
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast<GLsizeiptr>(poison.size() * sizeof(float)),
poison.data(), GL_DYNAMIC_DRAW);
glEnable(GL_RASTERIZER_DISCARD);
glUseProgram(program);
glBeginTransformFeedback(GL_POINTS);
glDrawArrays(GL_POINTS, 0, 1);
glEndTransformFeedback();
glDisable(GL_RASTERIZER_DISCARD);
std::vector<float> data(poison.size(), kPoison);
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0,
static_cast<GLsizeiptr>(data.size() * sizeof(float)), data.data());
glUseProgram(0);
for (int element = 0; element < 16; ++element) {
const float base = static_cast<float>(element) + 1.0f;
EXPECT_FLOAT_EQ(data[element * 4 + 0], base) << "element " << element;
EXPECT_FLOAT_EQ(data[element * 4 + 1], base + 100.0f) << "element " << element;
EXPECT_FLOAT_EQ(data[element * 4 + 2], base + 200.0f) << "element " << element;
EXPECT_FLOAT_EQ(data[element * 4 + 3], base + 300.0f) << "element " << element;
}
for (GLuint i = 0; i < 16; ++i) glVertexAttrib4f(i, 0.0f, 0.0f, 0.0f, 0.0f);
glDeleteProgram(program);
}
} // namespace MGITest
@@ -0,0 +1,317 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/XfbCaptureBufferReuseScenario.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// ONE capture buffer, SEVERAL capture spans - the shape most KHR-GL4x cases that
// use transform feedback as a readback channel are built on. They allocate the
// capture buffer once in a setup step and then run span after span through it,
// so a defect that only shows from the second span onwards fails the whole case
// while the first span (and every single-span scenario in this suite) stays
// green. The first thing checked here is therefore not the capture itself but
// that the bytes the capture wrote are the bytes the readback reads.
//
// Two ways of reusing the buffer, because they exercise different machinery:
//
// * respecified between spans (glBufferData while the buffer is still bound to
// the transform-feedback binding point), which is what a test helper that
// poisons its capture buffer before every span does;
// * allocated ONCE with immutable storage and never touched again, which is
// what KHR-GL45.direct_state_access.vertex_arrays_enable_disable_attributes
// does - glBufferStorage(4 bytes) in its setup, then two draws.
//
// The negative control (a fresh buffer object per span) is a separate case
// rather than a parameter: it is the configuration that already worked, so it
// has to keep working for the others to mean anything.
#include <cmath>
#include <cstdio>
#include <cstring>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr float kPoison = -1234.0f;
// One vec4 per point, one point per draw.
constexpr std::size_t kCaptureFloats = 4;
constexpr std::size_t kCaptureBytes = kCaptureFloats * sizeof(float);
GLuint CompileShader(GLenum type, const std::string& source, std::string* log) {
const GLuint shader = glCreateShader(type);
const char* text = source.c_str();
glShaderSource(shader, 1, &text, nullptr);
glCompileShader(shader);
GLint status = GL_FALSE;
glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
if (status == GL_FALSE) {
GLint length = 0;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
glGetShaderInfoLog(shader, length + 1, nullptr, buffer.data());
if (log != nullptr) *log = buffer.data();
glDeleteShader(shader);
return 0;
}
return shader;
}
// Vertex-only capture program: whatever the draw fetched at location 0 comes
// straight back out through the capture. Runs under GL_RASTERIZER_DISCARD, so
// there is no fragment stage.
GLuint BuildCaptureProgram(std::string* log) {
const std::string vertexSource = R"(#version 430 core
layout(location = 0) in vec4 vs_in_value;
out vec4 vs_out_value;
void main() {
vs_out_value = vs_in_value;
}
)";
const GLuint vertexShader = CompileShader(GL_VERTEX_SHADER, vertexSource, log);
if (vertexShader == 0) return 0;
const GLuint program = glCreateProgram();
glAttachShader(program, vertexShader);
const char* varying = "vs_out_value";
glTransformFeedbackVaryings(program, 1, &varying, GL_INTERLEAVED_ATTRIBS);
glLinkProgram(program);
glDeleteShader(vertexShader);
GLint status = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &status);
if (status == GL_FALSE) {
GLint length = 0;
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
glGetProgramInfoLog(program, length + 1, nullptr, buffer.data());
if (log != nullptr) *log = buffer.data();
glDeleteProgram(program);
return 0;
}
return program;
}
class XfbCaptureBufferReuseScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
std::string log;
m_program = BuildCaptureProgram(&log);
ASSERT_NE(m_program, 0u) << "capture program failed to build: " << log;
glGenVertexArrays(1, &m_vao);
glBindVertexArray(m_vao);
glGenBuffers(1, &m_vbo);
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
glBufferData(GL_ARRAY_BUFFER, kCaptureBytes, nullptr, GL_DYNAMIC_DRAW);
glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, nullptr);
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
}
void TearDown() override {
if (!Ready()) return;
glBindVertexArray(0);
if (m_vbo != 0) glDeleteBuffers(1, &m_vbo);
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
if (m_program != 0) glDeleteProgram(m_program);
glUseProgram(0);
ScenarioTest::TearDown();
}
// The vertex the next span will fetch and capture.
void SetVertex(float value) {
const float data[kCaptureFloats] = {value, value + 1.0f, value + 2.0f, value + 3.0f};
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
glBufferSubData(GL_ARRAY_BUFFER, 0, kCaptureBytes, data);
glBindBuffer(GL_ARRAY_BUFFER, 0);
}
// One capture span over the buffer currently bound to capture point 0.
void RunSpan() {
glEnable(GL_RASTERIZER_DISCARD);
glUseProgram(m_program);
glBindVertexArray(m_vao);
glBeginTransformFeedback(GL_POINTS);
glDrawArrays(GL_POINTS, 0, 1);
glEndTransformFeedback();
glDisable(GL_RASTERIZER_DISCARD);
glUseProgram(0);
}
static ::testing::AssertionResult CapturedIs(const float* data, float value) {
for (std::size_t i = 0; i < kCaptureFloats; ++i) {
const float expected = value + static_cast<float>(i);
const float got = data[i];
// isfinite first: every ordered comparison against a NaN is false, so a
// pair of one-sided range tests REPORTS SUCCESS for uninitialised
// storage that happens to read as NaN - which is exactly the failure
// these scenarios exist to catch.
if (!std::isfinite(got) || std::fabs(got - expected) > 0.01f) {
return ::testing::AssertionFailure()
<< "component " << i << " is " << got << ", expected " << expected
<< (got == kPoison ? " (the capture never reached these bytes)" : "");
}
}
return ::testing::AssertionSuccess();
}
GLuint m_program = 0;
GLuint m_vao = 0;
GLuint m_vbo = 0;
};
// The negative control: one buffer object per span. This is the configuration
// every multi-span scenario in this suite works around the others with, so it
// has to hold or nothing below is interpretable.
TEST_F(XfbCaptureBufferReuseScenario, EverySpanIntoABufferObjectOfItsOwn) {
if (!Ready()) GTEST_SKIP();
for (int span = 0; span < 3; ++span) {
const float value = 10.0f * static_cast<float>(span + 1);
const std::vector<float> poison(kCaptureFloats, kPoison);
GLuint xfbBuffer = 0;
glGenBuffers(1, &xfbBuffer);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, kCaptureBytes, poison.data(), GL_DYNAMIC_DRAW);
SetVertex(value);
RunSpan();
float readback[kCaptureFloats] = {kPoison, kPoison, kPoison, kPoison};
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, kCaptureBytes, readback);
EXPECT_TRUE(CapturedIs(readback, value)) << "span " << span;
glDeleteBuffers(1, &xfbBuffer);
}
EXPECT_EQ(glGetError(), GL_NO_ERROR);
}
// The same three spans through ONE buffer object, respecified before each of
// them WHILE it is bound to capture point 0 - a helper poisoning its capture
// buffer, which is what makes "captured nothing" legible in the first place.
//
// A respecification is free to replace the storage underneath (that is what
// orphaning is), and on a buffer whose bytes the backend has already handed
// the frontend a pointer into, the replacement has to reach that pointer too.
// It did not: the capture wrote the new storage and the readback kept reading
// the old one, so every span after the first came back poison.
TEST_F(XfbCaptureBufferReuseScenario, EverySpanIntoOneRespecifiedBufferObject) {
if (!Ready()) GTEST_SKIP();
GLuint xfbBuffer = 0;
glGenBuffers(1, &xfbBuffer);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
for (int span = 0; span < 3; ++span) {
const float value = 10.0f * static_cast<float>(span + 1);
const std::vector<float> poison(kCaptureFloats, kPoison);
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, kCaptureBytes, poison.data(), GL_DYNAMIC_DRAW);
SetVertex(value);
RunSpan();
float readback[kCaptureFloats] = {kPoison, kPoison, kPoison, kPoison};
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, kCaptureBytes, readback);
EXPECT_TRUE(CapturedIs(readback, value)) << "span " << span;
}
glDeleteBuffers(1, &xfbBuffer);
EXPECT_EQ(glGetError(), GL_NO_ERROR);
}
// A respecification that CHANGES the size, which is the case a re-pointing
// that only handled same-size storage would still get wrong - and, before the
// fix, the case that wrote the new (larger) contents through a mapping sized
// for the old ones.
TEST_F(XfbCaptureBufferReuseScenario, ARespecificationMayChangeTheCaptureBufferSize) {
if (!Ready()) GTEST_SKIP();
GLuint xfbBuffer = 0;
glGenBuffers(1, &xfbBuffer);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
// Sized for one point, then for four, then back down to one.
const std::size_t pointCapacity[] = {1, 4, 1};
for (int span = 0; span < 3; ++span) {
const float value = 10.0f * static_cast<float>(span + 1);
const std::size_t floats = kCaptureFloats * pointCapacity[span];
const std::vector<float> poison(floats, kPoison);
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast<GLsizeiptr>(floats * sizeof(float)),
poison.data(), GL_DYNAMIC_DRAW);
SetVertex(value);
RunSpan();
std::vector<float> readback(floats, kPoison);
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0,
static_cast<GLsizeiptr>(floats * sizeof(float)), readback.data());
EXPECT_TRUE(CapturedIs(readback.data(), value)) << "span " << span;
// The bytes past the one point the draw produced must still be the
// poison the respecification put there, not whatever the previous
// (differently sized) storage held.
for (std::size_t i = kCaptureFloats; i < floats; ++i) {
EXPECT_FLOAT_EQ(readback[i], kPoison) << "span " << span << " float " << i;
}
}
glDeleteBuffers(1, &xfbBuffer);
EXPECT_EQ(glGetError(), GL_NO_ERROR);
}
// The KHR-GL45.direct_state_access.vertex_arrays_enable_disable_attributes
// shape: the capture buffer gets IMMUTABLE storage once, in a setup step, and
// is never respecified - two spans simply run through it, each read back with
// glMapBuffer. Nothing here may depend on a respecification to reset the
// capture: glBeginTransformFeedback does that on its own.
TEST_F(XfbCaptureBufferReuseScenario, EverySpanIntoOneImmutableStorageBuffer) {
if (!Ready()) GTEST_SKIP();
GLuint xfbBuffer = 0;
glGenBuffers(1, &xfbBuffer);
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, xfbBuffer);
// Poisoned at creation - the storage is immutable, so this is the only chance to
// put a recognisable value there, and without it a span that captured nothing
// would be indistinguishable from one that captured the right thing whenever the
// untouched bytes happened to read back as the expected number.
const std::vector<float> poison(kCaptureFloats, kPoison);
glBufferStorage(GL_TRANSFORM_FEEDBACK_BUFFER, kCaptureBytes, poison.data(), GL_MAP_READ_BIT);
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "glBufferStorage on the capture buffer";
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
for (int span = 0; span < 3; ++span) {
const float value = 10.0f * static_cast<float>(span + 1);
SetVertex(value);
RunSpan();
const void* mapped = glMapBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, GL_READ_ONLY);
ASSERT_NE(mapped, nullptr) << "span " << span << ": glMapBuffer returned null";
float readback[kCaptureFloats] = {kPoison, kPoison, kPoison, kPoison};
std::memcpy(readback, mapped, kCaptureBytes);
glUnmapBuffer(GL_TRANSFORM_FEEDBACK_BUFFER);
EXPECT_TRUE(CapturedIs(readback, value)) << "span " << span;
}
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, 0);
glDeleteBuffers(1, &xfbBuffer);
EXPECT_EQ(glGetError(), GL_NO_ERROR);
}
} // namespace
} // namespace MGITest
@@ -75,10 +75,42 @@ namespace MobileGL::MG_State::GLState {
NotifySubData(offset, size);
}
void BufferObject::Respecify(SizeT size, const void* data) {
ReleaseMemory();
// A (re)definition of the store is about to write `size` bytes through Bytes().
// Sizing the shadow is all that takes for a shadow-backed buffer. A buffer whose
// bytes were adopted into backend GPU memory has to give the adoption back first,
// because the mapping it holds describes exactly the OLD store: writing the new
// contents through it runs past its end the moment the store grows, and a backend
// that replaces the storage for the new store - which is what an orphaning
// respecification asks for - would leave that mapping, and therefore every later
// read of this buffer, addressing storage nothing writes to any more. That was the
// transform feedback capture that wrote one buffer while the readback read another.
//
// Given back rather than renewed here, deliberately. Renewing in place would mean
// memcpying the new contents into storage that submitted-but-unretired draws may
// still be reading, which is precisely what the orphaning idiom exists to avoid;
// avoiding THAT would mean either stalling on a fence in the middle of a frame or
// teaching the persistent-map op to orphan, and the op must never orphan for the
// other kind of caller (an application-held GL_MAP_PERSISTENT_BIT mapping, whose
// pointer has to stay valid for the buffer's whole life). Handing the store back to
// the CPU shadow needs none of that: the backend's ordinary respecification path
// then does the busy-tracking and the conditional orphan it has always done, and the
// next binding that wants GPU residency takes a fresh mapping of the new store.
void BufferObject::RedefineStorage(SizeT size) {
if (m_resource.IsGpuResident()) {
m_resource.ReleasePersistentMap();
// Whatever a shader or a capture wrote is in the store being replaced, so
// there is nothing left to reconcile - and leaving the flag set would make
// the next read of this buffer wait for GPU work on behalf of bytes the
// application has just thrown away.
m_gpuWritePending = false;
}
m_size = size;
m_resource.ResizeShadow(size);
}
void BufferObject::Respecify(SizeT size, const void* data) {
ReleaseMemory();
RedefineStorage(size);
if (data && size > 0) {
Memcpy(m_resource.Bytes(), data, size);
}
@@ -96,8 +128,7 @@ namespace MobileGL::MG_State::GLState {
void BufferObject::AllocateImmutableStorage(SizeT size, const void* data, GLbitfield storageFlags) {
ReleaseMemory();
m_size = size;
m_resource.ResizeShadow(size);
RedefineStorage(size);
if (data) {
Memcpy(m_resource.Bytes(), data, size);
} else if (size > 0) {
@@ -205,6 +205,9 @@ namespace MobileGL {
void SetBackendResource(SharedPtr<BackendBufferResource> resource);
private:
// Sizes the store for a (re)definition, renewing an adopted GPU-resident
// mapping across it. See the definition for why the renewal is not optional.
void RedefineStorage(SizeT size);
void NotifyRespecify();
void NotifySubData(SizeT offset, SizeT size);
void NotifyFlushMappedRange(Range1D range, Flags<BufferMappingAccessBit> appAccess);
@@ -70,6 +70,15 @@ namespace MobileGL::MG_State::GLState {
m_shadow->shrink_to_fit();
}
// Give the adoption back: the bytes resolve against the shadow again (which
// the caller must (re)size, it was released on adoption). Used when the store
// itself is redefined - the mapping describes exactly the store that is going
// away, so it may neither be written through nor kept. It is NOT a general
// "unmap": a persistent map the application holds outlives every unmap by
// definition, and the calls that could redefine such a buffer's store are
// errors the frontend refuses before reaching here.
void ReleasePersistentMap() { m_gpuMapped = nullptr; }
// Backend GPU resource, owned here in both modes.
const SharedPtr<BackendBufferResource>& Backend() const { return m_backend; }
void SetBackend(SharedPtr<BackendBufferResource> backend) { m_backend = std::move(backend); }
+4 -4
View File
@@ -196,7 +196,7 @@ namespace MobileGL::MG_State {
// (which expands to nothing outside debug builds).
void GLContext::SetCurrentVertexAttributeFloat(Uint index, const Array<Float, 4>& value) {
if (index >= m_currentVertexAttributes.size()) {
MGLOG_E("SetCurrentVertexAttributeFloat: index %u is out of range", index);
MGLOG_E_ONCE("SetCurrentVertexAttributeFloat: index %u is out of range", index);
return;
}
@@ -210,7 +210,7 @@ namespace MobileGL::MG_State {
void GLContext::SetCurrentVertexAttributeInt(Uint index, const Array<Int32, 4>& value) {
if (index >= m_currentVertexAttributes.size()) {
MGLOG_E("SetCurrentVertexAttributeInt: index %u is out of range", index);
MGLOG_E_ONCE("SetCurrentVertexAttributeInt: index %u is out of range", index);
return;
}
@@ -224,7 +224,7 @@ namespace MobileGL::MG_State {
void GLContext::SetCurrentVertexAttributeUint(Uint index, const Array<Uint32, 4>& value) {
if (index >= m_currentVertexAttributes.size()) {
MGLOG_E("SetCurrentVertexAttributeUint: index %u is out of range", index);
MGLOG_E_ONCE("SetCurrentVertexAttributeUint: index %u is out of range", index);
return;
}
@@ -239,7 +239,7 @@ namespace MobileGL::MG_State {
const CurrentVertexAttributeValue& GLContext::GetCurrentVertexAttribute(Uint index) const {
static const CurrentVertexAttributeValue defaultValue{};
if (index >= m_currentVertexAttributes.size()) {
MGLOG_E("GetCurrentVertexAttribute: index %u is out of range", index);
MGLOG_E_ONCE("GetCurrentVertexAttribute: index %u is out of range", index);
return defaultValue;
}
return m_currentVertexAttributes[index];
@@ -14,10 +14,10 @@
namespace MobileGL::MG_State::GLState {
void ErrorState::RecordError(ErrorCode code, UniquePtr<ErrorInfo> info) {
if (code == ErrorCode::NoError) {
MGLOG_E("Recording Non-OpenGL error:\n%s", info->toString().c_str());
MGLOG_D("Recording Non-OpenGL error:\n%s", info->toString().c_str());
m_nonGLErrors.push_back(MakeUnique<Error>(code, Move(info)));
} else {
MGLOG_E("Recording OpenGL error (%s):\n%s",
MGLOG_D("Recording OpenGL error (%s):\n%s",
MG_Util::ConvertGLEnumToString(MG_Util::ConvertErrorCodeToGLEnum(code)).c_str(),
info->toString().c_str());
// GL error semantics are sticky flags, not a queue (GL 3.3 core §2.5): with multiple
@@ -68,6 +68,8 @@ namespace {
return type && type->getQualifier().builtIn != glslang::EbvNone;
}
// Locations one ELEMENT of a vertex input occupies (GL 4.6 core 11.1.1): a matrix
// takes one per column, everything else this backend can feed takes one.
static int GetVertexInputLocationSpan(GLenum glType) {
switch (glType) {
case GL_FLOAT_MAT2:
@@ -87,6 +89,26 @@ namespace {
}
}
// How many elements an ARRAY vertex input has. glslang reflects such an input as ONE
// record spelled "name[0]" carrying the ELEMENT's glDefineType and the array length,
// so the type alone cannot say how many locations the declaration covers: GL 4.6 core
// 11.1.1 gives an array one location per element (times the element's own span), and
// `in vec4 a[16]` at location 0 therefore occupies 0..15, not 0. Missing that left
// every location above the base with no recorded name or type, which is what the
// backends read to decide whether an attribute is active at all.
static MobileGL::Int GetVertexInputArrayElements(const glslang::TObjectReflection& input) {
const glslang::TType* type = input.getType();
if (type == nullptr || !type->isArray()) return 1;
// An unsized input array has no span to compute; treat it as one element rather
// than guessing, so it can only ever under-claim locations.
if (!type->isSizedArray()) return 1;
return std::max(1, type->getCumulativeArraySize());
}
static MobileGL::Int GetVertexInputTotalLocationSpan(const glslang::TObjectReflection& input) {
return GetVertexInputLocationSpan(input.glDefineType) * GetVertexInputArrayElements(input);
}
static GLenum GetVertexInputLocationType(GLenum glType) {
switch (glType) {
case GL_FLOAT_MAT2:
@@ -563,8 +585,20 @@ namespace MobileGL::MG_State::GLState {
// - SharedStd140UBO: a DECLARED uniform block is active even when no member is
// ever read (reflected from the linker objects). PreprocessShaderSource coerces
// every block to std140, so this covers all of them.
// - IntermediateIO: GL_PROGRAM_INPUT is the input interface of the program's FIRST
// stage and GL_PROGRAM_OUTPUT the output interface of its LAST one. Without this
// glslang hardcodes those boundaries to vertex/fragment, so a separable program
// made of one non-vertex stage has an empty input interface and one made of a
// non-fragment stage an empty output interface
// (KHR-GL43.program_interface_query.separate-programs-*).
// - UnwrapIOBlocks: an inter-stage interface block enumerates as its MEMBERS -
// "Color.r", and "gl_Position" for an anonymous gl_PerVertex - not as the block
// instance. Only reachable through IntermediateIO: a vertex stage's inputs and a
// fragment stage's outputs can never be blocks, so this is inert for a program
// whose boundary stages are the hardcoded ones.
if (!artifacts.program->buildReflection(EShReflectionStrictArraySuffix | EShReflectionBasicArraySuffix |
EShReflectionAllBlockVariables | EShReflectionSharedStd140UBO)) {
EShReflectionAllBlockVariables | EShReflectionSharedStd140UBO |
EShReflectionIntermediateIO | EShReflectionUnwrapIOBlocks)) {
artifacts.linkStatus = false;
artifacts.infoLog = "Build reflection failed.";
DeferLog(std::format("ProgramObject {}: DoReflection - buildReflection() returned false",
@@ -852,14 +886,21 @@ namespace MobileGL::MG_State::GLState {
}
// ------------ attributes (vertex in) ---------------
Int inCount = artifacts.program->getNumPipeInputs();
// The pipe-input list is the input interface of the program's FIRST stage, which is only
// the vertex attribute set when the program actually HAS a vertex stage. A separable
// fragment/geometry/tessellation program reflects its own stage inputs here, and those are
// varyings - registering them as vertex attributes would hand glGetActiveAttrib and the
// attribute location table interstage varyings.
Int inCount = artifacts.program->getIntermediate(EShLangVertex) != nullptr
? artifacts.program->getNumPipeInputs()
: 0;
MGLOG_D("ProgramObject %u: Reflection - pipe input count (attributes) = %d", in.externalIndex, inCount);
Int maxLoc = -1;
for (int i = 0; i < inCount; ++i) {
Int loc = (Int)artifacts.program->getPipeInput(i).layoutLocation();
if (loc >= 0 && loc != glslang::TQualifier::layoutLocationEnd) {
const Int locationSpan = GetVertexInputLocationSpan(artifacts.program->getPipeInput(i).glDefineType);
const Int locationSpan = GetVertexInputTotalLocationSpan(artifacts.program->getPipeInput(i));
maxLoc = std::max(maxLoc, loc + locationSpan - 1);
}
MGLOG_D("ProgramObject %u: Reflection - pipe input[%d] name='%s' layoutLocation=%d glType=%u",
@@ -895,7 +936,7 @@ namespace MobileGL::MG_State::GLState {
(Int)ProgramObject::NormalizeBuiltinPipeInputName(inVar.name).length());
if (location >= 0 && location < (int)artifacts.attribs.size()) {
const Int locationSpan = GetVertexInputLocationSpan(inVar.glDefineType);
const Int locationSpan = GetVertexInputTotalLocationSpan(inVar);
const GLenum locationType = GetVertexInputLocationType(inVar.glDefineType);
for (Int locationOffset = 0; locationOffset < locationSpan; ++locationOffset) {
const Int expandedLocation = location + locationOffset;
@@ -951,6 +992,11 @@ namespace MobileGL::MG_State::GLState {
Bool ProgramLinkTask::ValidateFragmentOutputLocations() {
if (!artifacts.program) return false;
// The pipe-output list is the output interface of the program's LAST stage. Only a
// fragment stage's outputs are color numbers indexed against GL_MAX_DRAW_BUFFERS; a
// separable vertex/geometry/tessellation program's outputs are varyings, and holding
// them to the draw-buffer range fails the link of every such program.
if (artifacts.program->getIntermediate(EShLangFragment) == nullptr) return true;
UnorderedMap<Int, String> colorNumberOwners;
const Int outputCount = artifacts.program->getNumPipeOutputs();
@@ -165,10 +165,18 @@ namespace MobileGL::MG_State::GLState {
const Int elements = init.arraySize;
if (componentsPerElement <= 0 || elements <= 0) continue;
const Bool isFloat = init.basicType == glslang::EbtFloat || init.basicType == glslang::EbtFloat16;
// EbtDouble belongs with the floats now, not with the skipped types: every 64-bit
// float in a shader is narrowed to 32 bits before the module reaches a backend
// (ShaderTranspiler::DemoteFloat64Pass), so a `uniform double d = 1.5;` has exactly
// the 32-bit shadow encoding a `uniform float` does - and glslang already folded its
// value into floatValues, which is a vector<double> either way. Leaving it out meant
// the initializer was silently dropped and the uniform came up zero.
const Bool isFloat = init.basicType == glslang::EbtFloat ||
init.basicType == glslang::EbtFloat16 ||
init.basicType == glslang::EbtDouble;
const Bool isInt = init.basicType == glslang::EbtInt || init.basicType == glslang::EbtUint ||
init.basicType == glslang::EbtBool;
// Anything else (fp64, 64-bit integers) has no 32-bit shadow encoding here, and a
// Anything else (64-bit integers) has no 32-bit shadow encoding here, and a
// half-written uniform is worse than an untouched one.
if (!isFloat && !isInt) continue;
const SizeT provided = isFloat ? init.floatValues.size() : init.intValues.size();
@@ -247,7 +255,7 @@ namespace MobileGL::MG_State::GLState {
static_cast<SizeT>(offset) + write.byteOffsetInUniform + write.byteSize > uboSize) {
// Same verdict the live write path reaches for a uniform without backing
// storage: log and drop, rather than fault.
MGLOG_E("ProgramObject %u: buffered uniform write at location %u has no backing storage "
MGLOG_E_ONCE("ProgramObject %u: buffered uniform write at location %u has no backing storage "
"(offset=%u size=%u uboSize=%zu); dropping write",
m_externalIndex, write.location, offset, write.byteSize, uboSize);
continue;
@@ -428,7 +436,7 @@ namespace MobileGL::MG_State::GLState {
defaultFS->Compile(); // TODO: use a global default FS object.
auto status = defaultFS->GetCompileStatus();
if (!status) {
MGLOG_E("ProgramObject %u: Failed to compile default fragment shader. InfoLog:\n%s", m_externalIndex,
MGLOG_E_ONCE("ProgramObject %u: Failed to compile default fragment shader. InfoLog:\n%s", m_externalIndex,
defaultFS->GetInfoLog().c_str());
return;
}
@@ -326,15 +326,22 @@ namespace MobileGL::MG_State::GLState {
: kInvalidUniformOffset;
}
Uint GetUniformSizesInBytes(Uint location) const { return MG_Util::GetGLTypeSize(GetUniformType(location)); }
// Bytes a uniform actually occupies in the global UBO, which is not its GL type size:
// std140 pads each column of a float matrix out to a vec4, so a mat3 spans 48 bytes
// even though only 36 of them carry components. Anything reading or writing a whole
// uniform's storage - a bounds check, a copy between two programs' shadows - wants
// this rather than GetUniformSizesInBytes.
// Bytes a uniform actually occupies in the global UBO, which is not its GL type size,
// for two reasons. std140 pads each column of a matrix out to a vec4, so a mat3 spans
// 48 bytes even though only 36 of them carry components. And every 64-bit float in a
// shader is narrowed to 32 bits before the module reaches a backend
// (ShaderTranspiler::DemoteFloat64Pass) - the global UBO is laid out by reflecting that
// demoted module - so a `double` uniform occupies exactly what its float-typed twin
// would, half its GL type size, and a `dmat4` is padded like any other matrix. Anything
// reading or writing a whole uniform's storage - a bounds check, a copy between two
// programs' shadows - wants this rather than GetUniformSizesInBytes.
static SizeT UniformStorageSpanInBytes(const glslang::TType* type, SizeT tightSize) {
if (type != nullptr && type->isMatrix() && type->getBasicType() != glslang::EbtDouble) {
if (type != nullptr && type->isMatrix()) {
return static_cast<SizeT>(type->getMatrixCols()) * 4 * sizeof(Float);
}
if (type != nullptr && type->getBasicType() == glslang::EbtDouble) {
return tightSize / 2;
}
return tightSize;
}
SizeT GetUniformStorageSpanInBytes(Uint location) const {
@@ -187,6 +187,14 @@ namespace MobileGL {
}
// -------------------- Capabilities --------------------
namespace {
// CapabilityInput lists ClipDistance0..7 contiguously (RenderState.h); the caller
// has already rejected anything outside that run, so the subtraction is in range.
Uint32 ClipDistanceBit(CapabilityInput cap) {
return 1u << (static_cast<Uint>(cap) - static_cast<Uint>(CapabilityInput::ClipDistance0));
}
} // namespace
void RenderState::SetCapability(CapabilityInput cap, Bool enabled) {
#define SET_CAPABILITY(capability, flag) \
case CapabilityInput::capability: \
@@ -228,6 +236,27 @@ namespace MobileGL {
if (stateChanged) BumpVersions();
break;
}
case CapabilityInput::ClipDistance0:
case CapabilityInput::ClipDistance1:
case CapabilityInput::ClipDistance2:
case CapabilityInput::ClipDistance3:
case CapabilityInput::ClipDistance4:
case CapabilityInput::ClipDistance5:
case CapabilityInput::ClipDistance6:
case CapabilityInput::ClipDistance7: {
const Uint32 bit = ClipDistanceBit(cap);
const Uint32 updated =
enabled ? (m_parameters.ClipDistanceEnabledMask | bit)
: (m_parameters.ClipDistanceEnabledMask & ~bit);
if (updated == m_parameters.ClipDistanceEnabledMask) break;
m_parameters.ClipDistanceEnabledMask = updated;
// Deliberately NOT BumpVersions(): no backend bakes a clip-distance enable
// into a pipeline object (DirectGLES issues glEnable, DirectVulkan takes the
// set from the shader's declared array), so bumping the pipeline version here
// would evict cached pipelines for state they do not contain.
++m_version;
break;
}
default: // not supported currently
break;
}
@@ -263,6 +292,15 @@ namespace MobileGL {
RETURN_CAPABILITY(ProgramPointSize);
case CapabilityInput::Blend:
return m_parameters.BlendStates[0].Enabled;
case CapabilityInput::ClipDistance0:
case CapabilityInput::ClipDistance1:
case CapabilityInput::ClipDistance2:
case CapabilityInput::ClipDistance3:
case CapabilityInput::ClipDistance4:
case CapabilityInput::ClipDistance5:
case CapabilityInput::ClipDistance6:
case CapabilityInput::ClipDistance7:
return (m_parameters.ClipDistanceEnabledMask & ClipDistanceBit(cap)) != 0;
default:
return false;
}
@@ -303,6 +303,12 @@ namespace MobileGL {
Bool StencilTestEnabled = false;
Bool ProgramPointSizeEnabled = false;
IntVec4 ScissorBox = IntVec4(0, 0, 0, 0); // x, y, width, height
// glEnable(GL_CLIP_DISTANCE0 + i) for i in [0, 8), one bit each. A bitmask rather than
// eight bools because every consumer wants the set, not an individual flag, and because
// the SYNC_CAPABILITY/SET_CAPABILITY macros key off a "<Name>Enabled" field name that
// eight numbered capabilities cannot share. Lives in the tail span (after LogicOp), so
// DirectGLES' span memcmp picks a change up like any other capability.
Uint32 ClipDistanceEnabledMask = 0;
};
namespace MG_State {
@@ -117,7 +117,15 @@ namespace MobileGL::MG_State::GLState {
void SetFixedSampleLocations(Bool fixedSampleLocations) override;
Uint64 GetLifetimeId() const override;
GLenum GetDepthStencilTextureMode() const override { return m_depthStencilTextureMode; }
void SetDepthStencilTextureMode(GLenum mode) override { m_depthStencilTextureMode = mode; }
// Bumps the params version like every other backend-visible texture parameter: the mode
// decides which ASPECT of a packed depth/stencil image a sampler reads, which DirectGLES
// forwards as a texture parameter and DirectVulkan bakes into the sampled image view. A
// silent write here would leave both backends showing the aspect they last built.
void SetDepthStencilTextureMode(GLenum mode) override {
if (m_depthStencilTextureMode == mode) return;
m_depthStencilTextureMode = mode;
++m_textureParamsVersion;
}
protected:
static Uint64 AllocateLifetimeId();
@@ -61,12 +61,16 @@ namespace MobileGL::MG_State::GLState {
}
void VertexArrayObject::SetAttributeFormat(Uint index, int size, DataType type, Bool normalized, int stride,
SizeT offset, Bool isInteger, Bool isBgra) {
SizeT offset, Bool isInteger, Bool isBgra, int effectiveStride) {
if (index >= MAX_VERTEX_ATTRIBS) return;
if (size < 1 || size > 4) {
return;
}
// See VertexAttribute::Stride: the resolved field carries the effective stride so that
// a zero in it can only ever mean the binding model's "do not advance".
const int resolvedStride = effectiveStride >= 0 ? effectiveStride : stride;
// The classic pointer-style API takes back full ownership of the resolved fields.
m_attributeUsesBindingModel[index] = false;
@@ -77,7 +81,7 @@ namespace MobileGL::MG_State::GLState {
m_attributes[index].LegacyPointer = offset;
if (m_attributes[index].Size == size && m_attributes[index].Type == type &&
m_attributes[index].Normalized == normalized && m_attributes[index].Stride == stride &&
m_attributes[index].Normalized == normalized && m_attributes[index].Stride == resolvedStride &&
m_attributes[index].Offset == offset && m_attributes[index].IsInteger == isInteger &&
m_attributes[index].IsBgra == isBgra && !m_attributes[index].IsLong) {
return;
@@ -87,7 +91,7 @@ namespace MobileGL::MG_State::GLState {
attr.Size = size;
attr.Type = type;
attr.Normalized = normalized;
attr.Stride = stride;
attr.Stride = resolvedStride;
attr.Offset = offset;
attr.IsInteger = isInteger;
attr.IsBgra = isBgra;
@@ -19,6 +19,14 @@ namespace MobileGL {
int Size = 4;
DataType Type = DataType::Float32;
Bool Normalized = false;
// The RESOLVED byte distance between consecutive elements, never the raw
// glVertexAttrib*Pointer argument: a pointer call's stride 0 means "tightly
// packed" and is resolved to the element size here, so a zero that survives
// into this field can only have come from the binding model, where a zero
// VERTEX_BINDING_STRIDE means the opposite - every vertex reads the SAME
// element and the fetch address never advances (GL 4.6 core 10.3.1). Backends
// consume this verbatim; collapsing 0 back into the element size is what made
// KHR-GL43.vertex_attrib_binding.basic-input-case7/8 read past the buffer.
int Stride = 0;
SizeT Offset = 0;
Bool IsInteger = false;
@@ -76,8 +84,12 @@ namespace MobileGL {
void DisableAttribute(Uint index);
Bool IsAttributeEnabled(Uint index) const;
// `stride` is the raw glVertexAttrib*Pointer argument, reported verbatim by
// GL_VERTEX_ATTRIB_ARRAY_STRIDE. `effectiveStride` is what the fetch actually
// advances by - the same value when the argument is non-zero, the tightly
// packed element size when it is zero. Pass -1 to say the two are the same.
void SetAttributeFormat(Uint index, int size, DataType type, Bool normalized, int stride, SizeT offset,
Bool isInteger, Bool isBgra = false);
Bool isInteger, Bool isBgra = false, int effectiveStride = -1);
void BindAttributeBuffer(Uint index, const SharedPtr<BufferObject>& buffer);
@@ -16,8 +16,11 @@
#include <MG_Backend/DirectGLES/Utils.h>
using namespace MobileGL;
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::BakeImageFormatQualifiers;
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::ForceFlatIntegerVaryings;
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::IMAGE_WRITE_ALIAS_PREFIX;
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RemoveLayoutBinding;
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RequestExtendedImageFormats;
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::SplitReadWriteImageUniforms;
namespace {
@@ -367,3 +370,183 @@ void main() {}
<< "an unrelated extension must survive untouched:\n" << out;
EXPECT_EQ(CountOf(out, "GL_OES_texture_buffer"), 1u);
}
// Interpolation is only ever consumed at a fragment input, but an ES linker still compares the
// two sides of EVERY stage interface and rejects a program whose producer says `flat` and whose
// consumer does not. SPIRV-Cross prints `flat` on a vertex output and a geometry input of
// integer type and on nothing else, so a program with tessellation in the middle came out
// mismatched at both ends of the tessellator - "output vs_tcs_result interpolation mismatch
// with other stage" on Adreno, and a program that fails to link is a draw that paints nothing.
TEST(ForceFlatIntegerVaryingsTest, TessellationStagesGetTheQualifierOnBothSides) {
const String tessControl = R"(#version 320 es
layout(vertices = 1) out;
layout(location = 0) in uint vs_tcs_result[];
layout(location = 0) out uint tcs_tes_result[1];
void main() { tcs_tes_result[gl_InvocationID] = vs_tcs_result[gl_InvocationID]; }
)";
const String control = ForceFlatIntegerVaryings(tessControl, GL_TESS_CONTROL_SHADER);
EXPECT_TRUE(Contains(control, "layout(location = 0) flat in uint vs_tcs_result[];")) << control;
EXPECT_TRUE(Contains(control, "layout(location = 0) flat out uint tcs_tes_result[1];")) << control;
const String tessEval = R"(#version 320 es
layout(isolines, point_mode) in;
layout(location = 0) in uint tcs_tes_result[];
layout(location = 0) out uint tes_gs_result;
void main() { tes_gs_result = tcs_tes_result[0]; }
)";
const String eval = ForceFlatIntegerVaryings(tessEval, GL_TESS_EVALUATION_SHADER);
EXPECT_TRUE(Contains(eval, "layout(location = 0) flat in uint tcs_tes_result[];")) << eval;
EXPECT_TRUE(Contains(eval, "layout(location = 0) flat out uint tes_gs_result;")) << eval;
}
// The two ends the tessellation stages have to meet: what a vertex shader and a geometry shader
// already emitted before this pass learned about tessellation at all. Pinned here so the two
// sides cannot drift apart again.
TEST(ForceFlatIntegerVaryingsTest, TheStagesAroundTessellationAreUnchanged) {
const String vertex = R"(#version 320 es
layout(location = 0) out uint vs_tcs_result;
void main() { vs_tcs_result = 1u; }
)";
EXPECT_TRUE(Contains(ForceFlatIntegerVaryings(vertex, GL_VERTEX_SHADER),
"layout(location = 0) flat out uint vs_tcs_result;"));
const String geometry = R"(#version 320 es
layout(points) in;
layout(triangle_strip, max_vertices = 4) out;
layout(location = 0) in uint tes_gs_result[1];
layout(location = 0) out uint gs_fs_result;
void main() { gs_fs_result = tes_gs_result[0]; EmitVertex(); }
)";
const String gs = ForceFlatIntegerVaryings(geometry, GL_GEOMETRY_SHADER);
EXPECT_TRUE(Contains(gs, "layout(location = 0) flat in uint tes_gs_result[1];")) << gs;
EXPECT_TRUE(Contains(gs, "layout(location = 0) flat out uint gs_fs_result;")) << gs;
}
// Non-integer interfaces keep whatever interpolation they were given: adding `flat` to a float
// varying would turn a smoothly interpolated value into a per-provoking-vertex constant, which
// is a rendering change, not a linker one.
TEST(ForceFlatIntegerVaryingsTest, FloatVaryingsAreNotTouched) {
const String tessEval = R"(#version 320 es
layout(isolines, point_mode) in;
layout(location = 1) in vec2 tcs_tes_coord[];
layout(location = 1) out vec2 tes_gs_coord;
void main() { tes_gs_coord = tcs_tes_coord[0]; }
)";
const String out = ForceFlatIntegerVaryings(tessEval, GL_TESS_EVALUATION_SHADER);
EXPECT_TRUE(Contains(out, "layout(location = 1) in vec2 tcs_tes_coord[];")) << out;
EXPECT_TRUE(Contains(out, "layout(location = 1) out vec2 tes_gs_coord;")) << out;
EXPECT_EQ(CountOf(out, "flat"), 0u) << out;
}
// --- image format qualifier completion ---------------------------------------------------------
//
// GLSL ES requires a format layout qualifier on every image; desktop GLSL lets a writeonly
// declaration omit one. The format is normally written into the SPIR-V before SPIRV-Cross runs
// (BakeImageFormatsPass), but SPIRV-Cross THROWS rather than printing the formats it calls
// desktop-only for ESSL - r8ui among them - so those are completed here, on the emitted text.
// The KHR-GL4x.packed_depth_stencil.stencil_texturing stencil half: `writeonly uniform uimage2D`
// with GL_R8UI bound to its unit.
TEST(BakeImageFormatQualifiersTest, AFormatlessDeclarationGetsTheBoundFormat) {
const String source = R"(#version 320 es
layout(binding = 1) uniform writeonly highp uimage2D uni_image;
void main() { imageStore(uni_image, ivec2(0), uvec4(15u)); }
)";
const String out = BakeImageFormatQualifiers(source, {{"uni_image", "r8ui"}});
EXPECT_TRUE(Contains(out, "layout(r8ui, binding = 1) uniform writeonly highp uimage2D uni_image;")) << out;
}
// A declaration with NO layout at all still has to end up with one, or the driver rejects it for
// exactly the reason this pass exists.
TEST(BakeImageFormatQualifiersTest, ADeclarationWithNoLayoutGetsOne) {
const String source = R"(#version 320 es
uniform writeonly highp uimage2D uni_image;
void main() { imageStore(uni_image, ivec2(0), uvec4(1u)); }
)";
const String out = BakeImageFormatQualifiers(source, {{"uni_image", "r16i"}});
EXPECT_TRUE(Contains(out, "layout(r16i) uniform writeonly highp uimage2D uni_image;")) << out;
}
// A DECLARED format is authoritative and must survive, whatever the map says - the frontend never
// puts a declared image in the map, and the pass must not depend on that being true.
TEST(BakeImageFormatQualifiersTest, ADeclaredFormatIsNeverOverwritten) {
const String source = R"(#version 320 es
layout(binding = 1, rgba8ui) uniform writeonly highp uimage2D uni_image;
void main() { imageStore(uni_image, ivec2(0), uvec4(1u)); }
)";
const String out = BakeImageFormatQualifiers(source, {{"uni_image", "r8ui"}});
EXPECT_EQ(out, source) << out;
}
// Only the named uniform. A second image in the same shader - format-less because the pass
// declined it, or because its unit holds nothing - must be left exactly as it is.
TEST(BakeImageFormatQualifiersTest, OnlyTheNamedUniformIsTouched) {
const String source = R"(#version 320 es
layout(binding = 0) uniform writeonly highp uimage2D named;
layout(binding = 1) uniform writeonly highp uimage2D other;
void main() { imageStore(named, ivec2(0), uvec4(1u)); imageStore(other, ivec2(0), uvec4(2u)); }
)";
const String out = BakeImageFormatQualifiers(source, {{"named", "r8ui"}});
EXPECT_TRUE(Contains(out, "layout(r8ui, binding = 0) uniform writeonly highp uimage2D named;")) << out;
EXPECT_TRUE(Contains(out, "layout(binding = 1) uniform writeonly highp uimage2D other;")) << out;
}
// The format the pass writes has to survive the two passes that run after it, or nothing was
// gained: the read+write split copies declarations, and the binding strip edits layout qualifiers.
TEST(BakeImageFormatQualifiersTest, TheWrittenFormatSurvivesTheLaterImagePasses) {
const String source = R"(#version 320 es
layout(binding = 3) uniform writeonly highp uimage2D uni_image;
void main() { imageStore(uni_image, ivec2(0), uvec4(1u)); }
)";
String out = BakeImageFormatQualifiers(source, {{"uni_image", "r8ui"}});
out = SplitReadWriteImageUniforms(out);
out = RemoveLayoutBinding(out);
EXPECT_TRUE(Contains(out, "r8ui")) << out;
EXPECT_TRUE(Contains(out, "binding = 3")) << out;
}
TEST(BakeImageFormatQualifiersTest, AnEmptyMapOrAnImagelessShaderIsANoOp) {
const String withImage = R"(#version 320 es
layout(binding = 1) uniform writeonly highp uimage2D uni_image;
void main() { imageStore(uni_image, ivec2(0), uvec4(1u)); }
)";
EXPECT_EQ(BakeImageFormatQualifiers(withImage, {}), withImage);
const String withoutImage = R"(#version 320 es
layout(location = 0) out highp vec4 mg_FragColor;
void main() { mg_FragColor = vec4(1.0); }
)";
EXPECT_EQ(BakeImageFormatQualifiers(withoutImage, {{"uni_image", "r8ui"}}), withoutImage);
}
// --- GL_NV_image_formats directive --------------------------------------------------------------
TEST(RequestExtendedImageFormatsTest, TheDirectiveGoesRightAfterTheVersionLine) {
const String source = R"(#version 320 es
layout(r8ui, binding = 1) uniform writeonly highp uimage2D uni_image;
void main() { imageStore(uni_image, ivec2(0), uvec4(1u)); }
)";
const String out = RequestExtendedImageFormats(source, true);
EXPECT_TRUE(Contains(out, "#version 320 es\n#extension GL_NV_image_formats : require\n")) << out;
}
// Never speculatively: `#extension` naming an extension the driver does not advertise is itself a
// compile error, so the caller's "not needed" answer has to be honoured exactly.
TEST(RequestExtendedImageFormatsTest, NotNeededMeansNotEmitted) {
const String source = R"(#version 320 es
layout(rgba8ui, binding = 1) uniform writeonly highp uimage2D uni_image;
void main() { imageStore(uni_image, ivec2(0), uvec4(1u)); }
)";
EXPECT_EQ(RequestExtendedImageFormats(source, false), source);
}
TEST(RequestExtendedImageFormatsTest, AnAlreadyPresentDirectiveIsNotDuplicated) {
const String source = R"(#version 320 es
#extension GL_NV_image_formats : require
layout(r8ui, binding = 1) uniform writeonly highp uimage2D uni_image;
void main() { imageStore(uni_image, ivec2(0), uvec4(1u)); }
)";
const String out = RequestExtendedImageFormats(source, true);
EXPECT_EQ(out, source);
EXPECT_EQ(CountOf(out, "GL_NV_image_formats"), 1u) << out;
}
+185
View File
@@ -1610,3 +1610,188 @@ TEST_F(GeneralBufferTest, General_CoherentAsFlush_PersistentMapAdoptsZeroCopyBac
EXPECT_EQ(GetError(), GL_NO_ERROR);
g_zeroCopyMock = nullptr;
}
// A buffer whose bytes the backend adopted into its own GPU memory - which is what
// EnsureGpuResidentStorage does for a transform-feedback capture target or a shader
// storage binding, so that MapBuffer/GetBufferSubData read real GPU results - and which
// the application then REDEFINES.
//
// The store the adopted mapping describes is the one being thrown away. Keeping that
// mapping across the redefinition is what let a transform feedback capture be written to
// one buffer and read back out of another: the backend replaced the storage (a
// respecification is the orphaning point) while the frontend went on resolving every read
// through a mapping of the storage it had just released. Two capture spans into one
// re-specified buffer came back empty from the second one onwards.
//
// So the mapping is handed back and the buffer returns to the CPU-shadow model until
// something asks for residency again. These pin all three parts of that: the adoption
// really is dropped, the new contents really do land where later reads resolve, and the
// backend really is told to respecify - it must not skip the storage, or its copy would
// keep the old bytes.
TEST_F(BufferTest, RedefiningAnAdoptedBufferHandsTheMappingBack) {
ZeroCopyMockBackend mock;
g_zeroCopyMock = &mock;
ScopedBackendOps scopedOps(&kZeroCopyMockOps);
GLuint buffer = 0;
GenBuffers(1, &buffer);
BindBuffer(GL_ARRAY_BUFFER, buffer);
const GLint before[4] = {1, 2, 3, 4};
BufferData(GL_ARRAY_BUFFER, sizeof(before), before, GL_DYNAMIC_DRAW);
ASSERT_EQ(GetError(), GL_NO_ERROR);
auto bufferObject = MG_State::pGLContext->GetBufferObject(buffer);
ASSERT_NE(bufferObject, nullptr);
// The backend adopts the bytes, exactly as a capture target or an SSBO binding does.
ASSERT_TRUE(bufferObject->EnsureGpuResidentStorage());
ASSERT_TRUE(bufferObject->IsBackendPersistentMapped());
ASSERT_EQ(static_cast<const void*>(bufferObject->MappedData()),
static_cast<const void*>(mock.gpu.data()));
mock.respecifyCalls = 0;
const GLint after[4] = {10, 20, 30, 40};
BufferData(GL_ARRAY_BUFFER, sizeof(after), after, GL_DYNAMIC_DRAW);
ASSERT_EQ(GetError(), GL_NO_ERROR);
EXPECT_FALSE(bufferObject->IsBackendPersistentMapped());
EXPECT_NE(static_cast<const void*>(bufferObject->MappedData()),
static_cast<const void*>(mock.gpu.data()));
EXPECT_EQ(std::memcmp(bufferObject->MappedData(), after, sizeof(after)), 0);
// The backend has a separate copy again, so it must have been told to refresh it.
EXPECT_EQ(mock.respecifyCalls, 1);
g_zeroCopyMock = nullptr;
}
// The same redefinition at a LARGER size, which is the case nothing could paper over: the
// adopted mapping is exactly as big as the old store, so writing the new contents through
// it ran past the end of the backend allocation.
TEST_F(BufferTest, RedefiningAnAdoptedBufferAtANewSizeStaysInBounds) {
ZeroCopyMockBackend mock;
g_zeroCopyMock = &mock;
ScopedBackendOps scopedOps(&kZeroCopyMockOps);
GLuint buffer = 0;
GenBuffers(1, &buffer);
BindBuffer(GL_ARRAY_BUFFER, buffer);
const GLint small[2] = {1, 2};
BufferData(GL_ARRAY_BUFFER, sizeof(small), small, GL_DYNAMIC_DRAW);
auto bufferObject = MG_State::pGLContext->GetBufferObject(buffer);
ASSERT_NE(bufferObject, nullptr);
ASSERT_TRUE(bufferObject->EnsureGpuResidentStorage());
ASSERT_EQ(mock.gpu.size(), sizeof(small));
const GLint large[8] = {1, 2, 3, 4, 5, 6, 7, 8};
BufferData(GL_ARRAY_BUFFER, sizeof(large), large, GL_DYNAMIC_DRAW);
ASSERT_EQ(GetError(), GL_NO_ERROR);
EXPECT_EQ(bufferObject->GetSize(), sizeof(large));
EXPECT_FALSE(bufferObject->IsBackendPersistentMapped());
EXPECT_EQ(std::memcmp(bufferObject->MappedData(), large, sizeof(large)), 0);
// The old, smaller GPU block was not written through: still the old size, still the
// old bytes.
EXPECT_EQ(mock.gpu.size(), sizeof(small));
EXPECT_EQ(std::memcmp(mock.gpu.data(), small, sizeof(small)), 0);
// And residency can be taken again, now over the new store.
ASSERT_TRUE(bufferObject->EnsureGpuResidentStorage());
EXPECT_TRUE(bufferObject->IsBackendPersistentMapped());
EXPECT_EQ(mock.gpu.size(), sizeof(large));
EXPECT_EQ(std::memcmp(bufferObject->MappedData(), large, sizeof(large)), 0);
g_zeroCopyMock = nullptr;
}
// glBufferStorage is the other way into a redefinition, and an adopted buffer can reach
// it: the adoption came from a binding rather than from an application map, so the buffer
// is still mutable and glBufferStorage is still legal on it.
TEST_F(BufferTest, ImmutableStorageOnAnAdoptedBufferHandsTheMappingBackToo) {
ZeroCopyMockBackend mock;
g_zeroCopyMock = &mock;
ScopedBackendOps scopedOps(&kZeroCopyMockOps);
GLuint buffer = 0;
GenBuffers(1, &buffer);
BindBuffer(GL_ARRAY_BUFFER, buffer);
const GLint before[4] = {1, 2, 3, 4};
BufferData(GL_ARRAY_BUFFER, sizeof(before), before, GL_DYNAMIC_DRAW);
auto bufferObject = MG_State::pGLContext->GetBufferObject(buffer);
ASSERT_NE(bufferObject, nullptr);
ASSERT_TRUE(bufferObject->EnsureGpuResidentStorage());
ASSERT_TRUE(bufferObject->IsBackendPersistentMapped());
const GLint after[6] = {9, 8, 7, 6, 5, 4};
BufferStorage(GL_ARRAY_BUFFER, sizeof(after), after, GL_MAP_READ_BIT);
ASSERT_EQ(GetError(), GL_NO_ERROR);
EXPECT_TRUE(bufferObject->IsImmutableStorage());
EXPECT_FALSE(bufferObject->IsBackendPersistentMapped());
EXPECT_EQ(bufferObject->GetSize(), sizeof(after));
EXPECT_EQ(std::memcmp(bufferObject->MappedData(), after, sizeof(after)), 0);
g_zeroCopyMock = nullptr;
}
// A redefinition to nothing. The backend declines residency for an empty store, so this
// is also the path where the mapping is given back and never retaken.
TEST_F(BufferTest, RedefiningAnAdoptedBufferToZeroBytesLeavesItOnTheShadow) {
ZeroCopyMockBackend mock;
g_zeroCopyMock = &mock;
ScopedBackendOps scopedOps(&kZeroCopyMockOps);
GLuint buffer = 0;
GenBuffers(1, &buffer);
BindBuffer(GL_ARRAY_BUFFER, buffer);
const GLint before[4] = {1, 2, 3, 4};
BufferData(GL_ARRAY_BUFFER, sizeof(before), before, GL_DYNAMIC_DRAW);
auto bufferObject = MG_State::pGLContext->GetBufferObject(buffer);
ASSERT_NE(bufferObject, nullptr);
ASSERT_TRUE(bufferObject->EnsureGpuResidentStorage());
ASSERT_TRUE(bufferObject->IsBackendPersistentMapped());
BufferData(GL_ARRAY_BUFFER, 0, nullptr, GL_DYNAMIC_DRAW);
ASSERT_EQ(GetError(), GL_NO_ERROR);
EXPECT_EQ(bufferObject->GetSize(), 0u);
EXPECT_FALSE(bufferObject->IsBackendPersistentMapped());
EXPECT_FALSE(bufferObject->EnsureGpuResidentStorage()); // nothing to make resident
// ...and it comes back to life on the next non-empty store.
const GLint again[3] = {5, 6, 7};
BufferData(GL_ARRAY_BUFFER, sizeof(again), again, GL_DYNAMIC_DRAW);
ASSERT_EQ(GetError(), GL_NO_ERROR);
EXPECT_TRUE(bufferObject->EnsureGpuResidentStorage());
EXPECT_EQ(std::memcmp(bufferObject->MappedData(), again, sizeof(again)), 0);
g_zeroCopyMock = nullptr;
}
// The negative control for the four above: a backend that DECLINES to hand out a mapping
// leaves the buffer shadow-backed throughout, so a redefinition is just a redefinition -
// no adoption to give back, and the backend still gets its Respecify.
TEST_F(BufferTest, RedefiningANonAdoptedBufferIsUnchanged) {
ZeroCopyMockBackend mock;
mock.provideMap = false;
g_zeroCopyMock = &mock;
ScopedBackendOps scopedOps(&kZeroCopyMockOps);
GLuint buffer = 0;
GenBuffers(1, &buffer);
BindBuffer(GL_ARRAY_BUFFER, buffer);
const GLint before[4] = {1, 2, 3, 4};
BufferData(GL_ARRAY_BUFFER, sizeof(before), before, GL_DYNAMIC_DRAW);
auto bufferObject = MG_State::pGLContext->GetBufferObject(buffer);
ASSERT_NE(bufferObject, nullptr);
EXPECT_FALSE(bufferObject->EnsureGpuResidentStorage());
EXPECT_FALSE(bufferObject->IsBackendPersistentMapped());
mock.respecifyCalls = 0;
const GLint after[4] = {10, 20, 30, 40};
BufferData(GL_ARRAY_BUFFER, sizeof(after), after, GL_DYNAMIC_DRAW);
ASSERT_EQ(GetError(), GL_NO_ERROR);
EXPECT_FALSE(bufferObject->IsBackendPersistentMapped());
EXPECT_EQ(std::memcmp(bufferObject->MappedData(), after, sizeof(after)), 0);
EXPECT_EQ(mock.respecifyCalls, 1);
g_zeroCopyMock = nullptr;
}
+1
View File
@@ -3,6 +3,7 @@ cmake_minimum_required(VERSION 3.14)
add_executable(
PipelineQuirkTest
PipelineQuirkTest.cpp
PassthroughTessControlTest.cpp
)
target_include_directories(PipelineQuirkTest PRIVATE
@@ -0,0 +1,247 @@
// MobileGL - MobileGL/MG_Test/Pipeline/PassthroughTessControlTest.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include <gtest/gtest.h>
#include "Includes.h"
#include "Init.h"
#include <map>
#include <set>
#include <MG_Backend/DirectVulkan/Renderer/ProgramFactory.h>
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <MG_Util/ShaderTranspiler/Types.h>
using namespace MobileGL;
using MobileGL::MG_Backend::DirectVulkan::ProgramFactory;
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
namespace {
// A test-side SPIR-V walker, deliberately independent of the production reflection: the
// generator's contract with the evaluation stage is "declare this many output vertices and
// write these built-ins", and that has to be readable off the module itself.
constexpr Uint32 kSpirvHeaderWordCount = 5;
constexpr Uint32 kOpExecutionMode = 16;
constexpr Uint32 kOpDecorate = 71;
constexpr Uint32 kOpMemberDecorate = 72;
constexpr Uint32 kExecutionModeOutputVertices = 26;
constexpr Uint32 kDecorationBuiltIn = 11;
// SpvBuiltIn values used below.
constexpr Uint32 kBuiltInPosition = 0;
constexpr Uint32 kBuiltInInvocationId = 8;
constexpr Uint32 kBuiltInTessLevelOuter = 11;
constexpr Uint32 kBuiltInTessLevelInner = 12;
template <typename Visitor>
void ForEachInstruction(const Vector<Uint32>& spirv, Visitor&& visit) {
for (SizeT i = kSpirvHeaderWordCount; i < spirv.size();) {
const Uint32 wordCount = spirv[i] >> 16;
const Uint32 opcode = spirv[i] & 0xFFFFu;
if (wordCount == 0 || i + wordCount > spirv.size()) break;
visit(opcode, &spirv[i], wordCount);
i += wordCount;
}
}
// -1 when the module declares no OutputVertices mode at all, which is itself a failure the
// tests want to see named rather than silently compared against a wrong number.
Int DeclaredOutputVertices(const Vector<Uint32>& spirv) {
Int declared = -1;
ForEachInstruction(spirv, [&](Uint32 opcode, const Uint32* words, Uint32 wordCount) {
if (opcode == kOpExecutionMode && wordCount >= 4 && words[2] == kExecutionModeOutputVertices) {
declared = static_cast<Int>(words[3]);
}
});
return declared;
}
// The built-in members of every block in the module, keyed by the struct's result id, in
// member order. A gl_PerVertex is exactly such a struct, and its member list IS the shape the
// neighbouring stage has to agree with.
constexpr Uint32 kOpTypeStruct = 30;
std::map<Uint32, Vector<Uint32>> BuiltInBlockShapes(const Vector<Uint32>& spirv) {
std::map<Uint32, Vector<Uint32>> shapes;
ForEachInstruction(spirv, [&](Uint32 opcode, const Uint32* words, Uint32 wordCount) {
if (opcode == kOpMemberDecorate && wordCount >= 5 && words[3] == kDecorationBuiltIn) {
shapes[words[1]].push_back(words[4]);
}
});
return shapes;
}
// Member count of a struct type, so a shape comparison can also catch a block that grew a
// NON-built-in member (which the decoration walk above would not see).
Uint32 StructMemberCount(const Vector<Uint32>& spirv, Uint32 structId) {
Uint32 count = 0;
ForEachInstruction(spirv, [&](Uint32 opcode, const Uint32* words, Uint32 wordCount) {
if (opcode == kOpTypeStruct && wordCount >= 2 && words[1] == structId) {
count = wordCount - 2;
}
});
return count;
}
std::set<Uint32> DeclaredBuiltIns(const Vector<Uint32>& spirv) {
std::set<Uint32> builtIns;
ForEachInstruction(spirv, [&](Uint32 opcode, const Uint32* words, Uint32 wordCount) {
if (opcode == kOpDecorate && wordCount >= 4 && words[2] == kDecorationBuiltIn) {
builtIns.insert(words[3]);
}
if (opcode == kOpMemberDecorate && wordCount >= 5 && words[3] == kDecorationBuiltIn) {
builtIns.insert(words[4]);
}
});
return builtIns;
}
Vector<Uint32> CompileGeneratedSource(Uint32 patchVertices) {
using namespace MG_Util::ShaderTranspiler;
const String source = ProgramFactory::BuildPassthroughTessControlSource(patchVertices);
ShaderAttrib shaderAttrib{.shaderType = GL_TESS_CONTROL_SHADER, .sourceStr = source};
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
EXPECT_TRUE(shaderResult) << (shaderResult ? String{} : shaderResult.error().log) << "\n" << source;
if (!shaderResult) return {};
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
EXPECT_TRUE(programResult) << (programResult ? String{} : programResult.error().log);
if (!programResult) return {};
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_TESS_CONTROL_SHADER},
.program = *programResult.value()};
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
EXPECT_TRUE(binaryResult) << (binaryResult ? String{} : binaryResult.error().log);
if (!binaryResult || binaryResult->empty()) return {};
return binaryResult->front();
}
} // namespace
class PassthroughTessControlTest : public ::testing::Test {
protected:
void SetUp() override { MobileGL::Initialize(); }
};
// The whole reason this stage is generated per patch size rather than once: GL takes the output
// patch size from PATCH_VERTICES, which is draw state. A program that links at the default 3 and
// draws at 4 - which is exactly what
// KHR-GL43.shader_storage_buffer_object.advanced-write-tessellation does - must get a stage built
// for 4, or its evaluation stage reads gl_in[3] out of a three-element array.
TEST_F(PassthroughTessControlTest, DeclaresTheRequestedPatchSize) {
for (const Uint32 patchVertices : {1u, 2u, 3u, 4u, 16u, 32u}) {
const Vector<Uint32> spirv = CompileGeneratedSource(patchVertices);
ASSERT_FALSE(spirv.empty()) << "patchVertices=" << patchVertices;
EXPECT_EQ(DeclaredOutputVertices(spirv), static_cast<Int>(patchVertices))
<< "patchVertices=" << patchVertices;
}
}
// gl_Position in, gl_Position out, and both tessellation level arrays written: the four facts the
// evaluation stage downstream of this depends on. Position appearing at all is what makes the
// pass-through a pass-through; the levels are what GL's PATCH_DEFAULT_*_LEVEL state supplies when
// there is no control shader, and without them the tessellator produces nothing.
TEST_F(PassthroughTessControlTest, ForwardsPositionAndWritesBothLevelArrays) {
const Vector<Uint32> spirv = CompileGeneratedSource(4);
ASSERT_FALSE(spirv.empty());
const std::set<Uint32> builtIns = DeclaredBuiltIns(spirv);
EXPECT_TRUE(builtIns.contains(kBuiltInPosition));
EXPECT_TRUE(builtIns.contains(kBuiltInInvocationId));
EXPECT_TRUE(builtIns.contains(kBuiltInTessLevelOuter));
EXPECT_TRUE(builtIns.contains(kBuiltInTessLevelInner));
}
// The generated source carries nothing but gl_Position across the interface. If that ever grows a
// user-defined varying, ReflectPassthroughTessControlNeed's "built-ins only" refusal stops being
// the right gate and both have to move together.
TEST_F(PassthroughTessControlTest, InterfaceIsBuiltInsOnly) {
const String source = ProgramFactory::BuildPassthroughTessControlSource(4);
EXPECT_EQ(source.find("layout(location"), String::npos) << source;
EXPECT_NE(source.find("layout(vertices = 4) out;"), String::npos) << source;
}
// THE load-bearing test. Vulkan matches built-in interface blocks by their whole shape, and this
// stage is compiled ON ITS OWN - it never goes through the glslang link that gives a real program
// its gl_PerVertex. So the shape it declares has to equal the shape a linked vertex+evaluation
// program carries, and nothing at runtime says otherwise: a mismatch renders a black frame, no
// error, no validation message. That is exactly how the first cut of this shipped-and-failed
// (gl_Position only, three members short), and how the second did (glslang's default block for a
// standalone control stage, which appends gl_CullDistance where a linked program has no such
// member). This links the shader pair the motivating CTS case uses and compares the two shapes
// directly.
TEST_F(PassthroughTessControlTest, MatchesTheFrontendPerVertexBlock) {
using namespace MG_Util::ShaderTranspiler;
// Deliberately the shape of KHR-GL43.shader_storage_buffer_object.advanced-write-tessellation:
// a vertex stage feeding an evaluation stage with no control stage in between.
static const char* kVs = R"(#version 430 core
layout(location = 0) in vec4 g_in_position;
void main() { gl_Position = g_in_position; }
)";
static const char* kTes = R"(#version 430 core
layout(quads) in;
void main() {
vec4 p0 = mix(gl_in[0].gl_Position, gl_in[1].gl_Position, gl_TessCoord.x);
vec4 p1 = mix(gl_in[3].gl_Position, gl_in[2].gl_Position, gl_TessCoord.x);
gl_Position = mix(p0, p1, gl_TessCoord.y);
}
)";
static const char* kFs = R"(#version 430 core
layout(location = 0) out vec4 g_fs_out;
void main() { g_fs_out = vec4(0, 1, 0, 1); }
)";
const Vector<GLenum> types{GL_VERTEX_SHADER, GL_TESS_EVALUATION_SHADER, GL_FRAGMENT_SHADER};
const Vector<const char*> sources{kVs, kTes, kFs};
Vector<SharedPtr<glslang::TShader>> shaders;
for (SizeT i = 0; i < types.size(); ++i) {
ShaderAttrib attrib{.shaderType = types[i], .sourceStr = sources[i]};
auto compiled = ShaderCompiler::CompileShader(attrib);
ASSERT_TRUE(compiled) << compiled.error().log;
shaders.push_back(compiled.value());
}
ProgramAttrib programAttrib{.shaders = shaders};
auto linked = ShaderCompiler::LinkProgram(programAttrib);
ASSERT_TRUE(linked) << linked.error().log;
ProgramBinaryAttrib binaryAttrib{.shaderTypes = types, .program = *linked.value()};
auto binary = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
ASSERT_TRUE(binary);
ASSERT_EQ(binary->size(), types.size());
// The evaluation stage's gl_in is the block the pass-through has to feed. It is the only
// built-in block that stage declares as an input, so the module holds exactly one such shape
// besides its own gl_PerVertex output - and both are the same shape, which is the point.
const auto tesShapes = BuiltInBlockShapes((*binary)[1]);
ASSERT_FALSE(tesShapes.empty());
const Vector<Uint32> frontendShape = tesShapes.begin()->second;
const Uint32 frontendMembers = StructMemberCount((*binary)[1], tesShapes.begin()->first);
for (const auto& [structId, shape] : tesShapes) {
EXPECT_EQ(shape, frontendShape) << "the evaluation stage's own built-in blocks disagree";
EXPECT_EQ(StructMemberCount((*binary)[1], structId), frontendMembers);
}
const Vector<Uint32> passthrough = CompileGeneratedSource(4);
ASSERT_FALSE(passthrough.empty());
const auto passthroughShapes = BuiltInBlockShapes(passthrough);
ASSERT_FALSE(passthroughShapes.empty());
Uint32 perVertexBlocksChecked = 0;
for (const auto& [structId, shape] : passthroughShapes) {
// gl_TessLevelOuter/Inner are decorated on plain variables, not on a block, so every
// struct that reaches here is a gl_PerVertex - gl_in's and gl_out's.
EXPECT_EQ(shape, frontendShape)
<< "the pass-through control stage's gl_PerVertex no longer matches the one the "
"frontend gives a linked vertex+evaluation program";
EXPECT_EQ(StructMemberCount(passthrough, structId), frontendMembers)
<< "the pass-through control stage's gl_PerVertex has a different member count";
++perVertexBlocksChecked;
}
EXPECT_EQ(perVertexBlocksChecked, 2u) << "expected both gl_in and gl_out to be gl_PerVertex blocks";
}
+163 -11
View File
@@ -121,12 +121,6 @@ void main() { color = vec4(0, 1, 0, 1); }
ExpectLinked(p);
ClearErrors();
// KNOWN GAP, not an expectation: a separable FRAGMENT program's own inputs are not
// in the reflection at all - glslang builds the "pipe input" list from the vertex
// stage unless EShReflectionIntermediateIO is set, and setting that makes a
// vertex-only separable program report its VS outputs as fragment outputs, which
// fails ValidateFragmentOutputLocations and breaks glCreateShaderProgramv. So
// GL_PROGRAM_INPUT is empty here until that validation is stage-aware.
EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_OUTPUT, GL_ACTIVE_RESOURCES), 1);
EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_OUTPUT, GL_MAX_NAME_LENGTH), 6);
@@ -804,6 +798,163 @@ void main(void) {
EXPECT_EQ(TakeError(), GL_NO_ERROR);
}
// ------------------------------------------------------------- uniform-block-types --
// GL_REFERENCED_BY_*_SHADER is per block INSTANCE. An array of uniform blocks enumerates
// one resource per element, and only the elements a stage actually dereferences are
// referenced by it - declaring the array is not referencing every element.
TEST_F(ProgramInterfaceTest, ArrayedUniformBlockReferencedByIsPerElement) {
const char* vs = R"(#version 430
in vec4 position;
uniform SimpleBlock { mat3x2 a; mat4 b; vec4 c; };
void main(void) {
float tmp = a[0][1] * b[1][2] * c.x;
gl_Position = position * tmp;
}
)";
const char* fs = R"(#version 430
uniform TrickyBlock { mat4 b; uint c; } e[2];
out vec4 color;
void main() { color = vec4(0, 1, 0, 1) * e[0].b[0][0]; }
)";
const GLuint p = MakeProgram(vs, fs);
BindAttribLocation(p, 0, "position");
BindFragDataLocation(p, 0, "color");
LinkProgram(p);
ExpectLinked(p);
ClearErrors();
EXPECT_EQ(Interfaceiv(p, GL_UNIFORM_BLOCK, GL_ACTIVE_RESOURCES), 3);
ExpectResource(p, GL_UNIFORM_BLOCK, "TrickyBlock[0]", "TrickyBlock[0]");
ExpectResource(p, GL_UNIFORM_BLOCK, "TrickyBlock[1]", "TrickyBlock[1]");
const std::vector<GLenum> refProps = {GL_REFERENCED_BY_VERTEX_SHADER, GL_REFERENCED_BY_FRAGMENT_SHADER};
EXPECT_EQ(PropsOf(p, GL_UNIFORM_BLOCK, "SimpleBlock", refProps), (std::vector<GLint>{1, 0}));
EXPECT_EQ(PropsOf(p, GL_UNIFORM_BLOCK, "TrickyBlock[0]", refProps), (std::vector<GLint>{0, 1}));
EXPECT_EQ(PropsOf(p, GL_UNIFORM_BLOCK, "TrickyBlock[1]", refProps), (std::vector<GLint>{0, 0}))
<< "the unreferenced element of a block array must not inherit its sibling's stages";
EXPECT_EQ(TakeError(), GL_NO_ERROR);
}
// The boundary of the rule above, and the case that caught it on device
// (KHR-GL43.program_interface_query.ssb-types): a SHADER STORAGE block array's buffer
// variables reflect under ONE subscript-free spelling shared by every element, so a union
// over them credits element 0 and starves every other element - even the ones the shader
// plainly reads. Storage blocks keep glslang's own mask, and both elements here must report
// the fragment stage.
TEST_F(ProgramInterfaceTest, ArrayedStorageBlockKeepsGlslangStagesForEveryElement) {
const char* vs = R"(#version 430
in vec4 position;
void main(void) { gl_Position = position; }
)";
const char* fs = R"(#version 430
layout(binding = 4) buffer SimpleStorage { vec4 a; } ss[2];
out vec4 color;
void main() { color = ss[0].a + ss[1].a; }
)";
const GLuint p = MakeProgram(vs, fs);
BindAttribLocation(p, 0, "position");
BindFragDataLocation(p, 0, "color");
LinkProgram(p);
ExpectLinked(p);
ClearErrors();
const std::vector<GLenum> refProps = {GL_REFERENCED_BY_VERTEX_SHADER, GL_REFERENCED_BY_FRAGMENT_SHADER};
for (const char* name : {"SimpleStorage[0]", "SimpleStorage[1]"}) {
EXPECT_EQ(PropsOf(p, GL_SHADER_STORAGE_BLOCK, name, refProps), (std::vector<GLint>{0, 1}))
<< "for " << name << ": a storage block the fragment stage reads must say so";
}
EXPECT_EQ(TakeError(), GL_NO_ERROR);
}
// --------------------------------------------------------- separate-programs-vertex --
// A vertex-only separable program's OUTPUT interface is its own stage outputs, and an
// inter-stage block enumerates as its members: "Color.r", and "gl_Position" for the
// anonymous gl_PerVertex redeclaration - never the block instance name "vs_color".
TEST_F(ProgramInterfaceTest, SeparableVertexProgramEnumeratesItsOwnOutputBlockMembers) {
const char* vs = R"(#version 430 core
layout(location = 0) in vec4 in_vertex;
out Color { float r, g, b; vec4 iLikePie; } vs_color;
out gl_PerVertex { vec4 gl_Position; };
uniform float u;
uniform vec4 v;
void main() {
gl_Position = in_vertex;
vs_color.r = u; vs_color.g = 0.0; vs_color.b = 0.0; vs_color.iLikePie = v;
}
)";
const GLuint p = CreateShaderProgramv(GL_VERTEX_SHADER, 1, &vs);
ExpectLinked(p);
ClearErrors();
// Exactly 5: the four Color members plus gl_Position. The anonymous gl_PerVertex
// redeclaration drops gl_PointSize and gl_ClipDistance, which glslang keeps in the
// block type as hidden members rather than erasing.
EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_INPUT, GL_ACTIVE_RESOURCES), 1);
EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_INPUT, GL_MAX_NAME_LENGTH), 10);
EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_OUTPUT, GL_ACTIVE_RESOURCES), 5);
EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_OUTPUT, GL_MAX_NAME_LENGTH), 15);
ExpectResource(p, GL_PROGRAM_INPUT, "in_vertex", "in_vertex");
for (const char* name : {"Color.r", "Color.g", "Color.b", "Color.iLikePie", "gl_Position"}) {
ExpectResource(p, GL_PROGRAM_OUTPUT, name, name);
}
EXPECT_EQ(GetProgramResourceIndex(p, GL_PROGRAM_OUTPUT, "vs_color"), GL_INVALID_INDEX)
<< "the block instance is not the resource; its members are";
// A vertex-stage output has no color number, hence no index either, and it is not
// per-patch. NAME_LENGTH/TYPE/ARRAY_SIZE come from the member, not the block.
EXPECT_EQ(PropsOf(p, GL_PROGRAM_OUTPUT, "Color.iLikePie",
{GL_NAME_LENGTH, GL_TYPE, GL_ARRAY_SIZE, GL_REFERENCED_BY_COMPUTE_SHADER,
GL_REFERENCED_BY_FRAGMENT_SHADER, GL_REFERENCED_BY_GEOMETRY_SHADER,
GL_REFERENCED_BY_TESS_CONTROL_SHADER, GL_REFERENCED_BY_TESS_EVALUATION_SHADER,
GL_REFERENCED_BY_VERTEX_SHADER, GL_IS_PER_PATCH, GL_LOCATION_INDEX}),
(std::vector<GLint>{15, GL_FLOAT_VEC4, 1, 0, 0, 0, 0, 0, 1, 0, -1}));
EXPECT_EQ(TakeError(), GL_NO_ERROR);
}
// ------------------------------------------------------- separate-programs-geometry --
// Both boundaries are a middle stage here. The input block carries an instance name
// (gl_in[]) so its members are prefixed with the BLOCK name, and the arrayed-ness of the
// block itself is dropped: "gl_PerVertex.gl_Position", array size 1.
TEST_F(ProgramInterfaceTest, SeparableGeometryProgramEnumeratesBothBlockBoundaries) {
const char* gs = R"(#version 430
layout(triangles) in;
layout(triangle_strip, max_vertices = 4) out;
out gl_PerVertex { vec4 gl_Position; float gl_PointSize; float gl_ClipDistance[]; };
in gl_PerVertex { vec4 gl_Position; float gl_PointSize; float gl_ClipDistance[]; } gl_in[];
void main() {
gl_Position = vec4(-1, 1, 0, 1); EmitVertex();
gl_Position = gl_in[0].gl_Position; EmitVertex();
EndPrimitive();
}
)";
const GLuint p = CreateShaderProgramv(GL_GEOMETRY_SHADER, 1, &gs);
ExpectLinked(p);
ClearErrors();
ExpectResource(p, GL_PROGRAM_INPUT, "gl_PerVertex.gl_Position", "gl_PerVertex.gl_Position");
ExpectResource(p, GL_PROGRAM_OUTPUT, "gl_Position", "gl_Position");
// A non-fragment stage's outputs are varyings: no color number, so no color index.
EXPECT_EQ(PropsOf(p, GL_PROGRAM_OUTPUT, "gl_Position", {GL_LOCATION_INDEX}), (std::vector<GLint>{-1}));
const std::vector<GLenum> stageProps = {
GL_NAME_LENGTH,
GL_TYPE,
GL_ARRAY_SIZE,
GL_REFERENCED_BY_COMPUTE_SHADER,
GL_REFERENCED_BY_FRAGMENT_SHADER,
GL_REFERENCED_BY_GEOMETRY_SHADER,
GL_REFERENCED_BY_TESS_CONTROL_SHADER,
GL_REFERENCED_BY_TESS_EVALUATION_SHADER,
GL_REFERENCED_BY_VERTEX_SHADER,
GL_IS_PER_PATCH};
EXPECT_EQ(PropsOf(p, GL_PROGRAM_INPUT, "gl_PerVertex.gl_Position", stageProps),
(std::vector<GLint>{25, GL_FLOAT_VEC4, 1, 0, 0, 1, 0, 0, 0, 0}));
EXPECT_EQ(PropsOf(p, GL_PROGRAM_OUTPUT, "gl_Position", stageProps),
(std::vector<GLint>{12, GL_FLOAT_VEC4, 1, 0, 0, 1, 0, 0, 0, 0}));
EXPECT_EQ(TakeError(), GL_NO_ERROR);
}
// ------------------------------------------------------- separate-programs-fragment --
TEST_F(ProgramInterfaceTest, SeparableFragmentProgramSeparatesUniformsFromBufferVariables) {
const char* fs = R"(#version 430
@@ -817,11 +968,12 @@ void main() { fs_color = vs_color + x + a; }
ExpectLinked(p);
ClearErrors();
// KNOWN GAP, not an expectation - the same one SimpleShaders documents: a separable
// FRAGMENT program's own inputs are absent from the glslang reflection, so
// GL_PROGRAM_INPUT is empty. Spec-correct values here would be 1 and 9 ("vs_color").
EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_INPUT, GL_ACTIVE_RESOURCES), 0);
EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_INPUT, GL_MAX_NAME_LENGTH), 0);
// GL_PROGRAM_INPUT is the input interface of the program's FIRST stage, which for a
// separable fragment program is the fragment stage: "vs_color", name length 9.
EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_INPUT, GL_ACTIVE_RESOURCES), 1);
EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_INPUT, GL_MAX_NAME_LENGTH), 9);
EXPECT_EQ(GetProgramResourceIndex(p, GL_PROGRAM_INPUT, "vs_color"), 0u);
EXPECT_EQ(ResourceName(p, GL_PROGRAM_INPUT, 0), "vs_color");
EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_OUTPUT, GL_ACTIVE_RESOURCES), 1);
EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_OUTPUT, GL_MAX_NAME_LENGTH), 9);
// The buffer variable is NOT a uniform, even though the frontend reflection keeps
@@ -21,6 +21,7 @@
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <MG_Util/ShaderTranspiler/ShaderSourceProcessor.h>
#include <MG_Util/ShaderTranspiler/SpirvPasses/LegalizeFragmentOutputIndexPass.h>
#include <MG_Util/ShaderTranspiler/SpirvPasses/Lower1DArrayImagesPass.h>
#include <MG_Util/ShaderTranspiler/SpirvPasses/RenameSamplerFunctionParameterPass.h>
#include <MG_Util/ShaderTranspiler/Types.h>
#include <MG_Util/ShaderTranspiler/glslang/UniformTraverser.h>
@@ -3481,3 +3482,523 @@ void main() { fragColor = vec4(texelFetch(Data, 3)); }
<< essl320;
}
namespace {
// OpTypeImage words: result id (+1), sampled type (+2), Dim (+3), Depth (+4), Arrayed (+5),
// MS (+6), Sampled (+7). Dim::Dim1D == 0, and Sampled == 2 is a storage image.
SizeT Count1DArrayStorageImageTypes(const Vector<Uint32>& spirv) {
constexpr unsigned kOpTypeImage = 25, kDim1D = 0;
SizeT count = 0;
for (SizeT i = 5; i < spirv.size();) {
const unsigned wordCount = spirv[i] >> 16;
const unsigned opcode = spirv[i] & 0xFFFFu;
if (wordCount == 0 || i + wordCount > spirv.size()) break;
if (opcode == kOpTypeImage && wordCount >= 8 && spirv[i + 3] == kDim1D && spirv[i + 5] == 1u &&
spirv[i + 7] == 2u) {
++count;
}
i += wordCount;
}
return count;
}
const char* k1DArrayImageCompute = R"(#version 440 core
layout (local_size_x = 1) in;
layout (location = 0, r32ui) readonly uniform uimage1DArray i0;
layout (std430, binding = 0) buffer SSB { uint sum; } ssb;
void main() { ssb.sum = imageLoad(i0, ivec2(2, 3)).r; }
)";
} // namespace
// The negative control, and the whole reason the pass exists: SPIRV-Cross's ES emulation of 1D
// images does not ask whether the type is arrayed, so it wraps an already-two-component
// coordinate in a two-component constructor. Pinning the upstream behaviour here means that if a
// future SPIRV-Cross bump fixes it, this test fails and says so, rather than the pass quietly
// becoming dead weight.
TEST_F(ProgramUtilTest, SpirvCrossEmitsAMalformedCoordinateFor1DArrayImages) {
using namespace MG_Util::ShaderTranspiler;
const Vector<Uint32> spirv = BuildSpirvForStage(k1DArrayImageCompute, GL_COMPUTE_SHADER);
ASSERT_FALSE(spirv.empty());
ASSERT_EQ(Count1DArrayStorageImageTypes(spirv), 1u)
<< "glslang no longer emits a Dim1D/Arrayed/Sampled=2 image for uimage1DArray";
const String essl = DecompileToEssl(spirv);
ASSERT_FALSE(essl.empty());
EXPECT_NE(essl.find("ivec2(ivec2("), String::npos)
<< "SPIRV-Cross is expected to emit ivec2(ivec2(...), 0) here - three components in a "
"two-component constructor, which every ES driver rejects. If this no longer happens, "
"Lower1DArrayImagesForEssl may no longer be needed:\n"
<< essl;
}
// The fix: the type becomes a 2D array and the coordinate becomes three components, so
// SPIRV-Cross's 1D path never fires and the emitted ESSL is something a driver accepts.
TEST_F(ProgramUtilTest, Lower1DArrayImagesRewritesTheTypeAndWidensTheCoordinate) {
using namespace MG_Util::ShaderTranspiler;
const Vector<Uint32> raw = BuildSpirvForStage(k1DArrayImageCompute, GL_COMPUTE_SHADER);
ASSERT_FALSE(raw.empty());
// Through the shared chain first, exactly as the DirectGLES transpile path does: the pass
// runs on sanitized bytes, and the explicit uniform LOCATION this fixture carries (the
// conformance case's own spelling) is illegal on UniformConstant storage until
// StripUniformLocationsPass has removed it. Validating raw glslang output would latch that
// pre-existing property against this pass.
Vector<Uint32> spirv;
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, spirv));
ASSERT_EQ(Count1DArrayStorageImageTypes(spirv), 1u)
<< "the shared chain must leave the 1D-array image for this pass to handle";
SpirvValidationScope validationOn(true);
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
Vector<Uint32> lowered;
ASSERT_TRUE(ShaderCompiler::Lower1DArrayImagesForEssl(spirv, lowered));
ASSERT_FALSE(lowered.empty());
EXPECT_EQ(Count1DArrayStorageImageTypes(lowered), 0u)
<< "no 1D-array storage image type may survive the pass:\n"
<< DisassembleSpirv(lowered);
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
<< "the lowered module must stay validator-clean";
const String essl = DecompileToEssl(lowered);
ASSERT_FALSE(essl.empty());
EXPECT_NE(essl.find("uimage2DArray"), String::npos)
<< "the image must be declared as the 2D array the texture is stored as:\n" << essl;
EXPECT_EQ(essl.find("ivec2(ivec2("), String::npos)
<< "the malformed constructor must be gone:\n" << essl;
// The ORDER is the whole point, and it is what a widening that merely appended the 0 would
// get wrong while still producing a three-component constructor that compiles. The fixture
// reads (u=2, layer=3), and the ES 2D array holds height 1 with the layers in depth
// (TextureImpl::GetBackendUploadSize), so the only correct spelling is (2, 0, 3).
EXPECT_NE(essl.find("ivec3(2, 0, 3)"), String::npos)
<< "the layer must land in the third component and Y must be 0; ivec3(2, 3, 0) would read "
"row 3 of a one-row texture and layer 0 of every access:\n"
<< essl;
}
// The shape that made the first cut of this pass emit INVALID SPIR-V, and the shape the
// conformance case actually has: a 1D-array image and a real 2D-array image of the same sampled
// type and format in one module. Rewriting the first one's Dim in place makes the two
// OpTypeImage declarations structurally identical, and SPIR-V forbids duplicate non-aggregate
// types - so the module the ESSL path hands on failed validation and quietly bumped the latch.
// A single-image fixture cannot see any of that.
TEST_F(ProgramUtilTest, Lower1DArrayImagesDeduplicatesAgainstAnExisting2DArrayImage) {
using namespace MG_Util::ShaderTranspiler;
const Vector<Uint32> raw = BuildSpirvForStage(R"(#version 440 core
layout (local_size_x = 1) in;
layout (location = 0, r32ui) readonly uniform uimage1DArray i0;
layout (location = 1, r32ui) readonly uniform uimage2DArray i1;
layout (std430, binding = 0) buffer SSB { uint sum; } ssb;
void main() { ssb.sum = imageLoad(i0, ivec2(2, 3)).r + imageLoad(i1, ivec3(1, 1, 1)).r; }
)",
GL_COMPUTE_SHADER);
ASSERT_FALSE(raw.empty());
Vector<Uint32> spirv;
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, spirv));
ASSERT_EQ(Count1DArrayStorageImageTypes(spirv), 1u);
SpirvValidationScope validationOn(true);
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
Vector<Uint32> lowered;
ASSERT_TRUE(ShaderCompiler::Lower1DArrayImagesForEssl(spirv, lowered));
ASSERT_FALSE(lowered.empty());
EXPECT_EQ(Count1DArrayStorageImageTypes(lowered), 0u) << DisassembleSpirv(lowered);
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
<< "the rewritten 1D-array image collided with the module's own 2D-array image and left a "
"duplicate type declaration behind:\n"
<< DisassembleSpirv(lowered);
const String essl = DecompileToEssl(lowered);
ASSERT_FALSE(essl.empty());
EXPECT_NE(essl.find("ivec3(2, 0, 3)"), String::npos) << essl;
}
// Scope, half one: a NON-arrayed 1D storage image is emitted correctly by the very same
// SPIRV-Cross code, so the pass must not touch it - replacing working emission with our own buys
// nothing and risks everything.
TEST_F(ProgramUtilTest, Lower1DArrayImagesLeavesNonArrayed1DImagesToSpirvCross) {
using namespace MG_Util::ShaderTranspiler;
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 440 core
layout (local_size_x = 1) in;
layout (location = 0, r32ui) readonly uniform uimage1D i0;
layout (std430, binding = 0) buffer SSB { uint sum; } ssb;
void main() { ssb.sum = imageLoad(i0, 2).r; }
)",
GL_COMPUTE_SHADER);
ASSERT_FALSE(spirv.empty());
Vector<Uint32> lowered;
ASSERT_TRUE(ShaderCompiler::Lower1DArrayImagesForEssl(spirv, lowered));
EXPECT_EQ(lowered, spirv) << "a non-arrayed 1D storage image must pass through byte for byte";
const String essl = DecompileToEssl(lowered);
ASSERT_FALSE(essl.empty());
EXPECT_NE(essl.find("uimage2D "), String::npos)
<< "SPIRV-Cross's own 1D-as-2D emulation must still be what handles this:\n" << essl;
}
// Scope, half two: a 1D-array SAMPLER reaches SPIRV-Cross's sampler path, which does check
// `arrayed` and does move the layer into the third component. The pass is storage-image only.
TEST_F(ProgramUtilTest, Lower1DArrayImagesLeavesSampledImagesAlone) {
using namespace MG_Util::ShaderTranspiler;
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 440 core
uniform sampler1DArray uTex;
in vec2 vUv;
out vec4 fragColor;
void main() { fragColor = texture(uTex, vUv); }
)",
GL_FRAGMENT_SHADER);
ASSERT_FALSE(spirv.empty());
Vector<Uint32> lowered;
ASSERT_TRUE(ShaderCompiler::Lower1DArrayImagesForEssl(spirv, lowered));
EXPECT_EQ(lowered, spirv) << "a sampled 1D-array image must pass through byte for byte";
}
// The declined shape. After the rewrite the image is a 2D array, so a size query on it yields
// three components where the shader consumes two, and there is no correct two-component answer to
// substitute - the ES texture genuinely has a height the GL one does not. The module is handed
// back untouched rather than half-translated.
TEST_F(ProgramUtilTest, Lower1DArrayImagesDeclinesAModuleThatQueriesTheImageSize) {
using namespace MG_Util::ShaderTranspiler;
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 440 core
layout (local_size_x = 1) in;
layout (location = 0, r32ui) readonly uniform uimage1DArray i0;
layout (std430, binding = 0) buffer SSB { uint sum; } ssb;
void main() { ssb.sum = uint(imageSize(i0).x) + imageLoad(i0, ivec2(0, 0)).r; }
)",
GL_COMPUTE_SHADER);
ASSERT_FALSE(spirv.empty());
const auto traits = Lower1DArrayImagesPass::InspectBinary(spirv);
ASSERT_TRUE(traits.declaresImage && traits.queriesImageSize)
<< "the fixture must contain the shape the pass declines";
Vector<Uint32> lowered;
ASSERT_TRUE(ShaderCompiler::Lower1DArrayImagesForEssl(spirv, lowered));
EXPECT_EQ(lowered, spirv) << "a declined module must be handed back untouched, not partly rewritten";
EXPECT_EQ(Count1DArrayStorageImageTypes(lowered), 1u)
<< "declining means the 1D-array type is still there for the driver to reject";
}
// --- image format qualifier bake (BakeImageFormatsPass) ---------------------------------------
//
// Desktop GLSL 4.2 lets a writeonly image declaration omit its format layout qualifier; GLSL ES
// requires one of every image, and Adreno says so as "all images have to define layout format",
// losing the whole program. The only correct qualifier to substitute is the format the
// application passed to glBindImageTexture for that unit, so the transpile bakes it in.
namespace {
Uint CountSpirvOpcode(const String& disassembly, const String& opcode) {
Uint count = 0;
SizeT offset = 0;
const String needle = opcode + " ";
while ((offset = disassembly.find(needle, offset)) != String::npos) {
count += 1;
offset += needle.size();
}
return count;
}
constexpr Uint kGlR32ui = 0x8236;
constexpr Uint kGlRgba32ui = 0x8D70;
constexpr Uint kGlR8ui = 0x8232;
constexpr Uint kGlR32f = 0x822E;
} // namespace
// The KHR-GL4x.packed_depth_stencil.stencil_texturing compute shader, reduced: one format-less
// writeonly image, and a bind of a concrete format to the unit it addresses. (The DEPTH half of
// that case binds GL_R32F; the stencil half's GL_R8UI is one SPIRV-Cross will not print and takes
// the text route instead - see the test below.)
TEST_F(ProgramUtilTest, BakeImageFormatsGivesAFormatlessImageTheFormatBoundToItsUnit) {
using namespace MG_Util::ShaderTranspiler;
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 430 core
layout (local_size_x = 1) in;
writeonly uniform uimage2D uni_image;
void main() { imageStore(uni_image, ivec2(gl_GlobalInvocationID.xy), uvec4(15u, 0u, 0u, 0u)); }
)",
GL_COMPUTE_SHADER);
ASSERT_FALSE(spirv.empty());
ASSERT_TRUE(ShaderCompiler::DeclaresFormatlessStorageImage(spirv))
<< "the fixture must reproduce the defect before the fix is asked to remove it:\n"
<< DisassembleSpirv(spirv);
// Precondition: SPIRV-Cross prints no format for it, which is the ESSL the driver refuses.
EXPECT_EQ(DecompileToEssl(spirv).find("r32ui"), String::npos);
SpirvValidationScope validationOn(true);
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
Vector<Uint32> baked;
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR32ui}}, baked));
ASSERT_FALSE(baked.empty());
EXPECT_FALSE(ShaderCompiler::DeclaresFormatlessStorageImage(baked)) << DisassembleSpirv(baked);
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
<< "the baked module must stay validator-clean:\n"
<< DisassembleSpirv(baked);
const String essl = DecompileToEssl(baked);
ASSERT_FALSE(essl.empty());
EXPECT_NE(essl.find("r32ui"), String::npos)
<< "the bound format must reach the declaration as a layout qualifier:\n" << essl;
EXPECT_NE(essl.find("writeonly"), String::npos)
<< "the access qualifier the declaration already had must survive:\n" << essl;
}
// SPIRV-Cross THROWS rather than printing the formats it calls desktop-only when it targets ESSL
// (Compiler::is_desktop_only_format), and a throw loses the whole stage - so baking one of those
// into the module would trade a missing qualifier for a missing shader. They are left format-less
// here and completed on the emitted text instead (PrgramImpl::BakeImageFormatQualifiers). r8ui,
// which the stencil half of the packed_depth_stencil case binds, is one of them.
TEST_F(ProgramUtilTest, BakeImageFormatsLeavesTheFormatsSpirvCrossRefusesToPrint) {
using namespace MG_Util::ShaderTranspiler;
ASSERT_FALSE(ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(kGlR8ui))
<< "if SPIRV-Cross ever learns to print r8ui for ES, the text completion can go";
ASSERT_TRUE(ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(kGlR32ui));
EXPECT_EQ(ShaderCompiler::EsslImageFormatSpelling(kGlR8ui), "r8ui");
EXPECT_EQ(ShaderCompiler::EsslImageFormatSpelling(0x8051 /*GL_RGB8*/), "");
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 430 core
layout (local_size_x = 1) in;
writeonly uniform uimage2D uni_image;
void main() { imageStore(uni_image, ivec2(0), uvec4(15u)); }
)",
GL_COMPUTE_SHADER);
ASSERT_FALSE(spirv.empty());
Vector<Uint32> baked;
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR8ui}}, baked));
EXPECT_EQ(baked, spirv) << "a format SPIRV-Cross cannot print must leave the module untouched";
// ...and the stage still transpiles, which is the whole point of declining.
EXPECT_FALSE(DecompileToEssl(baked).empty());
}
// A DECLARED format is authoritative: GL requires the qualifier, the bind format and the
// texture's internal format to be in the same class, but the qualifier is what the shader is
// specified to read the memory as, and a bake that overrode it would change what the shader does.
TEST_F(ProgramUtilTest, BakeImageFormatsNeverOverridesADeclaredFormat) {
using namespace MG_Util::ShaderTranspiler;
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 430 core
layout (local_size_x = 1) in;
layout (binding = 0, rgba32ui) writeonly uniform uimage2D uni_image;
void main() { imageStore(uni_image, ivec2(0), uvec4(1u)); }
)",
GL_COMPUTE_SHADER);
ASSERT_FALSE(spirv.empty());
ASSERT_FALSE(ShaderCompiler::DeclaresFormatlessStorageImage(spirv));
Vector<Uint32> baked;
// Even asked to, with a format of the right component class.
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR32ui}}, baked));
EXPECT_EQ(baked, spirv) << "a module with nothing format-less must pass through byte for byte";
EXPECT_NE(DecompileToEssl(baked).find("rgba32ui"), String::npos);
}
// Review finding. Every use has to be one the retype can carry end to end, and the decision has
// to be made BEFORE anything is mutated - a half-retyped module is not something a later decline
// could undo. An image handed to a FUNCTION is the shape that reaches SPIRV-Cross intact (nothing
// in the ESSL chain inlines), and its OpFunctionCall is a use this pass does not follow.
TEST_F(ProgramUtilTest, BakeImageFormatsDeclinesAnImagePassedToAFunction) {
using namespace MG_Util::ShaderTranspiler;
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 430 core
layout (local_size_x = 1) in;
writeonly uniform uimage2D uni_image;
void writeIt(writeonly uimage2D img) { imageStore(img, ivec2(0), uvec4(1u)); }
void main() { writeIt(uni_image); }
)",
GL_COMPUTE_SHADER);
ASSERT_FALSE(spirv.empty());
ASSERT_TRUE(ShaderCompiler::DeclaresFormatlessStorageImage(spirv));
SpirvValidationScope validationOn(true);
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
Vector<Uint32> baked;
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR32ui}}, baked));
EXPECT_EQ(baked, spirv) << "a shape the retype cannot follow must leave the module untouched, "
"not partly rewritten:\n"
<< DisassembleSpirv(baked);
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore);
}
// spirv-val requires the Image Format's component class to agree with the OpTypeImage's Sampled
// Type. Binding a uint format to a float image is an application error GL leaves undefined;
// baking it would turn that into an INVALID module, which is strictly worse than the compile
// error the shader already has, so the image is left format-less.
TEST_F(ProgramUtilTest, BakeImageFormatsDeclinesAFormatOfTheWrongComponentClass) {
using namespace MG_Util::ShaderTranspiler;
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 430 core
layout (local_size_x = 1) in;
writeonly uniform image2D uni_image;
void main() { imageStore(uni_image, ivec2(0), vec4(1.0)); }
)",
GL_COMPUTE_SHADER);
ASSERT_FALSE(spirv.empty());
SpirvValidationScope validationOn(true);
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
Vector<Uint32> baked;
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR32ui}}, baked));
EXPECT_EQ(baked, spirv) << "a declined module must be handed back untouched, not partly rewritten";
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore);
// ...and the same image with a float bind format is baked, so the decline above is about the
// class and not about the pass refusing float images.
Vector<Uint32> bakedFloat;
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR32f}}, bakedFloat));
EXPECT_NE(DecompileToEssl(bakedFloat).find("r32f"), String::npos) << DisassembleSpirv(bakedFloat);
}
// Two format-less images of the same type share ONE OpTypeImage. Giving them different formats
// therefore cannot be an in-place edit of that type - each needs its own declaration, and the
// variable, the loads and (for arrays) the access chains all have to follow.
TEST_F(ProgramUtilTest, BakeImageFormatsSplitsATypeTwoImagesShareWhenTheirFormatsDiffer) {
using namespace MG_Util::ShaderTranspiler;
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 430 core
layout (local_size_x = 1) in;
writeonly uniform uimage2D imgA;
writeonly uniform uimage2D imgB;
void main() {
imageStore(imgA, ivec2(0), uvec4(1u));
imageStore(imgB, ivec2(0), uvec4(2u));
}
)",
GL_COMPUTE_SHADER);
ASSERT_FALSE(spirv.empty());
ASSERT_EQ(CountSpirvOpcode(DisassembleSpirv(spirv), "OpTypeImage"), 1u)
<< "the fixture must have the two images sharing one type:\n" << DisassembleSpirv(spirv);
SpirvValidationScope validationOn(true);
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
Vector<Uint32> baked;
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(
spirv, {{"imgA", kGlR32ui}, {"imgB", kGlRgba32ui}}, baked));
ASSERT_FALSE(baked.empty());
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
<< "splitting the shared type must not leave a dangling or duplicate declaration:\n"
<< DisassembleSpirv(baked);
EXPECT_FALSE(ShaderCompiler::DeclaresFormatlessStorageImage(baked));
const String essl = DecompileToEssl(baked);
ASSERT_FALSE(essl.empty());
EXPECT_NE(essl.find("r32ui"), String::npos) << essl;
EXPECT_NE(essl.find("rgba32ui"), String::npos) << essl;
}
// The mirror of the split: when the module ALREADY declares the type the bake wants, the two must
// be JOINED, not duplicated. SPIR-V forbids two identical non-aggregate type declarations, and
// that is exactly the defect an earlier image pass shipped and a reviewer caught.
TEST_F(ProgramUtilTest, BakeImageFormatsJoinsATypeTheModuleAlreadyDeclares) {
using namespace MG_Util::ShaderTranspiler;
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 430 core
layout (local_size_x = 1) in;
writeonly uniform uimage2D formatless;
layout (binding = 1, r32ui) writeonly uniform uimage2D declared;
void main() {
imageStore(formatless, ivec2(0), uvec4(1u));
imageStore(declared, ivec2(0), uvec4(2u));
}
)",
GL_COMPUTE_SHADER);
ASSERT_FALSE(spirv.empty());
ASSERT_EQ(CountSpirvOpcode(DisassembleSpirv(spirv), "OpTypeImage"), 2u)
<< "the fixture needs one Unknown-format and one r32ui image type:\n" << DisassembleSpirv(spirv);
SpirvValidationScope validationOn(true);
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
Vector<Uint32> baked;
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"formatless", kGlR32ui}}, baked));
ASSERT_FALSE(baked.empty());
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
<< "the baked image collided with the module's own r32ui image and left a duplicate type:\n"
<< DisassembleSpirv(baked);
EXPECT_EQ(CountSpirvOpcode(DisassembleSpirv(baked), "OpTypeImage"), 1u)
<< "the two identical image types must be the same declaration:\n" << DisassembleSpirv(baked);
}
// An ARRAY of format-less images: the variable's type is a pointer to an array, every use goes
// through an OpAccessChain, and all three levels have to be rebuilt for the load to still type-check.
TEST_F(ProgramUtilTest, BakeImageFormatsRetypesAnArrayOfFormatlessImagesThroughItsAccessChains) {
using namespace MG_Util::ShaderTranspiler;
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 430 core
layout (local_size_x = 1) in;
writeonly uniform uimage2D imgs[2];
void main() {
for (int i = 0; i < 2; ++i) imageStore(imgs[i], ivec2(0), uvec4(uint(i)));
}
)",
GL_COMPUTE_SHADER);
ASSERT_FALSE(spirv.empty());
ASSERT_TRUE(ShaderCompiler::DeclaresFormatlessStorageImage(spirv));
SpirvValidationScope validationOn(true);
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
Vector<Uint32> baked;
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"imgs", kGlR32ui}}, baked));
ASSERT_FALSE(baked.empty());
EXPECT_FALSE(ShaderCompiler::DeclaresFormatlessStorageImage(baked)) << DisassembleSpirv(baked);
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
<< "the array and pointer types above the image must have been rebuilt too:\n"
<< DisassembleSpirv(baked);
EXPECT_NE(DecompileToEssl(baked).find("r32ui"), String::npos);
}
// A SAMPLED image's format operand is Unknown in every GLSL dialect and has no qualifier to bake;
// only storage images (Sampled == 2) are in scope.
TEST_F(ProgramUtilTest, BakeImageFormatsLeavesSampledImagesAlone) {
using namespace MG_Util::ShaderTranspiler;
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 430 core
uniform usampler2D uni_sampler;
out uvec4 fragColor;
in vec2 vUv;
void main() { fragColor = texture(uni_sampler, vUv); }
)",
GL_FRAGMENT_SHADER);
ASSERT_FALSE(spirv.empty());
EXPECT_FALSE(ShaderCompiler::DeclaresFormatlessStorageImage(spirv))
<< "a sampled image must not read as a format-less STORAGE image:\n" << DisassembleSpirv(spirv);
Vector<Uint32> baked;
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_sampler", kGlR32ui}}, baked));
EXPECT_EQ(baked, spirv) << "a sampled image must pass through byte for byte";
}
// The core/extended split the emitted ESSL depends on: GLSL ES has thirteen image formats, and a
// bind format outside them only compiles with GL_NV_image_formats - which the backend must not
// request on a driver that does not advertise it.
TEST_F(ProgramUtilTest, EsslCoreImageFormatSetIsTheThirteenTheSpecLists) {
using namespace MG_Util::ShaderTranspiler;
EXPECT_TRUE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(kGlR32ui));
EXPECT_TRUE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(kGlRgba32ui));
EXPECT_TRUE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(kGlR32f));
EXPECT_TRUE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(0x8058 /*GL_RGBA8*/));
// The stencil half of KHR-GL4x.packed_depth_stencil.stencil_texturing binds this one, and it
// is NOT core - the whole reason the directive machinery exists.
EXPECT_FALSE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(kGlR8ui));
EXPECT_FALSE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(0x822D /*GL_R16F*/));
// Not an image format at all.
EXPECT_FALSE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(0x8051 /*GL_RGB8*/));
EXPECT_FALSE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(0 /*GL_NONE*/));
}
+93 -2
View File
@@ -198,6 +198,38 @@ TEST(DirectGLESSanity, AdvertisesDepthTextureForGlmarkShadowScenes) {
EXPECT_NE(std::find(extensions.begin(), extensions.end(), MobileGL::E_GL_ARB_depth_texture), extensions.end());
}
// Two strings that name capabilities MobileGL has always had, and that were missing from the
// advertised list for as long as it existed.
//
// GL_ARB_uniform_buffer_object is the one with teeth: applications gate the ENTRY POINTS on the
// string rather than on the context version. KHR-GL4x.transform_feedback.draw_xfb_instanced_test
// resolves glGetUniformBlockIndex / glUniformBlockBinding only inside `if (is_arb_ubo)`, then
// calls them unconditionally because the context claims >= 4.2 - so a missing string turned into
// a call through a null pointer and took the whole process down with SIGSEGV. Withdrawing it
// again would restore that crash on both backends.
//
// GL_ARB_stencil_texturing is what makes DEPTH_STENCIL_TEXTURE_MODE = GL_STENCIL_INDEX reachable
// at all before GL 4.3, which is the whole of KHR-GL3x.packed_depth_stencil.stencil_texturing.
TEST(DirectGLESSanity, AdvertisesUniformBufferObjectAndStencilTexturing) {
MobileGL::MG_Backend::DirectGLES::BackendObject_DirectGLES backend;
const auto& extensions = backend.GetRendererInfo().RendererGLInfo.Extensions;
EXPECT_NE(std::find(extensions.begin(), extensions.end(), MobileGL::E_GL_ARB_uniform_buffer_object),
extensions.end());
EXPECT_NE(std::find(extensions.begin(), extensions.end(), MobileGL::E_GL_ARB_stencil_texturing),
extensions.end());
}
TEST(DirectVulkanSanity, AdvertisesUniformBufferObjectAndStencilTexturing) {
MobileGL::MG_Backend::DirectVulkan::BackendObject_DirectVulkan backend;
const auto& extensions = backend.GetRendererInfo().RendererGLInfo.Extensions;
EXPECT_NE(std::find(extensions.begin(), extensions.end(), MobileGL::E_GL_ARB_uniform_buffer_object),
extensions.end());
EXPECT_NE(std::find(extensions.begin(), extensions.end(), MobileGL::E_GL_ARB_stencil_texturing),
extensions.end());
}
// Voxy only ever needed the extensions, which stay advertised whatever the version is; the version
// assertion just pins what the backend really reports, now that V_OpenGL40 is in the list.
TEST(DirectGLESSanity, AdvertisesVoxyRequiredRenderingExtensions) {
@@ -346,8 +378,10 @@ void main() {
EXPECT_NE(rewritten.find("int instance = mg_ZeroBasedInstanceID + mg_BaseInstanceLowered;"),
MobileGL::String::npos);
EXPECT_NE(rewritten.find("#define mg_ZeroBasedInstanceID (gl_InstanceID - ((mg_BaseInstanceWordIndex >= 0) ? "
"int(mg_indirectWords[uint(mg_BaseInstanceWordIndex)]) : 0))"),
// One-based word index: zero is the "not an indirect draw" sentinel because that is
// the value a GLSL uniform starts at and no draw path writes it before the first draw.
EXPECT_NE(rewritten.find("#define mg_ZeroBasedInstanceID (gl_InstanceID - ((mg_BaseInstanceWordIndex > 0) ? "
"int(mg_indirectWords[uint(mg_BaseInstanceWordIndex - 1)]) : 0))"),
MobileGL::String::npos);
EXPECT_NE(rewritten.find(
"layout(std430, binding = 12) readonly buffer mg_IndirectParams { highp uint mg_indirectWords[]; };"),
@@ -356,6 +390,38 @@ void main() {
EXPECT_EQ(CountOccurrences(rewritten, "gl_InstanceID"), 1u);
}
// The sentinel itself, on the builtin it exists for. A zero-based index with a
// negative "off" value made every NON-indirect draw of such a program read
// mg_indirectWords[0] out of a storage buffer nothing had bound - the uniform starts
// at zero and no non-indirect draw path writes it - which is where the CTS
// shader_draw_parameters cases lost their geometry on Adreno. Pinned as text because
// this contract lives in two places at once: the generated ESSL below and the +1 that
// BackendProgramObjectImpl::SetBaseInstanceWordIndex applies.
TEST(DirectGLESSanity, TheIndirectWordIndexIsOneBasedSoItsUnwrittenValueMeansNotIndirect) {
const ScopedGLESCapabilitiesOverride capsGuard;
auto& caps = MobileGL::MG_Backend::DirectGLES::g_GLESCapabilities;
caps.IndirectDrawInstanceIdIncludesBaseInstance = false;
caps.MaxShaderStorageBufferBindings = 13;
const MobileGL::String source = R"(#version 310 es
highp int mg_BaseInstanceLowered;
void main() {
gl_Position = vec4(float(mg_BaseInstanceLowered));
}
)";
const auto rewritten = MobileGL::MG_Backend::DirectGLES::PromoteDrawParameterGlobalsToUniforms(
source, GL_VERTEX_SHADER);
EXPECT_NE(rewritten.find("#define mg_BaseInstanceLowered ((mg_BaseInstanceWordIndex > 0) ? "
"int(mg_indirectWords[uint(mg_BaseInstanceWordIndex - 1)]) : mg_BaseInstance)"),
MobileGL::String::npos)
<< rewritten;
// A zero-based form would spell either of these; neither may survive.
EXPECT_EQ(rewritten.find("mg_BaseInstanceWordIndex >= 0"), MobileGL::String::npos);
EXPECT_EQ(rewritten.find("uint(mg_BaseInstanceWordIndex)"), MobileGL::String::npos);
}
TEST(DirectGLESSanity, KeepsInstanceIdWhenIndirectDrawsAreConforming) {
const ScopedGLESCapabilitiesOverride capsGuard;
auto& caps = MobileGL::MG_Backend::DirectGLES::g_GLESCapabilities;
@@ -401,6 +467,31 @@ TEST(DirectVulkanSanity, AdvertisesTextureStorageForDirectStateAccess) {
EXPECT_NE(std::find(extensions.begin(), extensions.end(), MobileGL::E_GL_ARB_texture_storage), extensions.end());
}
// A sampled view of a combined depth/stencil image may name exactly one aspect, and
// GL_DEPTH_STENCIL_TEXTURE_MODE picks which - the whole of GL_ARB_stencil_texturing on this
// backend. Depth remains the answer for everything that does not ask for stencil, including
// depth-only images asked for the stencil aspect they do not have.
TEST(DirectVulkanSanity, SampledViewAspectFollowsDepthStencilTextureMode) {
using MobileGL::MG_Backend::DirectVulkan::VkTextureManager;
constexpr VkImageAspectFlags kPacked = VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT;
EXPECT_EQ(VkTextureManager::ResolveSampledImageViewAspectMask(kPacked, GL_DEPTH_COMPONENT),
static_cast<VkImageAspectFlags>(VK_IMAGE_ASPECT_DEPTH_BIT));
EXPECT_EQ(VkTextureManager::ResolveSampledImageViewAspectMask(kPacked, GL_STENCIL_INDEX),
static_cast<VkImageAspectFlags>(VK_IMAGE_ASPECT_STENCIL_BIT));
// The default argument is the pre-existing behaviour, for the call sites with no texture.
EXPECT_EQ(VkTextureManager::ResolveSampledImageViewAspectMask(kPacked),
static_cast<VkImageAspectFlags>(VK_IMAGE_ASPECT_DEPTH_BIT));
// Single-aspect images ignore the mode: there is only one aspect to name.
EXPECT_EQ(VkTextureManager::ResolveSampledImageViewAspectMask(VK_IMAGE_ASPECT_DEPTH_BIT, GL_STENCIL_INDEX),
static_cast<VkImageAspectFlags>(VK_IMAGE_ASPECT_DEPTH_BIT));
EXPECT_EQ(VkTextureManager::ResolveSampledImageViewAspectMask(VK_IMAGE_ASPECT_STENCIL_BIT, GL_DEPTH_COMPONENT),
static_cast<VkImageAspectFlags>(VK_IMAGE_ASPECT_STENCIL_BIT));
EXPECT_EQ(VkTextureManager::ResolveSampledImageViewAspectMask(VK_IMAGE_ASPECT_COLOR_BIT, GL_STENCIL_INDEX),
static_cast<VkImageAspectFlags>(VK_IMAGE_ASPECT_COLOR_BIT));
}
TEST(DirectVulkanSanity, RenderPassExtentUsesSwapchainSizeOnlyForDefaultFramebuffer) {
using MobileGL::MG_Backend::DirectVulkan::ResolveRenderPassFramebufferExtent;
@@ -3,6 +3,8 @@ cmake_minimum_required(VERSION 3.14)
add_executable(
SpirvPassTest
SpirvPassTest.cpp
DemoteFloat64Test.cpp
FlattenXfbInterfaceBlocksTest.cpp
)
target_include_directories(SpirvPassTest PRIVATE
@@ -0,0 +1,536 @@
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/DemoteFloat64Test.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include <gtest/gtest.h>
#include <string>
#include <vector>
#include "Includes.h"
#include "Init.h"
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <MG_Util/ShaderTranspiler/SpvcSession.h>
#include <MG_Util/ShaderTranspiler/Types.h>
#include <spirv-tools/libspirv.hpp>
using namespace MobileGL;
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
namespace {
// The test-side reference walker, deliberately independent of the production code: a bug in
// the pass must not be able to hide behind the same helper. Counts OpTypeFloat declarations of
// a given width and collects the Offset literal of every OpMemberDecorate, in module order.
constexpr Uint32 kSpirvHeaderWordCount = 5;
constexpr Uint32 kOpTypeFloat = 22;
constexpr Uint32 kOpName = 5;
constexpr Uint32 kOpMemberDecorate = 72;
constexpr Uint32 kOpFConvert = 115;
constexpr Uint32 kOpCapability = 17;
constexpr Uint32 kDecorationOffset = 35;
constexpr Uint32 kCapabilityFloat64 = 10;
template <typename Visitor>
void ForEachInstruction(const Vector<Uint32>& spirv, Visitor&& visit) {
for (SizeT i = kSpirvHeaderWordCount; i < spirv.size();) {
const Uint32 wordCount = spirv[i] >> 16;
const Uint32 opcode = spirv[i] & 0xFFFFu;
if (wordCount == 0 || i + wordCount > spirv.size()) break;
visit(opcode, &spirv[i], wordCount);
i += wordCount;
}
}
Uint32 CountFloatTypesOfWidth(const Vector<Uint32>& spirv, Uint32 width) {
Uint32 count = 0;
ForEachInstruction(spirv, [&](Uint32 opcode, const Uint32* words, Uint32 wordCount) {
if (opcode == kOpTypeFloat && wordCount >= 3 && words[2] == width) ++count;
});
return count;
}
Uint32 CountFConverts(const Vector<Uint32>& spirv) {
Uint32 count = 0;
ForEachInstruction(spirv, [&](Uint32 opcode, const Uint32*, Uint32) {
if (opcode == kOpFConvert) ++count;
});
return count;
}
Bool DeclaresFloat64Capability(const Vector<Uint32>& spirv) {
Bool found = false;
ForEachInstruction(spirv, [&](Uint32 opcode, const Uint32* words, Uint32 wordCount) {
if (opcode == kOpCapability && wordCount >= 2 && words[1] == kCapabilityFloat64) found = true;
});
return found;
}
// Byte offset of every member of the struct named `blockName`, in member order.
Vector<Uint32> CollectOffsetsOf(const Vector<Uint32>& spirv, const String& blockName) {
Uint32 structId = 0;
ForEachInstruction(spirv, [&](Uint32 opcode, const Uint32* words, Uint32 wordCount) {
if (opcode != kOpName || wordCount < 3 || structId != 0) return;
const char* text = reinterpret_cast<const char*>(&words[2]);
const SizeT maxBytes = (wordCount - 2) * sizeof(Uint32);
if (std::strncmp(text, blockName.c_str(), maxBytes) == 0) structId = words[1];
});
if (structId == 0) return {};
std::map<Uint32, Uint32> offsetByMember;
ForEachInstruction(spirv, [&](Uint32 opcode, const Uint32* words, Uint32 wordCount) {
if (opcode == kOpMemberDecorate && wordCount >= 5 && words[1] == structId &&
words[3] == kDecorationOffset) {
offsetByMember[words[2]] = words[4];
}
});
Vector<Uint32> offsets;
for (const auto& [member, offset] : offsetByMember) offsets.push_back(offset);
return offsets;
}
// Everything the production pipeline does to a source before the pass sees it.
Vector<Uint32> CompileToSpirv(GLenum stage, const String& source) {
using namespace MG_Util::ShaderTranspiler;
ShaderAttrib shaderAttrib{.shaderType = stage, .sourceStr = source};
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
EXPECT_TRUE(shaderResult) << (shaderResult ? String{} : shaderResult.error().log);
if (!shaderResult) return {};
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
EXPECT_TRUE(programResult) << (programResult ? String{} : programResult.error().log);
if (!programResult) return {};
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {stage}, .program = *programResult.value()};
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
EXPECT_TRUE(binaryResult) << (binaryResult ? String{} : binaryResult.error().log);
if (!binaryResult || binaryResult->empty()) return {};
return binaryResult->front();
}
String Disassemble(const Vector<Uint32>& spirv) {
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
String text;
tools.Disassemble(spirv, &text);
return text;
}
// A vertex shader that exercises every shape the pass has to handle at once: a block with
// double / dvec2 / dvec3 / dvec4 / dmat4 members between two floats (so a shifted offset would
// be visible), a default-block double uniform, a 64-bit vertex input, a double-typed array, an
// implicit float->double conversion and an explicit double->float one.
const char* kWideVertexSource = R"(#version 460 core
layout(std140, binding = 0) uniform Blk {
float a;
double d;
dvec2 v2;
dvec3 v3;
dvec4 v4;
dmat4 m4;
double arr[3];
float z;
};
layout(location = 0) uniform double uScale;
layout(location = 0) in dvec3 inPos;
layout(location = 1) in vec3 inNormal;
layout(location = 0) out float vOut;
void main() {
double s = d * uScale + a;
dvec3 p = inPos * v3 + v2.xyx + v4.xyz + dvec3(m4[0].xyz);
s += p.x + p.y + p.z + arr[0] + arr[1] + arr[2] + z + 0.5lf;
vOut = float(s) + inNormal.x;
gl_Position = vec4(float(s));
}
)";
} // namespace
class DemoteFloat64Test : public ::testing::Test {
protected:
void SetUp() override {
MobileGL::Initialize();
ShaderCompiler::SetSpirvValidationEnabled(true);
m_validationFailuresAtStart = ShaderCompiler::SpirvValidationFailureCount();
}
void TearDown() override {
// The wrapper validates its OUTPUT on every run, so this covers every demotion the test
// performed without any of them having to say so.
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), m_validationFailuresAtStart)
<< "the demoted module did not survive spirv-val";
}
Uint64 m_validationFailuresAtStart = 0;
};
TEST_F(DemoteFloat64Test, DemotesEveryWidthAndDropsTheCapability) {
const Vector<Uint32> input = CompileToSpirv(GL_VERTEX_SHADER, kWideVertexSource);
ASSERT_FALSE(input.empty());
ASSERT_EQ(CountFloatTypesOfWidth(input, 64), 1u) << Disassemble(input);
ASSERT_TRUE(DeclaresFloat64Capability(input));
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output));
EXPECT_EQ(CountFloatTypesOfWidth(output, 64), 0u) << Disassemble(output);
// And exactly one 32-bit float type survives: the merge has to happen, or spirv-val rejects
// the second declaration.
EXPECT_EQ(CountFloatTypesOfWidth(output, 32), 1u) << Disassemble(output);
EXPECT_FALSE(DeclaresFloat64Capability(output));
}
TEST_F(DemoteFloat64Test, RederivesTheStd140LayoutOfADemotedUniformBlock) {
const String source = R"(#version 460 core
layout(std140, binding = 0) uniform Blk {
float a;
double d;
dvec2 v2;
dvec3 v3;
dvec4 v4;
dmat4 m4;
double arr[3];
float z;
};
layout(location = 0) out float vOut;
void main() {
vOut = float(d + v2.x + v3.y + v4.z + m4[2].w + arr[1] + a + z);
gl_Position = vec4(vOut);
}
)";
const Vector<Uint32> input = CompileToSpirv(GL_VERTEX_SHADER, source);
ASSERT_FALSE(input.empty());
// What glslang laid out for the 64-bit members, which is what an application computing
// std140 by hand would also get.
EXPECT_EQ(CollectOffsetsOf(input, "Blk"), (Vector<Uint32>{0, 8, 16, 32, 64, 96, 224, 272}))
<< Disassemble(input);
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output));
// std140 for the demoted members: float at 4, vec2 at 8, vec3 at 16 (aligned like a vec4),
// vec4 at 32, mat4 at 48 with a 16-byte column stride, the array at 112 with the std140
// 16-byte element stride, and the trailing float at 160. This is what SPIRV-Cross has to be
// able to re-derive for GLSL ES, which has no member layout(offset=) to fall back on.
EXPECT_EQ(CollectOffsetsOf(output, "Blk"), (Vector<Uint32>{0, 4, 8, 16, 32, 48, 112, 160}))
<< Disassemble(output);
const String text = Disassemble(output);
EXPECT_NE(text.find("MatrixStride 16"), String::npos) << text;
EXPECT_NE(text.find("ArrayStride 16"), String::npos) << text;
}
TEST_F(DemoteFloat64Test, RederivesTheStd430LayoutOfADemotedStorageBlock) {
const String source = R"(#version 460 core
layout(std430, binding = 0) buffer Ssbo {
double head;
dvec4 wide;
double tail[4];
};
layout(location = 0) out float vOut;
void main() {
vOut = float(head + wide.w + tail[3]);
gl_Position = vec4(vOut);
}
)";
const Vector<Uint32> input = CompileToSpirv(GL_VERTEX_SHADER, source);
ASSERT_FALSE(input.empty());
EXPECT_EQ(CollectOffsetsOf(input, "Ssbo"), (Vector<Uint32>{0, 32, 64})) << Disassemble(input);
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output));
// std430, so the array packs at its element size rather than being rounded to 16: float at 0,
// vec4 at 16, float[4] at 32 with a 4-byte stride. A storage block must NOT come out std140,
// which is the whole reason the packing is chosen per storage class.
EXPECT_EQ(CollectOffsetsOf(output, "Ssbo"), (Vector<Uint32>{0, 16, 32})) << Disassemble(output);
EXPECT_NE(Disassemble(output).find("ArrayStride 4"), String::npos) << Disassemble(output);
}
TEST_F(DemoteFloat64Test, LeavesTheLayoutOfABlockWithoutDoublesAlone) {
const String source = R"(#version 460 core
layout(std140, binding = 0) uniform Blk {
float a;
vec3 v3;
mat4 m4;
};
layout(std140, binding = 1) uniform Wide {
float w;
double d;
};
layout(location = 0) out float vOut;
void main() {
vOut = float(a + v3.y + m4[1].z + float(d) + w);
gl_Position = vec4(vOut);
}
)";
const Vector<Uint32> input = CompileToSpirv(GL_VERTEX_SHADER, source);
ASSERT_FALSE(input.empty());
const Vector<Uint32> before = CollectOffsetsOf(input, "Blk");
ASSERT_FALSE(before.empty());
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output));
// Only the block that actually narrowed is re-laid-out. Touching the other one would be
// churn at best, and a disagreement with glslang's own layout at worst.
EXPECT_EQ(CollectOffsetsOf(output, "Blk"), before) << Disassemble(output);
EXPECT_EQ(CollectOffsetsOf(output, "Wide"), (Vector<Uint32>{0, 4})) << Disassemble(output);
}
TEST_F(DemoteFloat64Test, FoldsTheConversionsThatBecameIdentities) {
const Vector<Uint32> input = CompileToSpirv(GL_VERTEX_SHADER, kWideVertexSource);
ASSERT_FALSE(input.empty());
ASSERT_GT(CountFConverts(input), 0u) << "the fixture no longer converts between the two widths";
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output));
// SPIR-V requires the two component widths of an OpFConvert to differ, so every one of them
// has to be gone: both sides are 32 bits now.
EXPECT_EQ(CountFConverts(output), 0u) << Disassemble(output);
}
TEST_F(DemoteFloat64Test, NarrowsDoubleConstantsToTheirFloatValue) {
const String source = R"(#version 460 core
layout(location = 0) out float outValue;
void main() {
double d = 0.5lf;
outValue = float(d * 0.25lf);
gl_Position = vec4(0.0);
}
)";
const Vector<Uint32> input = CompileToSpirv(GL_VERTEX_SHADER, source);
ASSERT_FALSE(input.empty());
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output));
// A 64-bit literal is two words wide and a 32-bit one is a single word, so a constant left
// unconverted is not merely imprecise - it is an unparseable instruction. Disassembling both
// values proves the re-encode produced the right number, not just the right width.
const String text = Disassemble(output);
EXPECT_NE(text.find("OpConstant %float 0.5"), String::npos) << text;
EXPECT_NE(text.find("OpConstant %float 0.25"), String::npos) << text;
}
TEST_F(DemoteFloat64Test, LeavesAModuleWithoutDoublesByteIdentical) {
const String source = R"(#version 460 core
layout(location = 0) in vec4 inPos;
void main() { gl_Position = inPos; }
)";
const Vector<Uint32> input = CompileToSpirv(GL_VERTEX_SHADER, source);
ASSERT_FALSE(input.empty());
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output));
// The pass reports SuccessWithoutChange here, and SPIRV-Tools asserts (in assert-enabled
// builds) that such a run round-trips byte-identically.
EXPECT_EQ(output, input);
}
TEST_F(DemoteFloat64Test, DeclinesAModuleThatBitcastsAcrossTheWidthBoundary) {
// packDouble2x32 is defined only for a 64-bit result: there is no 32-bit answer to give, and
// narrowing one side of the surrounding OpBitcast alone produces a module spirv-val rejects.
// The contract is that such a module comes back untouched rather than broken.
const String source = R"(#version 460 core
#extension GL_ARB_gpu_shader_fp64 : require
layout(location = 0) uniform uvec2 uPacked;
layout(location = 0) out float outValue;
void main() {
double d = packDouble2x32(uPacked);
outValue = float(d);
gl_Position = vec4(0.0);
}
)";
const Vector<Uint32> input = CompileToSpirv(GL_VERTEX_SHADER, source);
ASSERT_FALSE(input.empty());
ASSERT_EQ(CountFloatTypesOfWidth(input, 64), 1u) << Disassemble(input);
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output));
EXPECT_EQ(output, input) << Disassemble(output);
EXPECT_TRUE(ShaderCompiler::ModuleDeclaresFloat64(output));
}
TEST_F(DemoteFloat64Test, ModuleDeclaresFloat64AnswersBothWays) {
const Vector<Uint32> wide = CompileToSpirv(GL_VERTEX_SHADER, kWideVertexSource);
ASSERT_FALSE(wide.empty());
EXPECT_TRUE(ShaderCompiler::ModuleDeclaresFloat64(wide));
Vector<Uint32> demoted;
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(wide, demoted));
EXPECT_FALSE(ShaderCompiler::ModuleDeclaresFloat64(demoted));
EXPECT_FALSE(ShaderCompiler::ModuleDeclaresFloat64({}));
}
TEST_F(DemoteFloat64Test, TheSharedChainDemotesToo) {
// Production never calls the pass on its own: it reaches it through the one chain every
// module goes through at link, on both backends.
const Vector<Uint32> input = CompileToSpirv(GL_VERTEX_SHADER, kWideVertexSource);
ASSERT_FALSE(input.empty());
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(input, output));
EXPECT_FALSE(ShaderCompiler::ModuleDeclaresFloat64(output)) << Disassemble(output);
}
// The payoff on the Espryt path: SPIRV-Cross throws "FP64 not supported in ES profile" for every
// one of these before demotion, so the program simply could not be transpiled at all.
class DemoteFloat64EsslTest : public DemoteFloat64Test, public ::testing::WithParamInterface<const char*> {};
INSTANTIATE_TEST_SUITE_P(
Shapes, DemoteFloat64EsslTest,
::testing::Values(
// A double that never reaches an interface: locals and literals only.
R"(#version 460 core
layout(location = 0) out float vOut;
void main() {
double s = 0.5lf;
for (int i = 0; i < 3; ++i) s = s * 1.5lf + 0.25lf;
vOut = float(s);
gl_Position = vec4(float(s));
}
)",
// A default-block double uniform: the glUniform*d path, and the block MobileGL lays out
// itself.
R"(#version 460 core
layout(location = 0) uniform double uScale;
layout(location = 1) uniform dvec3 uOffset;
layout(location = 2) uniform dmat4 uTransform;
layout(location = 0) out float vOut;
void main() {
dvec3 p = uOffset * uScale + dvec3(uTransform[1].xyz);
vOut = float(p.x + p.y + p.z);
gl_Position = vec4(float(p.x));
}
)",
// An application-declared std140 block whose members are 64-bit.
R"(#version 460 core
layout(std140, binding = 0) uniform Blk {
float a;
double d;
dvec2 v2;
dvec3 v3;
dvec4 v4;
dmat4 m4;
double arr[3];
float z;
};
layout(location = 0) out float vOut;
void main() {
double s = d + v2.x + v3.y + v4.z + m4[2].w + arr[1] + a + z;
vOut = float(s);
gl_Position = vec4(float(s));
}
)",
// A 64-bit vertex input, which is what glVertexAttribLFormat feeds.
R"(#version 460 core
layout(location = 0) in dvec3 inPos;
layout(location = 2) in double inWeight;
layout(location = 0) out float vOut;
void main() {
vOut = float(inPos.x + inPos.y + inPos.z + inWeight);
gl_Position = vec4(vOut);
}
)",
// An std430 storage block, whose double members pack differently again.
R"(#version 460 core
layout(std430, binding = 0) buffer Ssbo {
double head;
dvec4 wide;
double tail[4];
};
layout(location = 0) out float vOut;
void main() {
double s = head + wide.w + tail[3];
vOut = float(s);
gl_Position = vec4(float(s));
}
)"));
TEST_P(DemoteFloat64EsslTest, TheDemotedModuleCanBeEmittedAsEssl) {
using namespace MG_Util::ShaderTranspiler;
const Vector<Uint32> input = CompileToSpirv(GL_VERTEX_SHADER, GetParam());
ASSERT_FALSE(input.empty());
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(input, output));
SpvcSession session(output, SessionUsageBit::Transpile);
spvc_compiler_options options;
ASSERT_EQ(session.CreateOptions(&options), SPVC_SUCCESS);
spvc_compiler_options_set_uint(options, SPVC_COMPILER_OPTION_GLSL_VERSION, 320);
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_ES, SPVC_TRUE);
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_VULKAN_SEMANTICS, SPVC_FALSE);
ASSERT_EQ(session.SetOptions(options), SPVC_SUCCESS);
auto essl = ShaderCompiler::DecompileShader(session);
ASSERT_TRUE(essl) << essl.error().log;
EXPECT_NE(essl->find("#version 320 es"), String::npos) << *essl;
// ESSL has no 64-bit float spelling at all, so any of these in the output is SPIRV-Cross
// having emitted something no ES driver will compile.
EXPECT_EQ(essl->find("double"), String::npos) << *essl;
EXPECT_EQ(essl->find("dvec"), String::npos) << *essl;
EXPECT_EQ(essl->find("dmat"), String::npos) << *essl;
}
TEST_F(DemoteFloat64Test, RejectsGarbageInput) {
const Vector<Uint32> notSpirv{0xdeadbeefu, 0u, 0u, 0u, 0u};
Vector<Uint32> output;
EXPECT_FALSE(ShaderCompiler::DemoteFloat64ToFloat32(notSpirv, output));
}
// EliminateFloatEqualsZeroPass turns a comparison against 0.0 into an epsilon test, a
// workaround for drivers whose exact float compare misbehaves. Deciding WHICH constants are
// zero used to read every float constant as though it were 32 bits wide, and on a 64-bit
// constant that reads the LOW half of the mantissa - which is zero for 1.0lf, 2.0lf, 0.5lf and
// every other round double a shader is likely to spell. Each of those was mistaken for 0.0, so
// a comparison against 1.0lf became an epsilon test against ZERO, and came out true for a
// uniform holding exactly 1.0. That is the whole of KHR-GL43.compute_shader.fp64-case2.
//
// Asserted on the optimized module rather than through a driver, because that is where the
// rewrite happens and its fingerprint there is unambiguous: the epsilon form introduces a
// GLSL.std.450 FAbs, and nothing else in these shaders would.
namespace {
Bool RewritesToAnEpsilonTest(const String& source) {
const Vector<Uint32> input = CompileToSpirv(GL_COMPUTE_SHADER, source);
EXPECT_FALSE(input.empty());
if (input.empty()) return false;
Vector<Uint32> output;
EXPECT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(input, output));
return Disassemble(output).find("FAbs") != String::npos;
}
String CompareAgainst(const String& type, const String& literal) {
return "#version 430 core\n"
"layout(local_size_x = 1) in;\n"
"buffer Result { int g_result; };\n"
"uniform " + type + " g_0;\n"
"void main() {\n"
" g_result = 0;\n"
" if (g_0 != " + literal + ") g_result = 1;\n"
"}\n";
}
} // namespace
TEST_F(DemoteFloat64Test, AComparisonAgainstANonZeroDoubleIsLeftAlone) {
EXPECT_FALSE(RewritesToAnEpsilonTest(CompareAgainst("double", "1.0LF")))
<< "a double compared against 1.0lf was rewritten into an epsilon test against zero";
}
TEST_F(DemoteFloat64Test, AComparisonAgainstZeroIsStillRewritten) {
EXPECT_TRUE(RewritesToAnEpsilonTest(CompareAgainst("double", "0.0LF")))
<< "the rewrite must still fire for a genuine comparison against zero";
}
TEST_F(DemoteFloat64Test, TheThirtyTwoBitBehaviourIsUnchanged) {
EXPECT_FALSE(RewritesToAnEpsilonTest(CompareAgainst("float", "1.0")))
<< "a float compared against 1.0 must not be rewritten";
EXPECT_TRUE(RewritesToAnEpsilonTest(CompareAgainst("float", "0.0")))
<< "the 32-bit behaviour this pass shipped with must be preserved exactly";
}
@@ -0,0 +1,226 @@
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/FlattenXfbInterfaceBlocksTest.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include <gtest/gtest.h>
#include <set>
#include <string>
#include <vector>
#include "Includes.h"
#include "Init.h"
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <MG_Util/ShaderTranspiler/SpvcSession.h>
#include <MG_Util/ShaderTranspiler/Types.h>
#include <spirv-tools/libspirv.hpp>
using namespace MobileGL;
using MobileGL::MG_Util::ShaderTranspiler::SessionUsageBit;
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
using MobileGL::MG_Util::ShaderTranspiler::SpvcSession;
namespace {
Vector<Uint32> CompileToSpirv(GLenum stage, const String& source) {
using namespace MG_Util::ShaderTranspiler;
ShaderAttrib shaderAttrib{.shaderType = stage, .sourceStr = source};
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
EXPECT_TRUE(shaderResult) << (shaderResult ? String{} : shaderResult.error().log);
if (!shaderResult) return {};
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
EXPECT_TRUE(programResult) << (programResult ? String{} : programResult.error().log);
if (!programResult) return {};
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {stage}, .program = *programResult.value()};
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
EXPECT_TRUE(binaryResult) << (binaryResult ? String{} : binaryResult.error().log);
if (!binaryResult || binaryResult->empty()) return {};
return binaryResult->front();
}
String Disassemble(const Vector<Uint32>& spirv) {
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
String text;
tools.Disassemble(spirv, &text);
return text;
}
String Transpile(const Vector<Uint32>& spirv) {
SpvcSession session(spirv, SessionUsageBit::Transpile);
auto essl = ShaderCompiler::DecompileShader(session);
EXPECT_TRUE(essl) << (essl ? String{} : essl.error().log);
return essl ? essl.value() : String{};
}
// The KHR-GL43.vertex_attrib_binding.basic-input capture program's output side, verbatim:
// a 16-element vec4 array inside a named output block, which is what the capture list
// addresses member by member ("StageData.attrib[0]" ... "StageData.attrib[15]").
const char* kCaptureVertexSource = R"(#version 430 core
layout(location = 0) in vec4 vs_in_attrib;
out StageData {
vec4 attrib[16];
} vs_out;
void main() {
for (int i = 0; i < 16; ++i) {
vs_out.attrib[i] = vs_in_attrib;
}
}
)";
// Mixed member widths, so a member that claims the wrong number of locations moves every
// member after it.
// 440, because a location on the BLOCK is ARB_enhanced_layouts.
const char* kMixedBlockVertexSource = R"(#version 440 core
layout(location = 0) in vec4 vs_in_attrib;
layout(location = 0) out StageData {
vec4 first;
vec2 second;
mat4 third;
vec4 fourth;
} vs_out;
void main() {
vs_out.first = vs_in_attrib;
vs_out.second = vs_in_attrib.xy;
vs_out.third = mat4(vs_in_attrib.x);
vs_out.fourth = vs_in_attrib;
}
)";
} // namespace
class FlattenXfbInterfaceBlocksTest : public ::testing::Test {
protected:
void SetUp() override {
MobileGL::Initialize();
ShaderCompiler::SetSpirvValidationEnabled(true);
m_validationFailuresAtStart = ShaderCompiler::SpirvValidationFailureCount();
}
void TearDown() override {
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), m_validationFailuresAtStart)
<< "the flattened module did not survive spirv-val";
}
Uint64 m_validationFailuresAtStart = 0;
};
TEST_F(FlattenXfbInterfaceBlocksTest, FlattensACapturedBlockIntoOneVariablePerMember) {
const Vector<Uint32> input = CompileToSpirv(GL_VERTEX_SHADER, kCaptureVertexSource);
ASSERT_FALSE(input.empty());
std::set<String> flattened;
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(input, {"StageData"}, flattened, output));
ASSERT_FALSE(output.empty());
EXPECT_EQ(flattened, (std::set<String>{"StageData"}));
const String dis = Disassemble(output);
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
ASSERT_TRUE(tools.Validate(output)) << dis;
EXPECT_NE(dis.find("StageData_attrib"), String::npos) << dis;
// The block itself must have stopped being an interface variable, or the driver would see
// both spellings of the same data.
EXPECT_NE(dis.find("Private"), String::npos) << dis;
}
// The declaration is the point of the whole exercise: the emitted ESSL has to declare a plain
// output ARRAY, not an interface block, because that is the shape the Adreno driver can capture.
// SPIR-V validation does NOT catch the difference - leaving the struct's Block decoration on the
// demoted shadow produced a module that validated and emitted `StageData vs_out;` next to a block
// declaration the driver rejected with a bare "'vs_out' : syntax error".
TEST_F(FlattenXfbInterfaceBlocksTest, TheEmittedDeclarationIsAPlainArrayNotABlock) {
const Vector<Uint32> input = CompileToSpirv(GL_VERTEX_SHADER, kCaptureVertexSource);
ASSERT_FALSE(input.empty());
// Negative control: untouched, the block is emitted AS a block.
const String before = Transpile(input);
EXPECT_NE(before.find("out StageData"), String::npos) << before;
EXPECT_EQ(before.find("StageData_attrib"), String::npos) << before;
std::set<String> flattened;
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(input, {"StageData"}, flattened, output));
const String after = Transpile(output);
EXPECT_NE(after.find("StageData_attrib[16]"), String::npos) << after;
EXPECT_EQ(after.find("out StageData"), String::npos)
<< "the block must not still be declared as an output block:\n"
<< after;
}
// GL 4.6 core 11.1.2.1: consecutive members take consecutive locations, and a member takes as
// many as its type needs. Getting a span wrong silently moves every member after it.
TEST_F(FlattenXfbInterfaceBlocksTest, GivesEachMemberItsOwnConsecutiveLocations) {
const Vector<Uint32> input = CompileToSpirv(GL_VERTEX_SHADER, kMixedBlockVertexSource);
ASSERT_FALSE(input.empty());
std::set<String> flattened;
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(input, {"StageData"}, flattened, output));
ASSERT_FALSE(output.empty());
const String dis = Disassemble(output);
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
ASSERT_TRUE(tools.Validate(output)) << dis;
EXPECT_NE(dis.find("OpDecorate %StageData_first Location 0"), String::npos) << dis;
EXPECT_NE(dis.find("OpDecorate %StageData_second Location 1"), String::npos) << dis;
EXPECT_NE(dis.find("OpDecorate %StageData_third Location 2"), String::npos) << dis;
// mat4 takes four, so the member after it starts at 2 + 4.
EXPECT_NE(dis.find("OpDecorate %StageData_fourth Location 6"), String::npos) << dis;
}
// Nothing captures this block, so nothing may touch it: a shader that merely HAS an output
// block must reach the driver exactly as it was.
TEST_F(FlattenXfbInterfaceBlocksTest, LeavesABlockNoCaptureNamesAlone) {
const Vector<Uint32> input = CompileToSpirv(GL_VERTEX_SHADER, kCaptureVertexSource);
ASSERT_FALSE(input.empty());
std::set<String> flattened;
Vector<Uint32> output;
ASSERT_TRUE(
ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(input, {"SomeOtherBlock"}, flattened, output));
EXPECT_TRUE(flattened.empty());
const String after = Transpile(output);
EXPECT_NE(after.find("out StageData"), String::npos) << after;
EXPECT_EQ(after.find("StageData_attrib"), String::npos) << after;
}
// An empty request must not even run the optimizer: every program without transform feedback
// takes this path on every build.
TEST_F(FlattenXfbInterfaceBlocksTest, DeclinesAnEmptyRequestWithoutRewriting) {
const Vector<Uint32> input = CompileToSpirv(GL_VERTEX_SHADER, kCaptureVertexSource);
ASSERT_FALSE(input.empty());
std::set<String> flattened;
Vector<Uint32> output;
EXPECT_FALSE(ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(input, {}, flattened, output));
EXPECT_TRUE(flattened.empty());
EXPECT_TRUE(output.empty());
}
// The capture list has to follow the declaration exactly, and only for blocks that were
// actually rewritten - a member of a block left alone keeps the application's spelling, and so
// does a name with no block prefix at all (gl_Position, a plain varying).
TEST_F(FlattenXfbInterfaceBlocksTest, RewritesOnlyTheCaptureNamesOfFlattenedBlocks) {
String rewritten;
EXPECT_TRUE(ShaderCompiler::RewriteXfbCaptureNameForFlattenedBlock("StageData.attrib[0]", {"StageData"},
rewritten));
EXPECT_EQ(rewritten, "StageData_attrib[0]");
EXPECT_TRUE(
ShaderCompiler::RewriteXfbCaptureNameForFlattenedBlock("StageData.attrib", {"StageData"}, rewritten));
EXPECT_EQ(rewritten, "StageData_attrib");
EXPECT_FALSE(
ShaderCompiler::RewriteXfbCaptureNameForFlattenedBlock("Other.member", {"StageData"}, rewritten));
EXPECT_FALSE(ShaderCompiler::RewriteXfbCaptureNameForFlattenedBlock("gl_Position", {"StageData"}, rewritten));
EXPECT_FALSE(ShaderCompiler::RewriteXfbCaptureNameForFlattenedBlock("vColor", {"StageData"}, rewritten));
EXPECT_FALSE(ShaderCompiler::RewriteXfbCaptureNameForFlattenedBlock(".leading", {"StageData"}, rewritten));
}
@@ -128,6 +128,134 @@ namespace {
DrainErrors();
}
// KHR-GL44.multi_bind.errors_bind_textures / .errors_bind_image_textures / .errors_bind_samplers.
// Both entry points were silent no-op stubs, so every row here answered GL_NO_ERROR.
// errors_bind_samplers is in the list because that case checks the invalid-name rule by calling
// glBindTextures with a sampler-name array - a name from the wrong namespace is simply not an
// existing texture.
TEST_F(NegativeApiErrorsTest, MultiBindTexturesRejectsBadRangesAndNames) {
GLint maxUnits = 0;
GetIntegerv(GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, &maxUnits);
ASSERT_GT(maxUnits, 0);
GLint maxImageUnits = 0;
GetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
GLuint texture = 0;
GenTextures(1, &texture);
BindTexture(GL_TEXTURE_2D, texture);
TexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 4, 4);
// Reserved by glGenTextures but never bound: not an object yet, so the multi-bind entry
// points must refuse it instead of creating it the way glBindTexture would.
GLuint reservedOnly = 0;
GenTextures(1, &reservedOnly);
ASSERT_NE(reservedOnly, 0u);
ASSERT_EQ(IsTexture(reservedOnly), GL_FALSE);
DrainErrors();
const GLuint good[1] = {texture};
const GLuint mixed[2] = {texture, reservedOnly};
std::vector<Row> rows = {
{"glBindTextures with negative count", [&] { BindTextures(0, -1, good); }, GL_INVALID_VALUE},
{"glBindTextures with first + count past the last unit",
[&] { BindTextures(static_cast<GLuint>(maxUnits), 1, good); }, GL_INVALID_OPERATION},
{"glBindTextures with a reserved-but-uncreated name", [&] { BindTextures(0, 2, mixed); },
GL_INVALID_OPERATION},
{"glBindImageTextures with negative count", [&] { BindImageTextures(0, -1, good); }, GL_INVALID_VALUE},
};
if (maxImageUnits > 0) {
rows.push_back({"glBindImageTextures with first + count past the last image unit",
[&] { BindImageTextures(static_cast<GLuint>(maxImageUnits), 1, good); },
GL_INVALID_OPERATION});
rows.push_back({"glBindImageTextures with a reserved-but-uncreated name",
[&] { BindImageTextures(0, 2, mixed); }, GL_INVALID_OPERATION});
}
RunRows(rows);
// The loop semantics again: the good element at index 0 binds, the bad one does not.
GLint bound = -1;
GetIntegeri_v(GL_TEXTURE_BINDING_2D, 0, &bound);
EXPECT_EQ(static_cast<GLuint>(bound), texture) << "a rejected element must not take the valid ones with it";
GetIntegeri_v(GL_TEXTURE_BINDING_2D, 1, &bound);
EXPECT_EQ(bound, 0) << "the rejected element must not have bound anything";
DrainErrors();
}
// KHR-GL44.multi_bind.functional_bind_textures / .functional_bind_image_textures: the binding
// has to land on the texture's OWN target - glBindTextures takes no target parameter - and
// element zero has to unbind every target of its unit.
TEST_F(NegativeApiErrorsTest, MultiBindTexturesBindsToTheTexturesOwnTarget) {
GLuint textures[2] = {0, 0};
GenTextures(2, textures);
BindTexture(GL_TEXTURE_1D, textures[0]);
TexStorage1D(GL_TEXTURE_1D, 1, GL_RGBA8, 4);
BindTexture(GL_TEXTURE_3D, textures[1]);
TexStorage3D(GL_TEXTURE_3D, 1, GL_RGBA8, 4, 4, 4);
// Leave the active unit's slots clean so only the multi-bind result is under test.
BindTexture(GL_TEXTURE_1D, 0);
BindTexture(GL_TEXTURE_3D, 0);
DrainErrors();
BindTextures(0, 2, textures);
EXPECT_EQ(GetError(), GL_NO_ERROR);
GLint bound = -1;
GetIntegeri_v(GL_TEXTURE_BINDING_1D, 0, &bound);
EXPECT_EQ(static_cast<GLuint>(bound), textures[0]) << "a 1D texture must land on the unit's 1D slot";
GetIntegeri_v(GL_TEXTURE_BINDING_3D, 0, &bound);
EXPECT_EQ(bound, 0) << "no other target of the unit may be touched";
GetIntegeri_v(GL_TEXTURE_BINDING_3D, 1, &bound);
EXPECT_EQ(static_cast<GLuint>(bound), textures[1]) << "a 3D texture must land on the unit's 3D slot";
// A zero element - and a NULL array - unbind EVERY target of the unit, not just one.
const GLuint zeros[1] = {0};
BindTextures(0, 1, zeros);
GetIntegeri_v(GL_TEXTURE_BINDING_1D, 0, &bound);
EXPECT_EQ(bound, 0);
BindTextures(1, 1, nullptr);
GetIntegeri_v(GL_TEXTURE_BINDING_3D, 1, &bound);
EXPECT_EQ(bound, 0) << "a NULL <textures> unbinds the range";
EXPECT_EQ(GetError(), GL_NO_ERROR);
GLint maxImageUnits = 0;
GetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
if (maxImageUnits > 0) {
// ARB_multi_bind fixes every glBindImageTexture parameter but the unit and the name:
// level 0, layered, layer 0, READ_WRITE, and the texture's own internal format.
BindImageTextures(0, 1, &textures[1]);
EXPECT_EQ(GetError(), GL_NO_ERROR);
GetIntegeri_v(GL_IMAGE_BINDING_NAME, 0, &bound);
EXPECT_EQ(static_cast<GLuint>(bound), textures[1]);
GetIntegeri_v(GL_IMAGE_BINDING_LEVEL, 0, &bound);
EXPECT_EQ(bound, 0);
GetIntegeri_v(GL_IMAGE_BINDING_LAYERED, 0, &bound);
EXPECT_EQ(bound, GL_TRUE);
GetIntegeri_v(GL_IMAGE_BINDING_ACCESS, 0, &bound);
EXPECT_EQ(bound, GL_READ_WRITE);
GetIntegeri_v(GL_IMAGE_BINDING_FORMAT, 0, &bound);
EXPECT_EQ(bound, GL_RGBA8);
BindImageTextures(0, 1, nullptr);
GetIntegeri_v(GL_IMAGE_BINDING_NAME, 0, &bound);
EXPECT_EQ(bound, 0) << "a NULL <textures> resets the image unit";
}
// Through the EXPORTED entry points, not just the GLImpl functions: both of these were
// declared with the stub macro, so a working implementation that is never wired into
// Definitions.cpp still answers GL_NO_ERROR and binds nothing.
::glBindTextures(0, 1, &textures[0]);
GetIntegeri_v(GL_TEXTURE_BINDING_1D, 0, &bound);
EXPECT_EQ(static_cast<GLuint>(bound), textures[0]) << "glBindTextures is still exported as a no-op stub";
if (maxImageUnits > 0) {
::glBindImageTextures(0, 1, &textures[1]);
GetIntegeri_v(GL_IMAGE_BINDING_NAME, 0, &bound);
EXPECT_EQ(static_cast<GLuint>(bound), textures[1])
<< "glBindImageTextures is still exported as a no-op stub";
}
DrainErrors();
}
TEST_F(NegativeApiErrorsTest, BufferRangeOffsetAlignmentAppliesToTheBindingPoint) {
GLint ssboAlignment = 0;
GetIntegerv(GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT, &ssboAlignment);
@@ -195,6 +323,47 @@ namespace {
static_cast<void>(parameterBuffer);
}
// A transform feedback name has two different truths and glDrawTransformFeedback used to ask
// for the wrong one. glGenTransformFeedbacks only RESERVES a name; the first
// glBindTransformFeedback is what creates the object (GL 4.6 core 13.2.1), and
// glIsTransformFeedback reports exactly that distinction. glDrawTransformFeedback's
// "id is not the name of a transform feedback object" INVALID_VALUE has to agree with
// glIsTransformFeedback, or a caller that picks an unused name the way
// KHR-GL4x.transform_feedback.api_errors_test does - increment until glIsTransformFeedback
// says false - gets a name the draw then accepts, and the draw falls through to a different
// error entirely (INVALID_OPERATION, "glEndTransformFeedback has never been called").
//
// The draw path itself needs a backend and a linked program before it reaches the name, which
// this GPU-free suite has neither of, so what is pinned here is the predicate pair the fix
// turns on: the two must not collapse back into one.
TEST_F(NegativeApiErrorsTest, ReservedTransformFeedbackNameIsNotYetAnObject) {
GLuint name = 0;
GenTransformFeedbacks(1, &name);
ASSERT_NE(name, 0u);
DrainErrors();
// Reserved, so it is a legal argument to glBindTransformFeedback...
EXPECT_TRUE(MG_State::pGLContext->ValidateTransformFeedbackName(name));
// ...but not an object yet, which is what a draw must key off.
EXPECT_FALSE(MG_State::pGLContext->IsTransformFeedbackObject(name));
EXPECT_EQ(IsTransformFeedback(name), GL_FALSE);
BindTransformFeedback(GL_TRANSFORM_FEEDBACK, name);
EXPECT_EQ(GetError(), GL_NO_ERROR);
EXPECT_TRUE(MG_State::pGLContext->ValidateTransformFeedbackName(name));
EXPECT_TRUE(MG_State::pGLContext->IsTransformFeedbackObject(name));
EXPECT_EQ(IsTransformFeedback(name), GL_TRUE);
// The default object is never "an object" by this predicate and is always drawable, so
// the draw path has to special-case it rather than reuse the answer directly.
EXPECT_FALSE(MG_State::pGLContext->IsTransformFeedbackObject(0));
EXPECT_TRUE(MG_State::pGLContext->ValidateTransformFeedbackName(0));
BindTransformFeedback(GL_TRANSFORM_FEEDBACK, 0);
DrainErrors();
}
TEST_F(NegativeApiErrorsTest, TexStorage3DRejectsCompressedFormatsOnTexture3D) {
GLuint texture = 0;
GenTextures(1, &texture);
+745
View File
@@ -16,6 +16,7 @@
#include <MG_Backend/BackendObjects.h>
#include <MG_Backend/DirectGLES/Managers.h>
#include <MG_Backend/DirectGLES/Utils.h>
#include <MG_Impl/GLImpl/Buffer/GL_Buffer.h>
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
#include <MG_Impl/GLImpl/RenderState/GL_RenderState.h>
@@ -1572,6 +1573,509 @@ TEST_F(TextureTest, CompressedInternalFormatsResolveToTheirUncompressedStorage)
}
}
// Resolving to uncompressed storage is a storage decision, not a licence to answer the level
// queries as if the application had asked for an uncompressed format. GL 4.6 core 8.5 lets the
// implementation choose for the GENERIC formats (GL_COMPRESSED_RED and friends), but a SPECIFIC
// one commits the level: GL_TEXTURE_COMPRESSED is true, GL_TEXTURE_INTERNAL_FORMAT is the token
// that was passed, and GL_TEXTURE_COMPRESSED_IMAGE_SIZE answers instead of erroring - which is
// exactly the three-query sequence KHR-GL44.buffer_storage.map_persistent_texture opens with to
// size the image it then uploads through glCompressedTexSubImage2D.
TEST_F(TextureTest, ASpecificCompressedInternalFormatTagsTheLevelCompressed) {
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_COMPRESSED_RED_RGTC1, 8, 8, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
GLint compressed = GL_FALSE;
MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_COMPRESSED, &compressed);
EXPECT_EQ(compressed, GL_TRUE);
GLint internalFormat = 0;
MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_INTERNAL_FORMAT, &internalFormat);
EXPECT_EQ(internalFormat, static_cast<GLint>(GL_COMPRESSED_RED_RGTC1));
// 8x8 in 4x4 blocks of 8 bytes each.
GLint imageSize = 0;
MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_COMPRESSED_IMAGE_SIZE, &imageSize);
EXPECT_EQ(imageSize, 32);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// The texel shadow behind the tag still carries the uncompressed storage the format resolves
// to - which is what lets the level sample, and what every size computation downstream
// divides by.
const auto textureObject = MG_State::pGLContext->GetTextureObject(texture);
ASSERT_NE(textureObject, nullptr);
EXPECT_EQ(textureObject->GetFormat(), TextureInternalFormat::R8);
}
// The negative control for the case above, and the reason it cannot simply tag every
// GL_COMPRESSED_* token: for a generic format the implementation's choice IS the answer, and
// MobileGL chooses uncompressed - so the level is not compressed and the size query is the
// INVALID_OPERATION GL 4.6 core 8.11 prescribes for an uncompressed image.
TEST_F(TextureTest, AGenericCompressedInternalFormatLeavesTheLevelUncompressed) {
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_COMPRESSED_RED, 8, 8, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
GLint compressed = GL_TRUE;
MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_COMPRESSED, &compressed);
EXPECT_EQ(compressed, GL_FALSE);
GLint internalFormat = 0;
MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_INTERNAL_FORMAT, &internalFormat);
EXPECT_EQ(internalFormat, static_cast<GLint>(GL_R8));
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
GLint imageSize = 0;
MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_COMPRESSED_IMAGE_SIZE, &imageSize);
ExpectSingleGlError(GL_INVALID_OPERATION);
}
// A plain glTexImage2D over a level that was tagged compressed has to un-tag it, the same way it
// does for a level a glCompressedTexImage2D shadowed - otherwise the size query would keep
// answering for an image that no longer exists.
TEST_F(TextureTest, AnUncompressedRespecificationClearsTheCompressedTag) {
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_COMPRESSED_RED_RGTC1, 8, 8, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr);
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_R8, 8, 8, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
GLint compressed = GL_TRUE;
MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_COMPRESSED, &compressed);
EXPECT_EQ(compressed, GL_FALSE);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
namespace {
// A 16x16 RGBA8 texture with exactly `levelCount` levels, defined the way
// KHR-GL43.copy_image.non_existent_mipmap defines its textures - glTexImage2D per
// level, NOT glTexStorage2D, because an immutable allocation defines the whole chain
// up front and so cannot express "level 1 does not exist".
GLuint MakeCopyImageTexture(int levelCount) {
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
for (int level = 0; level < levelCount; ++level) {
const GLsizei extent = 16 >> level;
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, level, GL_RGBA8, extent, extent, 0, GL_RGBA, GL_UNSIGNED_BYTE,
nullptr);
}
return texture;
}
} // namespace
// KHR-GL43.copy_image.non_existent_mipmap. Level 1 of a texture that has only level 0 is
// not a level: GL 4.6 core 18.3.2 asks for GL_INVALID_VALUE. Until this check existed the
// level travelled all the way into the backends, and DirectVulkan built a VkImageCopy
// naming mip 1 of a VkImage created with one mip - which Adreno answered with a SIGSEGV
// inside vkCmdCopyImage, killing the glcts process in the middle of a negative test.
TEST_F(TextureTest, CopyImageSubDataRejectsALevelTheTextureDoesNotHave) {
const GLuint src = MakeCopyImageTexture(1);
const GLuint dst = MakeCopyImageTexture(1);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::CopyImageSubData(src, GL_TEXTURE_2D, 1, 0, 0, 0, dst, GL_TEXTURE_2D, 0, 0, 0, 0, 1, 1, 1);
ExpectSingleGlError(GL_INVALID_VALUE);
MG_Impl::GLImpl::CopyImageSubData(src, GL_TEXTURE_2D, 0, 0, 0, 0, dst, GL_TEXTURE_2D, 1, 0, 0, 0, 1, 1, 1);
ExpectSingleGlError(GL_INVALID_VALUE);
MG_Impl::GLImpl::CopyImageSubData(src, GL_TEXTURE_2D, 1, 0, 0, 0, dst, GL_TEXTURE_2D, 1, 0, 0, 0, 1, 1, 1);
ExpectSingleGlError(GL_INVALID_VALUE);
}
// The negative control, and the reason the pair below asks for a zero-sized copy: a
// validator that answered GL_INVALID_VALUE to every non-zero level would satisfy the test
// above. The two calls here are IDENTICAL except for how many levels the textures have,
// and a zero extent makes the validator decline the copy without an error just after the
// level check - so the level count is the only thing either assertion can be reading, and
// no backend (there is none in this binary) is ever reached.
TEST_F(TextureTest, CopyImageSubDataAcceptsALevelTheTextureDoesHave) {
const GLuint oneLevelSrc = MakeCopyImageTexture(1);
const GLuint oneLevelDst = MakeCopyImageTexture(1);
const GLuint twoLevelSrc = MakeCopyImageTexture(2);
const GLuint twoLevelDst = MakeCopyImageTexture(2);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::CopyImageSubData(oneLevelSrc, GL_TEXTURE_2D, 1, 0, 0, 0, oneLevelDst, GL_TEXTURE_2D, 1, 0, 0, 0,
0, 0, 0);
ExpectSingleGlError(GL_INVALID_VALUE);
MG_Impl::GLImpl::CopyImageSubData(twoLevelSrc, GL_TEXTURE_2D, 1, 0, 0, 0, twoLevelDst, GL_TEXTURE_2D, 1, 0, 0, 0,
0, 0, 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "level 1 of a two-level texture is a level";
// And the boundary from the other side: two levels means 0 and 1, not 2.
MG_Impl::GLImpl::CopyImageSubData(twoLevelSrc, GL_TEXTURE_2D, 2, 0, 0, 0, twoLevelDst, GL_TEXTURE_2D, 0, 0, 0, 0,
0, 0, 0);
ExpectSingleGlError(GL_INVALID_VALUE);
}
// A texture that has never been given an image is a different fault from a level out of
// range, and the spec spells it differently: an incomplete object named by a copy is
// GL_INVALID_OPERATION. Worth pinning because the natural implementation of the check
// above - level >= levelCount - reports INVALID_VALUE for level 0 of a texture whose level
// count is zero, which is the wrong answer to the wrong question.
//
// BOTH textures are imageless on purpose, and that is the whole point rather than symmetry
// for its own sake. With one imageless and one RGBA8 texture the format comparison further
// down already rejected the call, so the case proved nothing about this check. With both
// imageless the formats are Unknown == Unknown, they MATCH, and every validator downstream
// waves the call through - which is how the second crash in this entry point was found: the
// call reached DirectVulkan, SyncTextureAndGetDescriptor returned nothing for a texture with
// no image, and the release build (where the guarding MOBILEGL_ASSERT expands to nothing)
// dereferenced it. Reproduced deterministically on lavapipe by
// KHR-GL43.copy_image.functional_src_target_texture_2d_array_..._dst_format_rgb9_e5.
TEST_F(TextureTest, CopyImageSubDataRejectsTwoTexturesWithNoImageAtAll) {
GLuint firstEmpty = 0;
GLuint secondEmpty = 0;
MG_Impl::GLImpl::GenTextures(1, &firstEmpty);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, firstEmpty);
MG_Impl::GLImpl::GenTextures(1, &secondEmpty);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, secondEmpty);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::CopyImageSubData(firstEmpty, GL_TEXTURE_2D, 0, 0, 0, 0, secondEmpty, GL_TEXTURE_2D, 0, 0, 0, 0,
1, 1, 1);
ExpectSingleGlError(GL_INVALID_OPERATION);
}
// GL_DEPTH_STENCIL_TEXTURE_MODE used to be a pure frontend shadow: stored, answered by
// glGetTexParameter, and never shown to a backend. Sampling therefore always read the depth
// aspect however the mode was set, which is the whole of
// KHR-GL3x.packed_depth_stencil.stencil_texturing. Both backends pick the aspect up through the
// texture-params version - DirectGLES re-emits glTexParameteri when it moves, DirectVulkan
// rebuilds the sampled image view - so the version bump is the load-bearing part, and a
// no-op write must not spend one (every bump costs DirectVulkan a view recreation).
TEST_F(TextureTest, DepthStencilTextureModeIsBackendVisibleThroughTheParamsVersion) {
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
MG_Impl::GLImpl::TexStorage2D(GL_TEXTURE_2D, 1, GL_DEPTH24_STENCIL8, 8, 8);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
const auto textureObject = MG_State::pGLContext->GetTextureObject(texture);
ASSERT_NE(textureObject, nullptr);
EXPECT_EQ(textureObject->GetDepthStencilTextureMode(), static_cast<GLenum>(GL_DEPTH_COMPONENT));
const Uint16 initialVersion = textureObject->GetTextureParamsVersion();
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_STENCIL_INDEX);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
EXPECT_EQ(textureObject->GetDepthStencilTextureMode(), static_cast<GLenum>(GL_STENCIL_INDEX));
EXPECT_NE(textureObject->GetTextureParamsVersion(), initialVersion);
// Re-writing the value already in force is not a change and must not invalidate anything.
const Uint16 settledVersion = textureObject->GetTextureParamsVersion();
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_STENCIL_INDEX);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
EXPECT_EQ(textureObject->GetTextureParamsVersion(), settledVersion);
// ...and going back to the depth aspect is a change again.
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_DEPTH_COMPONENT);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
EXPECT_EQ(textureObject->GetDepthStencilTextureMode(), static_cast<GLenum>(GL_DEPTH_COMPONENT));
EXPECT_NE(textureObject->GetTextureParamsVersion(), settledVersion);
}
namespace {
// 8x8 RGTC1: 2x2 blocks of 8 bytes, so the stored image is 32 bytes and one block row is 16.
constexpr GLsizei kRgtc1Size8x8 = 32;
GLuint MakeCompressedRgtc1Texture8x8() {
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
MG_Impl::GLImpl::CompressedTexImage2D(GL_TEXTURE_2D, 0, GL_COMPRESSED_RED_RGTC1, 8, 8, 0, kRgtc1Size8x8,
nullptr);
return texture;
}
} // namespace
// glCompressedTexSubImage2D was a stub that answered GL_INVALID_ENUM to every call. It replaces a
// block-aligned rectangle of the stored image, and the arithmetic that places the incoming blocks
// is what the partial write below pins: a full-width write would pass with the rows concatenated
// in either order.
TEST_F(TextureTest, CompressedTexSubImage2DReplacesTheStoredBlocks) {
const GLuint texture = MakeCompressedRgtc1Texture8x8();
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
Uint8 whole[kRgtc1Size8x8];
for (Int i = 0; i < kRgtc1Size8x8; ++i) whole[i] = static_cast<Uint8>(i + 1);
MG_Impl::GLImpl::CompressedTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 8, 8, GL_COMPRESSED_RED_RGTC1, kRgtc1Size8x8,
whole);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
Uint8 stored[kRgtc1Size8x8] = {};
MG_Impl::GLImpl::GetCompressedTexImage(GL_TEXTURE_2D, 0, stored);
EXPECT_EQ(std::memcmp(stored, whole, sizeof(whole)), 0);
// The right-hand block column only: one block wide, two block rows high. Its two blocks land
// at byte 8 and byte 24, not at bytes 0 and 8.
const Uint8 column[16] = {0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7,
0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7};
MG_Impl::GLImpl::CompressedTexSubImage2D(GL_TEXTURE_2D, 0, 4, 0, 4, 8, GL_COMPRESSED_RED_RGTC1,
static_cast<GLsizei>(sizeof(column)), column);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
Uint8 expected[kRgtc1Size8x8];
std::memcpy(expected, whole, sizeof(expected));
std::memcpy(expected + 8, column, 8);
std::memcpy(expected + 24, column + 8, 8);
std::memset(stored, 0, sizeof(stored));
MG_Impl::GLImpl::GetCompressedTexImage(GL_TEXTURE_2D, 0, stored);
EXPECT_EQ(std::memcmp(stored, expected, sizeof(expected)), 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// The Y axis of the placement, which the whole-image and single-column cases above cannot see: an
// implementation that dropped the first-block-row term, or that divided yoffset by the block WIDTH,
// passes every one of them. The region here starts at block row 1, so its two blocks belong at
// bytes 16 and 24 and nowhere else.
TEST_F(TextureTest, CompressedTexSubImage2DPlacesTheFirstBlockRow) {
const GLuint texture = MakeCompressedRgtc1Texture8x8();
Uint8 whole[kRgtc1Size8x8];
for (Int i = 0; i < kRgtc1Size8x8; ++i) whole[i] = static_cast<Uint8>(i + 1);
MG_Impl::GLImpl::CompressedTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 8, 8, GL_COMPRESSED_RED_RGTC1, kRgtc1Size8x8,
whole);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// The bottom block row only: 8 texels wide, 4 high, starting at y = 4.
const Uint8 bottom[16] = {0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7,
0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7};
MG_Impl::GLImpl::CompressedTexSubImage2D(GL_TEXTURE_2D, 0, 0, 4, 8, 4, GL_COMPRESSED_RED_RGTC1,
static_cast<GLsizei>(sizeof(bottom)), bottom);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
Uint8 expected[kRgtc1Size8x8];
std::memcpy(expected, whole, sizeof(expected));
std::memcpy(expected + 16, bottom, sizeof(bottom));
Uint8 stored[kRgtc1Size8x8] = {};
MG_Impl::GLImpl::GetCompressedTexImage(GL_TEXTURE_2D, 0, stored);
EXPECT_EQ(std::memcmp(stored, expected, sizeof(expected)), 0);
// And one block in the far corner, which needs both terms at once.
const Uint8 corner[8] = {0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7};
MG_Impl::GLImpl::CompressedTexSubImage2D(GL_TEXTURE_2D, 0, 4, 4, 4, 4, GL_COMPRESSED_RED_RGTC1,
static_cast<GLsizei>(sizeof(corner)), corner);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
std::memcpy(expected + 24, corner, sizeof(corner));
std::memset(stored, 0, sizeof(stored));
MG_Impl::GLImpl::GetCompressedTexImage(GL_TEXTURE_2D, 0, stored);
EXPECT_EQ(std::memcmp(stored, expected, sizeof(expected)), 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
(void)texture;
}
// A level whose size is neither square nor a multiple of the block size, at a level above the
// base, in a format with SIXTEEN bytes per block. Between them these pin the row stride (which a
// square level cannot distinguish from the column count), the rounding-up of a partial edge block,
// the run-to-the-edge exemption from the whole-blocks rule, and the block size actually coming from
// the format rather than from a constant.
TEST_F(TextureTest, CompressedTexSubImage2DHandlesPartialBlocksAndAMipLevel) {
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
// 6x10 BPTC: 2 block columns x 3 block rows of 16 bytes = 96, one block row = 32.
constexpr GLsizei kBptcSize6x10 = 96;
MG_Impl::GLImpl::CompressedTexImage2D(GL_TEXTURE_2D, 1, GL_COMPRESSED_RGBA_BPTC_UNORM, 6, 10, 0, kBptcSize6x10,
nullptr);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
GLint imageSize = 0;
MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_2D, 1, GL_TEXTURE_COMPRESSED_IMAGE_SIZE, &imageSize);
EXPECT_EQ(imageSize, kBptcSize6x10);
Uint8 whole[kBptcSize6x10];
for (Int i = 0; i < kBptcSize6x10; ++i) whole[i] = static_cast<Uint8>(i + 1);
MG_Impl::GLImpl::CompressedTexSubImage2D(GL_TEXTURE_2D, 1, 0, 0, 6, 10, GL_COMPRESSED_RGBA_BPTC_UNORM,
kBptcSize6x10, whole);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// The right-hand column (2 texels wide - a partial block that runs to the edge) of the middle
// block row: one block, at byte 32 + 16.
Uint8 patch[16];
for (Int i = 0; i < 16; ++i) patch[i] = static_cast<Uint8>(0xF0 + i);
MG_Impl::GLImpl::CompressedTexSubImage2D(GL_TEXTURE_2D, 1, 4, 4, 2, 4, GL_COMPRESSED_RGBA_BPTC_UNORM,
static_cast<GLsizei>(sizeof(patch)), patch);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
Uint8 expected[kBptcSize6x10];
std::memcpy(expected, whole, sizeof(expected));
std::memcpy(expected + 48, patch, sizeof(patch));
Uint8 stored[kBptcSize6x10] = {};
MG_Impl::GLImpl::GetCompressedTexImage(GL_TEXTURE_2D, 1, stored);
EXPECT_EQ(std::memcmp(stored, expected, sizeof(expected)), 0);
// The partial edge block is only exempt from the whole-blocks rule AT the edge: the same
// 2-texel width one block to the left is not.
MG_Impl::GLImpl::CompressedTexSubImage2D(GL_TEXTURE_2D, 1, 0, 4, 2, 4, GL_COMPRESSED_RGBA_BPTC_UNORM,
static_cast<GLsizei>(sizeof(patch)), patch);
ExpectSingleGlError(GL_INVALID_OPERATION);
}
// The same call sourcing its blocks from a buffer bound to GL_PIXEL_UNPACK_BUFFER, where `data` is
// an offset into that buffer rather than a client pointer - which is the form
// KHR-GL44.buffer_storage.map_persistent_texture uses for every one of its operations.
TEST_F(TextureTest, CompressedTexSubImage2DUnpacksFromAPixelUnpackBuffer) {
Uint8 source[256];
for (Int i = 0; i < 256; ++i) source[i] = static_cast<Uint8>(i);
GLuint buffer = 0;
MG_Impl::GLImpl::GenBuffers(1, &buffer);
MG_Impl::GLImpl::BindBuffer(GL_PIXEL_UNPACK_BUFFER, buffer);
MG_Impl::GLImpl::BufferData(GL_PIXEL_UNPACK_BUFFER, sizeof(source), source, GL_STATIC_DRAW);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
const GLuint texture = MakeCompressedRgtc1Texture8x8();
MG_Impl::GLImpl::CompressedTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 8, 8, GL_COMPRESSED_RED_RGTC1, kRgtc1Size8x8,
reinterpret_cast<const void*>(static_cast<SizeT>(64)));
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
Uint8 stored[kRgtc1Size8x8] = {};
MG_Impl::GLImpl::GetCompressedTexImage(GL_TEXTURE_2D, 0, stored);
EXPECT_EQ(std::memcmp(stored, source + 64, sizeof(stored)), 0);
// Reading past the end of the buffer is the unpack-buffer error, not a read out of bounds.
MG_Impl::GLImpl::CompressedTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 8, 8, GL_COMPRESSED_RED_RGTC1, kRgtc1Size8x8,
reinterpret_cast<const void*>(static_cast<SizeT>(sizeof(source) - 8)));
ExpectSingleGlError(GL_INVALID_OPERATION);
MG_Impl::GLImpl::BindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
(void)texture;
}
// ARB_buffer_storage's whole point: a PERSISTENTLY mapped buffer stays usable while the map is
// live, including as the source of a texture upload - which is what
// KHR-GL44.buffer_storage.map_persistent_texture checks. An ordinary map still disqualifies it.
// Both compressed entry points share one validator, so both are checked here.
TEST_F(TextureTest, CompressedUploadsAcceptAPersistentlyMappedUnpackBuffer) {
Uint8 source[256];
for (Int i = 0; i < 256; ++i) source[i] = static_cast<Uint8>(255 - i);
GLuint buffer = 0;
MG_Impl::GLImpl::GenBuffers(1, &buffer);
MG_Impl::GLImpl::BindBuffer(GL_PIXEL_UNPACK_BUFFER, buffer);
MG_Impl::GLImpl::BufferStorage(GL_PIXEL_UNPACK_BUFFER, sizeof(source), source,
GL_MAP_PERSISTENT_BIT | GL_MAP_READ_BIT | GL_MAP_WRITE_BIT);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
void* mapped = MG_Impl::GLImpl::MapBufferRange(GL_PIXEL_UNPACK_BUFFER, 0, sizeof(source),
GL_MAP_PERSISTENT_BIT | GL_MAP_READ_BIT | GL_MAP_WRITE_BIT);
ASSERT_NE(mapped, nullptr);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
// The image call takes offset 0 and the sub-image call offset 128, so the readback can only
// match if the SUB-IMAGE call ran: were it refused (or a no-op), the level would still hold
// the image call's bytes.
MG_Impl::GLImpl::CompressedTexImage2D(GL_TEXTURE_2D, 0, GL_COMPRESSED_RED_RGTC1, 8, 8, 0, kRgtc1Size8x8,
reinterpret_cast<const void*>(static_cast<SizeT>(0)));
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "glCompressedTexImage2D over a persistent map";
MG_Impl::GLImpl::CompressedTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 8, 8, GL_COMPRESSED_RED_RGTC1, kRgtc1Size8x8,
reinterpret_cast<const void*>(static_cast<SizeT>(128)));
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "glCompressedTexSubImage2D over a persistent map";
Uint8 stored[kRgtc1Size8x8] = {};
MG_Impl::GLImpl::GetCompressedTexImage(GL_TEXTURE_2D, 0, stored);
EXPECT_EQ(std::memcmp(stored, source + 128, sizeof(stored)), 0);
MG_Impl::GLImpl::UnmapBuffer(GL_PIXEL_UNPACK_BUFFER);
// The negative control: an ORDINARY map is still an error, so the check above is not just
// "the mapped test was dropped".
GLuint plainBuffer = 0;
MG_Impl::GLImpl::GenBuffers(1, &plainBuffer);
MG_Impl::GLImpl::BindBuffer(GL_PIXEL_UNPACK_BUFFER, plainBuffer);
MG_Impl::GLImpl::BufferData(GL_PIXEL_UNPACK_BUFFER, sizeof(source), source, GL_STATIC_DRAW);
ASSERT_NE(MG_Impl::GLImpl::MapBuffer(GL_PIXEL_UNPACK_BUFFER, GL_READ_ONLY), nullptr);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::CompressedTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 8, 8, GL_COMPRESSED_RED_RGTC1, kRgtc1Size8x8,
reinterpret_cast<const void*>(static_cast<SizeT>(0)));
ExpectSingleGlError(GL_INVALID_OPERATION);
MG_Impl::GLImpl::UnmapBuffer(GL_PIXEL_UNPACK_BUFFER);
MG_Impl::GLImpl::BindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
}
// glCompressedTextureSubImage2D was an exported no-op that raised no error at all, so an
// application could not tell the write had not happened. It must reach the NAMED texture and leave
// the binding it borrowed exactly as it found it.
TEST_F(TextureTest, CompressedTextureSubImage2DModifiesTheNamedTextureOnly) {
const GLuint bound = MakeCompressedRgtc1Texture8x8();
Uint8 boundImage[kRgtc1Size8x8];
for (Int i = 0; i < kRgtc1Size8x8; ++i) boundImage[i] = 0x11;
MG_Impl::GLImpl::CompressedTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 8, 8, GL_COMPRESSED_RED_RGTC1, kRgtc1Size8x8,
boundImage);
const GLuint named = MakeCompressedRgtc1Texture8x8();
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, bound); // `named` is NOT the bound texture
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
Uint8 namedImage[kRgtc1Size8x8];
for (Int i = 0; i < kRgtc1Size8x8; ++i) namedImage[i] = 0x22;
MG_Impl::GLImpl::CompressedTextureSubImage2D(named, 0, 0, 0, 8, 8, GL_COMPRESSED_RED_RGTC1, kRgtc1Size8x8,
namedImage);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// The borrowed binding is back, and it kept its own image.
Uint8 stored[kRgtc1Size8x8] = {};
MG_Impl::GLImpl::GetCompressedTexImage(GL_TEXTURE_2D, 0, stored);
EXPECT_EQ(std::memcmp(stored, boundImage, sizeof(stored)), 0);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, named);
std::memset(stored, 0, sizeof(stored));
MG_Impl::GLImpl::GetCompressedTexImage(GL_TEXTURE_2D, 0, stored);
EXPECT_EQ(std::memcmp(stored, namedImage, sizeof(stored)), 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(TextureTest, CompressedTexSubImage2DRejectsTheRegionsGLForbids) {
const GLuint texture = MakeCompressedRgtc1Texture8x8();
Uint8 blocks[kRgtc1Size8x8] = {};
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// A format that is not the one the image is stored in.
MG_Impl::GLImpl::CompressedTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 8, 8, GL_COMPRESSED_RG_RGTC2, 64, blocks);
ExpectSingleGlError(GL_INVALID_OPERATION);
// A start that is not on a block boundary.
MG_Impl::GLImpl::CompressedTexSubImage2D(GL_TEXTURE_2D, 0, 2, 0, 4, 8, GL_COMPRESSED_RED_RGTC1, 16, blocks);
ExpectSingleGlError(GL_INVALID_OPERATION);
// A width that is neither a whole number of blocks nor a run to the image's edge.
MG_Impl::GLImpl::CompressedTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 2, 8, GL_COMPRESSED_RED_RGTC1, 16, blocks);
ExpectSingleGlError(GL_INVALID_OPERATION);
// A region that runs off the image.
MG_Impl::GLImpl::CompressedTexSubImage2D(GL_TEXTURE_2D, 0, 4, 0, 8, 8, GL_COMPRESSED_RED_RGTC1, 32, blocks);
ExpectSingleGlError(GL_INVALID_VALUE);
// An imageSize that does not match the region.
MG_Impl::GLImpl::CompressedTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 8, 8, GL_COMPRESSED_RED_RGTC1, 16, blocks);
ExpectSingleGlError(GL_INVALID_VALUE);
// A format with no defined block layout here.
MG_Impl::GLImpl::CompressedTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 8, 8, GL_RGBA8, 32, blocks);
ExpectSingleGlError(GL_INVALID_ENUM);
// An uncompressed image has nothing for it to replace.
GLuint plain = 0;
MG_Impl::GLImpl::GenTextures(1, &plain);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, plain);
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_R8, 8, 8, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::CompressedTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 8, 8, GL_COMPRESSED_RED_RGTC1, kRgtc1Size8x8,
blocks);
ExpectSingleGlError(GL_INVALID_OPERATION);
(void)texture;
}
// RGTC compresses 4x4 blocks of a 2D image and has no 3D form, so glTexImage3D must reject it even
// though the same enum is accepted on a 2D target. The generic compressed formats carry no such
// restriction and stay legal in 3D.
@@ -2675,6 +3179,193 @@ TEST_F(TextureTest, DecodeShadowDataToWideRGBACoversComponentAndPackedLayouts) {
}
}
// ---- GL_RGB9_E5 raw-preserving transfer --------------------------------------------------------
// RGB9_E5 packs three 9-bit mantissas against one shared 5-bit exponent, so a value has several
// legal encodings (shift the exponent up, shift every mantissa down). The spec's encode algorithm
// (GL 4.6 8.5.2) always emits the canonical one, which makes decode-to-float / re-encode
// value-preserving but NOT bit-preserving. glTexImage followed by glGetTexImage has to hand the
// application its own bits back, so a client (format, type) whose word already IS the storage word
// must move verbatim. GL CTS KHR-GL43.copy_image caught the round trip turning the uploaded
// 0xf8fc0000 into 0xe7e00000 ("CopyImageSubData modified contents of source image") and a copied-in
// 0x60000000 into 0x00000000 ("CopyImageSubData stored invalid data in copied region").
namespace {
Uint32 RoundTripSharedExponentWord(Uint32 word) {
Float rgb[3];
MG_Util::DecodeSharedExponentRGB9E5(word, rgb);
return MG_Util::EncodeSharedExponentRGB9E5(rgb);
}
} // namespace
TEST(SharedExponentRGB9E5Test, EncodeReproducesCanonicalWordsExactly) {
// Canonical encodings - the ones the spec algorithm emits - must survive a decode/encode round
// trip untouched, or every conversion INTO RGB9_E5 would be off as well.
const Uint32 canonical[] = {
0x00000000u, // all zero
0x0FFFFFFFu, // exponent 1, every mantissa saturated (smallest normalized exponent in use)
0x000003FFu, // exponent 0: the denormal range, mantissas 511 / 1 / 0
0x81010100u, // (1.0, 0.5, 0.25)
0xE7E00000u, // (0, 0, 8064) - what the CTS round trip produced
0xFFFFFFFFu, // exponent 31 with saturated mantissas = the largest representable texel
};
for (const Uint32 word : canonical) {
EXPECT_EQ(RoundTripSharedExponentWord(word), word) << "word 0x" << std::hex << word;
// Encoding is idempotent: a second pass may not drift either.
EXPECT_EQ(RoundTripSharedExponentWord(RoundTripSharedExponentWord(word)), word);
}
}
TEST(SharedExponentRGB9E5Test, EncodeCanonicalizesRedundantWords) {
// The exact QPA signatures. Both pairs hold the same value, so the encoder is not wrong - which
// is why the fix has to be a raw path rather than an encoder change.
Float observed[3];
MG_Util::DecodeSharedExponentRGB9E5(0xF8FC0000u, observed);
Float canonical[3];
MG_Util::DecodeSharedExponentRGB9E5(0xE7E00000u, canonical);
EXPECT_EQ(observed[2], 8064.0f);
EXPECT_EQ(canonical[2], 8064.0f);
EXPECT_EQ(RoundTripSharedExponentWord(0xF8FC0000u), 0xE7E00000u);
// Exponent 12 with all-zero mantissas is still the value zero, and canonicalizes to the
// all-zero word.
EXPECT_EQ(RoundTripSharedExponentWord(0x60000000u), 0x00000000u);
// Mantissa 1 at exponent 1 renormalizes down into the denormal range.
EXPECT_EQ(RoundTripSharedExponentWord(0x08000001u), 0x00000002u);
}
TEST(SharedExponentRGB9E5Test, RawPackedPixelTransferCoversOnlyIdenticalLayouts) {
using MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer;
// The four pairs whose client word is bit-identical to the packed storage word.
EXPECT_TRUE(IsRawPackedPixelTransfer(TextureInternalFormat::RGB9E5, TextureInputFormat::RGB,
TexturePixelDataType::UnsignedInt5999Rev));
EXPECT_TRUE(IsRawPackedPixelTransfer(TextureInternalFormat::R11FG11FB10F, TextureInputFormat::RGB,
TexturePixelDataType::UnsignedInt101111Rev));
EXPECT_TRUE(IsRawPackedPixelTransfer(TextureInternalFormat::RGB10A2, TextureInputFormat::RGBA,
TexturePixelDataType::UnsignedInt2101010Rev));
EXPECT_TRUE(IsRawPackedPixelTransfer(TextureInternalFormat::RGB10A2UI, TextureInputFormat::RGBAInteger,
TexturePixelDataType::UnsignedInt2101010Rev));
// A different packed float layout of the same width is still a conversion.
EXPECT_FALSE(IsRawPackedPixelTransfer(TextureInternalFormat::RGB9E5, TextureInputFormat::RGB,
TexturePixelDataType::UnsignedInt101111Rev));
EXPECT_FALSE(IsRawPackedPixelTransfer(TextureInternalFormat::R11FG11FB10F, TextureInputFormat::RGB,
TexturePixelDataType::UnsignedInt5999Rev));
// So is a component client type, or the same word against a component internal format.
EXPECT_FALSE(IsRawPackedPixelTransfer(TextureInternalFormat::RGB9E5, TextureInputFormat::RGB,
TexturePixelDataType::Float));
EXPECT_FALSE(IsRawPackedPixelTransfer(TextureInternalFormat::RGB8, TextureInputFormat::RGB,
TexturePixelDataType::UnsignedInt5999Rev));
EXPECT_FALSE(IsRawPackedPixelTransfer(TextureInternalFormat::RGBA32F, TextureInputFormat::RGBA,
TexturePixelDataType::UnsignedInt2101010Rev));
// Integerness has to line up too: the normalized and integer 10/10/10/2 words are not the
// same client layout even though they are the same bit field.
EXPECT_FALSE(IsRawPackedPixelTransfer(TextureInternalFormat::RGB10A2, TextureInputFormat::RGBAInteger,
TexturePixelDataType::UnsignedInt2101010Rev));
EXPECT_FALSE(IsRawPackedPixelTransfer(TextureInternalFormat::RGB10A2UI, TextureInputFormat::RGBA,
TexturePixelDataType::UnsignedInt2101010Rev));
EXPECT_FALSE(IsRawPackedPixelTransfer(TextureInternalFormat::Unknown, TextureInputFormat::RGB,
TexturePixelDataType::UnsignedInt5999Rev));
}
TEST_F(TextureTest, TexImage2DRGB9E5KeepsNonCanonicalClientWords) {
// Upload direction: GL_RGB / GL_UNSIGNED_INT_5_9_9_9_REV into GL_RGB9_E5 stores the client
// words untouched, including the redundant encodings the CTS generates.
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
const Uint32 words[] = {0xF8FC0000u, 0x60000000u, 0x08000001u, 0x0FFFFFFFu};
MG_Impl::GLImpl::PixelStorei(GL_UNPACK_ALIGNMENT, 1);
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGB9_E5, 4, 1, 0, GL_RGB, GL_UNSIGNED_INT_5_9_9_9_REV, words);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
const auto* stored = GetBoundTexture2DLevelBytes(texture);
ASSERT_NE(stored, nullptr);
Uint32 readBack[4] = {};
std::memcpy(readBack, stored, sizeof(readBack));
for (Int i = 0; i < 4; ++i) {
EXPECT_EQ(readBack[i], words[i]) << "texel " << i;
}
MG_Impl::GLImpl::PixelStorei(GL_UNPACK_ALIGNMENT, 4);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0);
}
TEST_F(TextureTest, TexImage2DRGB9E5FromOtherPackedFloatTypeStillConverts) {
// Negative control for the raw path: a genuinely different client layout keeps the
// decode-to-float / re-encode conversion.
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
// 10F_11F_11F_REV word holding (1.0, 0.5, 0.25) - see the packed readback encode tests.
const Uint32 packedFloatWord = 0x681C03C0u;
MG_Impl::GLImpl::PixelStorei(GL_UNPACK_ALIGNMENT, 1);
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGB9_E5, 1, 1, 0, GL_RGB, GL_UNSIGNED_INT_10F_11F_11F_REV,
&packedFloatWord);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
const auto* stored = GetBoundTexture2DLevelBytes(texture);
ASSERT_NE(stored, nullptr);
Uint32 word = 0;
std::memcpy(&word, stored, sizeof(word));
const Float rgb[3] = {1.0f, 0.5f, 0.25f};
EXPECT_EQ(word, MG_Util::EncodeSharedExponentRGB9E5(rgb));
EXPECT_NE(word, packedFloatWord) << "the raw path must not swallow a real conversion";
MG_Impl::GLImpl::PixelStorei(GL_UNPACK_ALIGNMENT, 4);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0);
}
TEST_F(TextureTest, StorePackedWordsToClientCopiesWordsVerbatimUnderPackParams) {
// Readback direction: the raw store copies the words bit-for-bit while still honoring the
// client-side PACK addressing (alignment, skip rows/pixels) and GL_PACK_SWAP_BYTES.
namespace ReadbackImpl = MG_Backend::DirectGLES::ReadbackImpl;
const Uint32 source[] = {0xF8FC0000u, 0x60000000u, 0x08000001u, // row 0
0x0FFFFFFFu, 0xFFFFFFFFu, 0x00000000u}; // row 1
constexpr Uint32 kFill = 0xDEADBEEFu;
Uint32 destination[16];
std::fill(std::begin(destination), std::end(destination), kFill);
MG_Impl::GLImpl::PixelStorei(GL_PACK_ALIGNMENT, 8); // rows of 3 words (12 B) pad to 16 B
MG_Impl::GLImpl::PixelStorei(GL_PACK_SKIP_ROWS, 1);
MG_Impl::GLImpl::PixelStorei(GL_PACK_SKIP_PIXELS, 1);
ASSERT_TRUE(ReadbackImpl::StorePackedWordsToClient(reinterpret_cast<const Uint8*>(source), /*width=*/3,
/*sliceHeight=*/2, /*sliceCount=*/1,
GL_UNSIGNED_INT_5_9_9_9_REV, destination,
/*applyPackImageParams=*/false));
// Row 0 lands at SKIP_ROWS * 16 + SKIP_PIXELS * 4 = 20 bytes = word 5; row 1 one 16-byte
// stride further along, at word 9.
for (Int i = 0; i < 3; ++i) {
EXPECT_EQ(destination[5 + i], source[i]) << "row 0 texel " << i;
EXPECT_EQ(destination[9 + i], source[3 + i]) << "row 1 texel " << i;
}
// The skipped region and the row padding stay untouched.
EXPECT_EQ(destination[0], kFill);
EXPECT_EQ(destination[4], kFill);
EXPECT_EQ(destination[8], kFill);
EXPECT_EQ(destination[12], kFill);
// GL_PACK_SWAP_BYTES reverses each 4-byte word.
std::fill(std::begin(destination), std::end(destination), kFill);
MG_Impl::GLImpl::PixelStorei(GL_PACK_SKIP_ROWS, 0);
MG_Impl::GLImpl::PixelStorei(GL_PACK_SKIP_PIXELS, 0);
MG_Impl::GLImpl::PixelStorei(GL_PACK_ALIGNMENT, 1);
MG_Impl::GLImpl::PixelStorei(GL_PACK_SWAP_BYTES, GL_TRUE);
ASSERT_TRUE(ReadbackImpl::StorePackedWordsToClient(reinterpret_cast<const Uint8*>(source), /*width=*/3,
/*sliceHeight=*/1, /*sliceCount=*/1,
GL_UNSIGNED_INT_5_9_9_9_REV, destination,
/*applyPackImageParams=*/false));
EXPECT_EQ(destination[0], 0x0000FCF8u); // byte-reversed 0xF8FC0000
EXPECT_EQ(destination[1], 0x00000060u);
MG_Impl::GLImpl::PixelStorei(GL_PACK_SWAP_BYTES, GL_FALSE);
MG_Impl::GLImpl::PixelStorei(GL_PACK_ALIGNMENT, 4);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// GL 4.6 core table 23.18: GL_TEXTURE_COMPARE_FUNC takes the whole eight-function depth-compare
// range. The validator used to start it at GL_LEQUAL, which sits in the middle of the contiguous
// GL_NEVER..GL_ALWAYS block, so NEVER/LESS/EQUAL were rejected while GREATER/NOTEQUAL/GEQUAL only
@@ -3288,3 +3979,57 @@ TEST_F(TextureTest, GetTexLevelParameterOnBufferStorageReportsErrorInsteadOfTerm
ExpectSingleGlError(GL_INVALID_OPERATION);
}
}
// Immutable storage plus glCompressedTexSubImage2D is the modern way to upload a compressed
// texture, so glTexStorage2D has to commit its levels to a specific compressed internalformat
// exactly as glTexImage2D does. When it did not, the sub-image call found an uncompressed level
// and refused it, and glTexImage2D and glTexStorage2D disagreed about the same token.
TEST_F(TextureTest, TexStorage2DTagsEveryLevelForASpecificCompressedFormat) {
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
MG_Impl::GLImpl::TexStorage2D(GL_TEXTURE_2D, 2, GL_COMPRESSED_RED_RGTC1, 8, 8);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
for (GLint level = 0; level < 2; ++level) {
GLint compressed = GL_FALSE;
MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_2D, level, GL_TEXTURE_COMPRESSED, &compressed);
EXPECT_EQ(compressed, GL_TRUE) << "level " << level;
GLint internalFormat = 0;
MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_2D, level, GL_TEXTURE_INTERNAL_FORMAT, &internalFormat);
EXPECT_EQ(internalFormat, static_cast<GLint>(GL_COMPRESSED_RED_RGTC1)) << "level " << level;
GLint imageSize = 0;
MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_2D, level, GL_TEXTURE_COMPRESSED_IMAGE_SIZE, &imageSize);
// 8x8 -> 2x2 blocks -> 32 bytes; 4x4 -> 1 block -> 8 bytes.
EXPECT_EQ(imageSize, level == 0 ? 32 : 8) << "level " << level;
}
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// ...and the sub-image call the whole arrangement exists for now reaches both levels.
Uint8 blocks[kRgtc1Size8x8];
for (Int i = 0; i < kRgtc1Size8x8; ++i) blocks[i] = static_cast<Uint8>(0x40 + i);
MG_Impl::GLImpl::CompressedTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 8, 8, GL_COMPRESSED_RED_RGTC1, kRgtc1Size8x8,
blocks);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
Uint8 stored[kRgtc1Size8x8] = {};
MG_Impl::GLImpl::GetCompressedTexImage(GL_TEXTURE_2D, 0, stored);
EXPECT_EQ(std::memcmp(stored, blocks, sizeof(stored)), 0);
MG_Impl::GLImpl::CompressedTexSubImage2D(GL_TEXTURE_2D, 1, 0, 0, 4, 4, GL_COMPRESSED_RED_RGTC1, 8, blocks);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// The negative control: a generic compressed token leaves glTexStorage2D's levels uncompressed,
// because for those the implementation's choice IS the answer and MobileGL chooses uncompressed.
TEST_F(TextureTest, TexStorage2DLeavesAGenericCompressedFormatUncompressed) {
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
MG_Impl::GLImpl::TexStorage2D(GL_TEXTURE_2D, 1, GL_COMPRESSED_RED, 8, 8);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
GLint compressed = GL_TRUE;
MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_COMPRESSED, &compressed);
EXPECT_EQ(compressed, GL_FALSE);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
+20
View File
@@ -16,5 +16,25 @@ target_link_libraries(
${LINK_LIBRARIES}
)
# The log-severity ordering and the MOBILEGL_ASSERT gate keyed to it. Links gtest
# (not gtest_main): the suite needs its own main() to point MOBILEGL_LOG_FILE_PATH at a
# temp file before anything in the process logs and latches the sink's FILE*.
add_executable(
LogLevelTest
LogLevelTest.cpp
)
target_include_directories(LogLevelTest PRIVATE
${MGL_ROOT}/include
${MGL_ROOT}/MobileGL
)
target_link_libraries(
LogLevelTest PRIVATE
GTest::gtest
${LINK_LIBRARIES}
)
include(GoogleTest)
gtest_discover_tests(JobNodeTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
gtest_discover_tests(LogLevelTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
+204
View File
@@ -0,0 +1,204 @@
// MobileGL - MobileGL/MG_Test/Util/LogLevelTest.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// Guards the log-severity ordering and the MOBILEGL_ASSERT gate that hangs off it.
//
// Until 2026-08-13 the numeric order was DEBUG < WARN < ERROR < INFO < FATAL, so the
// production gate `#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_X` compiled
// MGLOG_W and MGLOG_E out of every INFO build. Failures logged with MGLOG_E were
// invisible in exactly the builds that shipped. Nothing in the suite noticed, which is
// why this file exists.
//
// This test is written to be meaningful in BOTH configurations - build it at
// MOBILEGL_LOG_LEVEL_INFO and at MOBILEGL_LOG_LEVEL_DEBUG and it checks the contract
// appropriate to each. It runs headless: no GL context, no device, just the file sink.
#include <gtest/gtest.h>
#include <Defines.h>
#include <MG_Util/Debug/Log.h>
#include <cstdio>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <string>
namespace {
// ---------------------------------------------------------------------------
// Compile-time contract
// ---------------------------------------------------------------------------
// The ordering itself. A renumbering that re-inverts the scale fails here.
static_assert(MOBILEGL_LOG_LEVEL_DEBUG < MOBILEGL_LOG_LEVEL_INFO, "DEBUG must be below INFO");
static_assert(MOBILEGL_LOG_LEVEL_INFO < MOBILEGL_LOG_LEVEL_WARN, "INFO must be below WARN");
static_assert(MOBILEGL_LOG_LEVEL_WARN < MOBILEGL_LOG_LEVEL_ERROR, "WARN must be below ERROR");
static_assert(MOBILEGL_LOG_LEVEL_ERROR < MOBILEGL_LOG_LEVEL_FATAL, "ERROR must be below FATAL");
// DEBUG must stay the floor: the MOBILEGL_ASSERT gate in Defines.h is spelled
// `ACTIVE <= MOBILEGL_LOG_LEVEL_DEBUG` and means "only in a DEBUG build". That
// reading is only correct while DEBUG is the minimum.
static_assert(MOBILEGL_LOG_LEVEL_DEBUG == 0, "DEBUG must be the lowest level");
// Defines.h and Log.h each define the five constants. Log.h's copy wins when both
// are included; if the two ever drift, the duplicate-definition warning is not
// guaranteed to be an error, so pin the values a second time from this TU's view.
static_assert(MOBILEGL_LOG_LEVEL_INFO == 1, "INFO must be 1 in both Defines.h and Log.h");
static_assert(MOBILEGL_LOG_LEVEL_WARN == 2, "WARN must be 2 in both Defines.h and Log.h");
static_assert(MOBILEGL_LOG_LEVEL_ERROR == 3, "ERROR must be 3 in both Defines.h and Log.h");
static_assert(MOBILEGL_LOG_LEVEL_FATAL == 4, "FATAL must be 4 in both Defines.h and Log.h");
// Whether this translation unit was compiled with asserts live. This is a literal
// copy of the Defines.h gate - the point of the test is to prove it agrees with
// MGLOG_D's liveness, observed at runtime below.
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
constexpr bool kAssertsLive = true;
#else
constexpr bool kAssertsLive = false;
#endif
// Whether the build is the production INFO configuration.
constexpr bool kBuiltAtInfo = (MOBILEGL_LOG_ACTIVE_LEVEL == MOBILEGL_LOG_LEVEL_INFO);
// ---------------------------------------------------------------------------
// Runtime observation of the sink
// ---------------------------------------------------------------------------
// The log file path is latched by MG_Util::Debug::InitFile() on the first write and
// never reopened, so the whole process gets one file. main() below points
// MOBILEGL_LOG_FILE_PATH at a temp file before gtest runs; this fixture emits one
// line per level and then reads the file back.
std::string g_logPath;
struct Emitted {
bool debug = false;
bool info = false;
bool warn = false;
bool error = false;
bool fatal = false;
};
Emitted EmitAndRead() {
// Distinctive markers so a substring search cannot collide with unrelated output.
MGLOG_D("MGLOGTEST_MARKER_DEBUG_5f3a");
MGLOG_I("MGLOGTEST_MARKER_INFO_5f3a");
MGLOG_W("MGLOGTEST_MARKER_WARN_5f3a");
MGLOG_E("MGLOGTEST_MARKER_ERROR_5f3a");
MGLOG_F("MGLOGTEST_MARKER_FATAL_5f3a");
std::ifstream in(g_logPath, std::ios::binary);
std::ostringstream ss;
ss << in.rdbuf();
const std::string text = ss.str();
Emitted e;
e.debug = text.find("MGLOGTEST_MARKER_DEBUG_5f3a") != std::string::npos;
e.info = text.find("MGLOGTEST_MARKER_INFO_5f3a") != std::string::npos;
e.warn = text.find("MGLOGTEST_MARKER_WARN_5f3a") != std::string::npos;
e.error = text.find("MGLOGTEST_MARKER_ERROR_5f3a") != std::string::npos;
e.fatal = text.find("MGLOGTEST_MARKER_FATAL_5f3a") != std::string::npos;
return e;
}
TEST(LogLevel, SinkIsReachableAtAll) {
// Guards the test itself: if the file sink were disabled or the path override
// ignored, every "level X is suppressed" assertion below would pass vacuously.
ASSERT_FALSE(g_logPath.empty()) << "test harness did not set MOBILEGL_LOG_FILE_PATH";
const Emitted e = EmitAndRead();
EXPECT_TRUE(e.fatal) << "FATAL is compiled in at every level; an empty log means the "
"file sink never opened and this suite proves nothing";
}
TEST(LogLevel, ProductionBuildKeepsErrorAndWarn) {
if constexpr (!kBuiltAtInfo) {
GTEST_SKIP() << "only meaningful when built at MOBILEGL_LOG_LEVEL_INFO";
} else {
const Emitted e = EmitAndRead();
// The regression this file exists for.
EXPECT_TRUE(e.error) << "MGLOG_E must be live in an INFO build";
EXPECT_TRUE(e.warn) << "MGLOG_W must be live in an INFO build";
EXPECT_TRUE(e.info) << "MGLOG_I must be live in an INFO build";
EXPECT_TRUE(e.fatal) << "MGLOG_F must be live in an INFO build";
// ...and the other half: D must still be compiled out, or production pays
// for every dev-only diagnostic in the tree.
EXPECT_FALSE(e.debug) << "MGLOG_D must be compiled out of an INFO build";
}
}
TEST(LogLevel, DebugBuildKeepsEverything) {
if constexpr (MOBILEGL_LOG_ACTIVE_LEVEL != MOBILEGL_LOG_LEVEL_DEBUG) {
GTEST_SKIP() << "only meaningful when built at MOBILEGL_LOG_LEVEL_DEBUG";
} else {
const Emitted e = EmitAndRead();
EXPECT_TRUE(e.debug);
EXPECT_TRUE(e.info);
EXPECT_TRUE(e.warn);
EXPECT_TRUE(e.error);
EXPECT_TRUE(e.fatal);
}
}
// ---------------------------------------------------------------------------
// The assert contract
// ---------------------------------------------------------------------------
TEST(LogLevel, AssertGateTracksDebugLiveness) {
// The contract: "INFO builds: asserts OFF; DEBUG builds: asserts ON". Stated
// without naming a level, that is exactly "asserts are live iff MGLOG_D is
// live" - which is checkable in whichever configuration this was built in,
// and is what makes the renumbering safe.
const Emitted e = EmitAndRead();
EXPECT_EQ(kAssertsLive, e.debug)
<< "MOBILEGL_ASSERT liveness (" << kAssertsLive << ") disagrees with MGLOG_D liveness ("
<< e.debug << "). The Defines.h assert gate and the Log.h MGLOG_D gate have drifted.";
}
TEST(LogLevel, AssertIsCompiledOutOfProductionBuilds) {
if constexpr (kAssertsLive) {
GTEST_SKIP() << "asserts are live in this configuration; see AssertIsLiveInDebugBuilds";
} else {
// If MOBILEGL_ASSERT were live here this would TRAP and take the process
// down, which is the behavioural half of the contract.
MOBILEGL_ASSERT(false, "this assert must be compiled out at %s", "INFO");
SUCCEED();
}
}
TEST(LogLevel, AssertIsLiveInDebugBuilds) {
if constexpr (!kAssertsLive) {
GTEST_SKIP() << "asserts are compiled out in this configuration";
} else {
// A satisfied assert must be a no-op rather than a trap; that it expands to
// real code at all is what kAssertsLive already established.
MOBILEGL_ASSERT(true, "a satisfied assert must not trap");
SUCCEED();
}
}
} // namespace
int main(int argc, char** argv) {
// Must happen before anything logs: MG_Util::Debug::InitFile() reads the variable
// once, on the first write, and caches the FILE*.
namespace fs = std::filesystem;
const fs::path path = fs::temp_directory_path() / "mobilegl-loglevel-test.log";
std::error_code ec;
fs::remove(path, ec);
g_logPath = path.string();
#if defined(_WIN32)
_putenv_s("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str());
#else
setenv("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str(), 1);
#endif
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
+3 -3
View File
@@ -32,11 +32,11 @@ namespace MobileGL::MG_Util::Async {
try {
continuation();
} catch (const std::exception& e) {
MGLOG_E("JobNode: a terminal continuation threw (%s); it has been contained, but whatever it "
MGLOG_E_ONCE("JobNode: a terminal continuation threw (%s); it has been contained, but whatever it "
"was going to do did not happen",
e.what());
} catch (...) {
MGLOG_E("JobNode: a terminal continuation threw a non-std exception; it has been contained, "
MGLOG_E_ONCE("JobNode: a terminal continuation threw a non-std exception; it has been contained, "
"but whatever it was going to do did not happen");
}
}
@@ -165,7 +165,7 @@ namespace MobileGL::MG_Util::Async {
Vector<String> lines;
lines.swap(node.diagnostics.logLines);
for (const String& line : lines) {
MGLOG_W("%s", line.c_str());
MGLOG_D("%s", line.c_str());
}
}
+1 -1
View File
@@ -281,7 +281,7 @@ namespace MobileGL::MG_Util::Async {
enqueued = true;
}
} catch (...) {
MGLOG_E("ShaderCompilePool::Post: enqueue failed; cancelling the job so its joiner "
MGLOG_E_ONCE("ShaderCompilePool::Post: enqueue failed; cancelling the job so its joiner "
"cannot block forever");
if (node) node->Cancel();
return;
@@ -540,6 +540,10 @@ namespace MobileGL::MG_Util::BackendLoader {
INIT_GLES_FUNC_OPTIONAL(glMultiDrawArraysIndirectEXT)
INIT_GLES_FUNC_OPTIONAL(glMultiDrawElementsIndirectEXT)
INIT_GLES_FUNC_OPTIONAL(glMultiDrawElementsBaseVertexEXT)
INIT_GLES_FUNC_OPTIONAL(glDrawArraysInstancedBaseInstanceEXT)
INIT_GLES_FUNC_OPTIONAL(glDrawElementsInstancedBaseInstanceEXT)
INIT_GLES_FUNC_OPTIONAL(glDrawElementsInstancedBaseVertexBaseInstanceEXT)
}
}
@@ -590,9 +594,10 @@ namespace MobileGL::MG_Util::BackendLoader {
#endif // !_WIN32
if (!eglLib) {
// MGLOG_F, not MGLOG_E: at the INFO log level every shipping and CI build
// uses, MGLOG_E is compiled out (Log.h orders DEBUG < WARN < ERROR < INFO),
// so this diagnosis was invisible in precisely the builds that needed it.
// MGLOG_F, not MGLOG_E: with no EGL there is no rendering at all, so this is a
// bring-up abort rather than a recoverable error. It was forced to F while the
// Log.h ordering compiled MGLOG_E out of every shipping and CI build; F is still
// the right level on its own merits, so it stays.
MGLOG_F("Failed to open EGL library: none of libEGL.so.1 / libEGL.so could be "
"dlopened; every EGL entry point will be null");
return;
@@ -851,6 +856,9 @@ namespace MobileGL::MG_Util::BackendLoader {
// Resolved into caps.TextureBufferSupport below, once the ES version is also known.
Bool hasExtTextureBuffer = false;
Bool hasOesTextureBuffer = false;
// Combined with the three entry points below; DirectGLES emulates baseInstance when this
// comes out false, so a stub pointer counting as support would silently break the draws.
Bool hasBaseInstanceExtension = false;
for (GLint i = 0; i < extCount; ++i) {
const char* extension = (const char*)glesFuncs.glGetStringi(GL_EXTENSIONS, i);
if (extension) {
@@ -891,7 +899,7 @@ namespace MobileGL::MG_Util::BackendLoader {
hasOesTextureBuffer = true;
}
if (std::strcmp(extension, "GL_EXT_base_instance") == 0) {
caps.SupportsBaseInstance = true;
hasBaseInstanceExtension = true;
}
if (std::strcmp(extension, "GL_EXT_disjoint_timer_query") == 0) {
caps.SupportsDisjointTimerQuery = true;
@@ -905,6 +913,9 @@ namespace MobileGL::MG_Util::BackendLoader {
if (std::strcmp(extension, "GL_NV_shader_noperspective_interpolation") == 0) {
caps.SupportsNoperspectiveInterpolation = true;
}
if (std::strcmp(extension, "GL_NV_image_formats") == 0) {
caps.SupportsExtendedImageFormats = true;
}
if (std::strcmp(extension, "GL_OES_shader_multisample_interpolation") == 0) {
caps.SupportsShaderMultisampleInterpolation = true;
}
@@ -918,6 +929,9 @@ namespace MobileGL::MG_Util::BackendLoader {
if (std::strcmp(extension, "GL_EXT_multi_draw_arrays") == 0) {
hasMultiDrawArraysExtension = true;
}
if (std::strcmp(extension, "GL_EXT_clip_cull_distance") == 0) {
caps.SupportsClipDistance = true;
}
}
}
// The pointer check on top of the extension check makes each flag sufficient on its own
@@ -929,6 +943,12 @@ namespace MobileGL::MG_Util::BackendLoader {
caps.SupportsMultiDrawElementsBaseVertex = hasDrawElementsBaseVertexExtension &&
hasMultiDrawArraysExtension &&
glesFuncs.glMultiDrawElementsBaseVertexEXT != nullptr;
// All three, not any: DirectGLES picks native-vs-emulated once per draw entry point off
// this single flag, so a driver that resolved only some of them must count as absent.
caps.SupportsBaseInstance = hasBaseInstanceExtension &&
glesFuncs.glDrawArraysInstancedBaseInstanceEXT != nullptr &&
glesFuncs.glDrawElementsInstancedBaseInstanceEXT != nullptr &&
glesFuncs.glDrawElementsInstancedBaseVertexBaseInstanceEXT != nullptr;
// Core from ES 3.2 on, so an extension string is not required there; below 3.2 the
// extension is, and the pointer still has to have resolved either way.
const Bool esAtLeast32 = caps.GLESVersion.Major > 3 ||
@@ -963,7 +983,16 @@ namespace MobileGL::MG_Util::BackendLoader {
MGLOG_I(" draw elements base vertex (ES 3.2 core or EXT/OES_draw_elements_base_vertex): %s",
caps.SupportsDrawElementsBaseVertex ? "yes" : "no");
MGLOG_I(" compute shaders (ES 3.1 core): %s", caps.SupportsComputeShader ? "yes" : "no");
MGLOG_I(" base instance (EXT_base_instance; emulated by attribute offsets when absent): %s",
caps.SupportsBaseInstance ? "yes" : "no");
MGLOG_I(" clip distances (EXT_clip_cull_distance): %s", caps.SupportsClipDistance ? "yes" : "no");
// LOAD-BEARING STRING, not just a banner. android-plugin/trace-replay-ci.sh's
// is_angle_surface_lost() greps mobilegl.log for exactly "OpenGL ES capabilities:" to
// decide whether MobileGL got far enough to have a working context: if the probe ran,
// a later surface loss is a real defect rather than an emulator fault worth retrying.
// Demoting this line, renaming it, or moving it before the context is usable silently
// inverts that retry logic. It is init-phase, so MGLOG_I is correct and it stays.
MGLOG_I("OpenGL ES capabilities:");
glesFuncs.glGetIntegerv(GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT, &caps.UniformBufferOffsetAlignment);
MGLOG_I(" GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT: %d", caps.UniformBufferOffsetAlignment);

Some files were not shown because too many files have changed in this diff Show More