Compare commits

...
Author SHA1 Message Date
swung0x48 aa2184e47a [Test] (TraceReplay): register iterationRP in-world fixture (non-CI, fixture files pending LFS) 2026-07-19 21:39:06 -04:00
swung0x48 9152a4a4bc [Test] (TraceReplay): enable improved-transparency fixture in CI and drop unneeded coherent_as_flush 2026-07-19 07:35:34 -04:00
swung0x48 1963b427db [Refactor] (TraceReplay): reorganize trace skills into uniform packages with bundled scripts 2026-07-19 07:00:56 -04:00
swung0x48 f3def150e7 [Fix] (DirectVulkan): suppress blended depth writes on Qualcomm and mark gl_Position invariant to fix MC 26.3 OIT cloud flicker 2026-07-19 05:47:23 -04:00
swung0x48 fc0688c223 [Fix] (CI): stabilize Android retrace jobs 2026-07-18 19:52:04 -04:00
swung0x48 626c7f26fd [Test] (CMake, CI): enable top-level testing and label tests (unit/benchmark/integration) so ctest runs from the build root 2026-07-18 11:37:56 -04:00
swung0x48 1d947d934b [Feat] (TraceReplay): add RenderDoc Android capture tools 2026-07-18 11:25:07 -04:00
swung0x48 4203837648 [Feat] (trace-replay): register improved transparency fixture 2026-07-18 07:28:55 -04:00
swung0x48 f5cba4c2f1 [Docs] (trace-replay): unify Android trace runner docs 2026-07-18 07:06:56 -04:00
swung0x48 c2a1db3fcb [Feat] (trace-replay): add improved transparency fixture 2026-07-18 05:34:01 -04:00
swung0x48 594916850f [Fix] (MG_State, MG_Backend/DirectVulkan): bump the texture bind generation when a context default texture crosses the undefined<->defined boundary so cached sampled sets re-resolve, and collect the fallback texture into the sampled set so its first use transitions outside the render pass 2026-07-17 22:33:31 -04:00
swung0x48 c718d6bad8 [Fix] (MG_Test/Backend/DirectVulkan): link the whole MobileGL_s archive on MSVC so dllimport-declared gl*/egl* references resolve against the in-library entry points 2026-07-17 21:32:17 -04:00
swung0x48 abfda60ff1 [Perf] (MG_Backend/DirectGLES): check the unbind cache before activating the texture unit so the bind-0 sweep stops issuing redundant glActiveTexture per draw 2026-07-17 21:32:16 -04:00
swung0x48 92cced9bcc [Fix] (MG_State, MG_Impl, MG_Test): enforce strict GL 3.3 core rules only on contexts that explicitly request a core profile - texture deleted-name reservation keep and VAO-0 draws relax otherwise or under MOBILEGL_RELAXED_SEMANTICS, and GL_CONTEXT_PROFILE_MASK reports the requested profile 2026-07-17 21:32:16 -04:00
swung0x48 1929a7c546 [Fix] (MG_State, MG_Backend/DirectGLES): preserve legacy texture reuse and clear default backend bindings - keep generated-but-unbound names alive for Minecraft 1.7.10 atlas uploads, synchronize bind-0 to native GLES without 1D/2D alias churn, and cover both paths with BGRA sub-image and binding-cache regressions 2026-07-17 21:32:15 -04:00
swung0x48 df7f5a369d [Fix] (MG_Backend/DirectVulkan): render passes had zero subpass dependencies and same-layout transitions emit no barrier, so tilers could race tile loads against prior passes' stores (flickering artifacts in multi-pass chains like MC 26.3 OIT); add conservative external dependencies both ways 2026-07-17 21:03:52 -04:00
swung0x48 0de9861da4 [Fix] (MG_Backend/DirectVulkan): render-pass cache grew unbounded; age entries per present and evict after 1024 unused frames 2026-07-17 20:03:54 -04:00
swung0x48 6b223e4d23 [Fix] (MG_Backend/DirectVulkan): blendEnable was baked into pipelines without checking VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BLEND_BIT; disable blending on formats that lack it 2026-07-17 20:03:53 -04:00
swung0x48 48568cdb89 [Fix] (MG_Backend/DirectVulkan): deferred glClear/glClearBuffer ignored color/depth/stencil write masks; gate the queued planes like the scissored path 2026-07-17 20:03:53 -04:00
swung0x48 8d83dedc0f [Fix] (MG_Backend/DirectVulkan): pending deferred clears outlived blit/copy writes into the same texture and later stomped them (OIT's cloud_depth copy was erased by its own earlier queued clear); materialize destination pending clears before blit/copy writes 2026-07-17 19:24:55 -04:00
swung0x48 a25d8ee0e7 [Fix] (MG_Backend/DirectGLES): GLES clamps glClearColor to [0,1], zeroing the -FLT_MAX MAX-blend identity; route out-of-range color clears through glClearBufferfv 2026-07-17 11:45:55 -04:00
swung0x48 c30bd0fabb [Fix] (MG_Backend/DirectGLES, MG_Impl/GLImpl): draw/read-buffer state could land on the wrong FBO (glDrawBuffer's static-array latch; SyncToBackend emitting glDrawBuffers/glReadBuffer for the non-bound target; no resync on bound-FBO attachment/drawbuffer edits) leaving MC 26.3's OIT color clears as no-ops; apply per bound target and track the FBO object version 2026-07-17 11:45:54 -04:00
swung0x48 c9fbf79d6a [Fix] (MG_Backend/DirectGLES): blend equations were never synced to the driver (OIT's GL_MAX ran as ADD); diff and emit glBlendEquationSeparate(i) alongside the factor sync 2026-07-17 11:45:53 -04:00
swung0x48 f39e8738da Merge origin/dev (readback overhaul c6d22e6e) into default-texture-objects - true per-target default texture objects supersede the readback branch texture-0 silent no-ops: removed the null-slot early-outs in TexImage1D/2D/3D(Multisample) and TexBuffer plus the DefaultTextureOperationsAreSilentNoOps test so name-0 operations actually (re)specify the default objects; deduped the shared state-reset fixes, keeping upstream std::clamp for GL_MAX_UNIFORM_BUFFER_BINDINGS, the Int-typed ActiveTexture combined-units range check, renderbuffer name-0 unbind, and vertex-attrib-0 current-value writes with the attrib-0 round-trip test 2026-07-16 23:50:47 -04:00
swung0x48 c6d22e6ece Merge branch 'dev' of https://github.com/MobileGL-Dev/MobileGL into dev 2026-07-16 23:38:48 -04:00
swung0x48 981f10e4da [Fix] (MG_Impl/GLImpl): unblock the non-texture sections of GL CTS per-case state reset - clamp advertised GL_MAX_UNIFORM_BUFFER_BINDINGS to the state layer indexed-binding capacity (glBindBufferBase rejected indices past it), let glBindRenderbuffer(0) unbind without recording INVALID_OPERATION (name 0 must never reach the name-table lookup), and allow writes to generic vertex attribute 0 current value (core GL has no attribute-0 restriction; gluStateReset writes vertexAttrib4f(0,...) after every case) - with these plus the default-texture work, multi-case glcts batches complete in one process instead of aborting after the first case 2026-07-16 23:36:58 -04:00
swung0x48 076cd0d19d [Feat] (MG_State, MG_Impl/GLImpl): per-target default texture objects (name 0) - binding 0 binds a real per-context default object (the initial binding of every unit/target slot, rebound on delete of a bound texture), so glTexImage*/glTexParameter*/glGetTex* on it work like any texture while glIsTexture(0)/Gen/Delete keep excluding it and TexStorage* rejects it per spec; backends skip image-less defaults as cheaply as the old null slots (DirectGLES per-draw sync/bind loops, DirectVulkan sampler-fallback resolve); also accept the full advertised GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS range in glActiveTexture, allow zero-layer TexImage3DMultisample, and let glTexBuffer(buffer=0) detach - the texture section of GL CTS per-case state reset (gluStateReset) now runs clean 2026-07-16 23:36:43 -04:00
swung0x48 5cd82f2002 Merge origin/dev (efd7b473) into readback overhaul - unify DirectGLES 2D-array target support under MapToBackendTextureTarget, keep canonical UNorm8 shadows for RGBA4/RGB565 (supersedes packed-word transfer types; GL_RGB565 aliases RGB5), keep upstream GLSL 330 normalization, anisotropy params, error-count semantics and VK clear/scissor fixes 2026-07-16 23:17:37 -04:00
swung0x48 d4922cb0fb [Fix] (MG_Backend, MG_Impl/GLImpl, MG_Util): make anisotropic filtering actually reachable - advertise GL_EXT/ARB_texture_filter_anisotropic only where the host driver or the samplerAnisotropy device feature supports it, answer GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT from the backend limit, and honor the sampler state on DirectVulkan (feature enable, limit clamp, LINEAR-only gate, resolved value in the sampler cache key) 2026-07-16 22:59:11 -04:00
swung0x48 870d882fef [Fix] (MG_Backend/DirectGLES, MG_Impl/GLImpl, MG_Util): GL CTS packed_pixels + texture_swizzle readback overhaul - canonical shadow layouts for legacy sized/unsized/packed internal formats (RGB5->RGB565, RGB10/12->RGB16, RGBA2->RGBA4, RGB10_A2(UI)/RGB9_E5/R11F_G11F_B10F packed-word shadows with per-texel encode/decode incl. 5_9_9_9_REV and 10F_11F_11F_REV client types), GL_UNSIGNED_INT_10_10_10_2 pixel type mapping, conversion-first GetTexImage with CPU-shadow fallback for non-attachable formats and stale-temp-FBO detach, narrow implementation read pairs + SNORM read candidates + 2_10_10_10_REV wide-read decode with RGBA expansion, PACK image/skip and SWAP_BYTES honored on the CPU repack (never in ES), state-reset conformance (default-texture TexParameter/TexImage/TexBuffer no-ops, renderbuffer 0 unbind, vertex attrib 0 current value, ActiveTexture up to combined units, UBO binding count clamp), FramebufferTexture3D/TextureLayer slice attachments via glFramebufferTextureLayer, capability-driven FBO UNSUPPORTED for non-renderable colors, ReadPixels integer-ness mismatch error, single-value texture swizzle validation, and DirectGLES 1D/1D-array/2D-array texture emulation (2D/2D-array backend targets matching SPIRV-Cross ES 1D-as-2D shaders) 2026-07-16 22:41:54 -04:00
swung0x48 e2f873c95c [Fix] (MG_Util/ShaderTranspiler): retry a legacy shader at 460 when it fails to parse as normalized 330 core, so sources using 420-era syntax without the matching #extension line keep compiling as they did on real drivers 2026-07-16 22:33:30 -04:00
swung0x48 efd7b47388 [Chore] (MG_Test): assert exact GL error counts - name-lifecycle regression tests per object family, plus fixtures that drain on setup and fail the test that leaks an unconsumed error 2026-07-16 22:12:12 -04:00
swung0x48 a08669df72 [Fix] (MG_Impl/GLImpl): stop recording GL errors on the delete/query paths of every object family - glDeleteBuffers/VertexArrays/Renderbuffers/Framebuffers must silently ignore unknown names and glIsTexture must never raise, while glBindSampler now reports INVALID_OPERATION like the other bind entry points 2026-07-16 22:11:43 -04:00
swung0x48 b8db509581 [Fix] (MG_Util/ShaderTranspiler): normalize legacy desktop shaders to GLSL 330 2026-07-16 21:39:39 -04:00
swung0x48 5d6b544021 [Fix] (MG_Impl/Texture): support anisotropic sampler parameters 2026-07-16 21:39:38 -04:00
swung0x48 346cd417ca [Fix] (MG_Backend/DirectVulkan): fix ERROR-level vertex stream build 2026-07-16 21:36:22 -04:00
swung0x48 f61675e9ce [Fix] (MG_Impl/Texture): validate the bound texture before dereferencing it in TexSubImage2D, and stop recording an error when glDeleteTextures is handed unknown names 2026-07-16 21:34:40 -04:00
swung0x48 8026838563 [Fix] (MG_Backend/DirectVulkan): honor scissor in glClearBuffer*/glClearNamedFramebuffer* and clamp their depth clear values to [0,1] 2026-07-16 21:18:01 -04:00
swung0x48 5bd8fa8c4e [Perf] (MG_Backend/DirectVulkan): route full-coverage scissored glClear back to the deferred loadOp path, skip render-pass churn for no-op clears, and drop the per-clear heap Vector (extracted PrepareScissoredClear) 2026-07-16 21:18:00 -04:00
swung0x48 37ef2cb600 [Fix] (MG_Backend/DirectVulkan): materialize mid-pass pending color clears at the subpass color slot index, not the compacted description index 2026-07-16 21:18:00 -04:00
swung0x48 5184901a5b [Fix] (MG_Backend/DirectVulkan): clear every layer of layered framebuffers in vkCmdClearAttachments paths (rename the never-read RenderPassEntry::subpass to layers) 2026-07-16 21:17:59 -04:00
swung0x48 4172959e49 [Fix] (MG_Backend/DirectVulkan): honor the front stencil write mask in scissored glClear and drop a redundant compatibility re-check 2026-07-16 21:17:58 -04:00
swung0x48 eb5b4bca3e [Fix] (MG_Backend/DirectVulkan): ignore glClear/glClearBuffer* while GL_RASTERIZER_DISCARD is enabled, and early-out on an empty clear mask 2026-07-16 21:17:58 -04:00
swung0x48 56db115a1a [Fix] (MG_Impl/GLImpl): clamp glClearDepth to [0,1] per GL 3.3 (Vulkan clear values require it) 2026-07-16 21:17:57 -04:00
swung0x48 b14edd515a Merge branch 'worktree-agent-a1381c9e6b342e8d4' into dev 2026-07-16 19:47:52 -04:00
swung0x48 57030b5fb9 [Chore] (MG_Test/Texture): regression tests for 2D-array unpack subcuboid selection and TexStorage3D layer-count semantics, DirectGLES 2D-array target support, packed-format canonical transfer types, and the GL_RGB565 enum round-trip 2026-07-16 19:44:37 -04:00
swung0x48 72532de780 [Fix] (MG_Util/Texture, MG_Util/Converters): canonical packed transfer types for RGBA4/RGB565/RGB10_A2UI - NormalizePixelFormat's default handed backends GL_UNSIGNED_BYTE (and a non-integer GL_RGB transfer format for RGB10_A2UI), so uploads read 4 bytes per texel from 2-byte packed shadow rows (rgba4 layers shifted by 2x slice stride) or were rejected outright; also map GL_RGB565 <-> TextureInternalFormat::RGB5 (the enum had no GL_RGB565 mapping at all, glTexImage* with it failed as unknown) 2026-07-16 19:44:36 -04:00
swung0x48 25323bfb8e [Fix] (MG_Backend/DirectGLES, MG_Impl/GLImpl): sync GL_TEXTURE_2D_ARRAY textures to the ES backend - the target was skipped as unsupported so array textures never uploaded or bound (every KHR-GL33.pixelstoragemodes.teximage3d case failed); also keep array layer counts constant across mip levels in TexStorage3D and generated-mip storage allocation (only true 3D textures halve depth) 2026-07-16 19:44:20 -04:00
swung0x48 9ed5dbf483 [Perf] (MG_Backend/DirectVulkan): return VkBufferResource by raw pointer from GetOrCreateResource to drop per-draw SharedPtr refcounting 2026-07-16 18:34:55 -04:00
swung0x48 3ca57068a8 Merge branch 'worktree-agent-a7bc533bede191e68' into dev 2026-07-16 16:37:58 -04:00
swung0x48 075471cdbd Merge branch 'worktree-agent-a7bc533bede191e68' into dev 2026-07-16 16:37:58 -04:00
swung0x48 62a0ad5639 [Fix] (MG_Backend/DirectGLES, MG_Util/ShaderTranspiler): make the uniform-block cross-stage precision fix surgical - revert the global SPVC ES highp-default options (they changed emission for EVERY fragment shader: sampling code that used to inherit the effective highp default was suddenly printed as explicit mediump, regressing KHR-GL3x.texture_repeat_mode NPOT mip cases on device) and instead strip RelaxedPrecision member decorations from uniform-block-reachable structs in a DirectGLES-only SPIR-V pass, so matched blocks declare identical (highp) member precision in both stages and every other shader keeps its previous emission byte-for-byte 2026-07-16 16:36:28 -04:00
swung0x48 8bf8f6f906 [Fix] (MG_Backend/DirectGLES, MG_Util/ShaderTranspiler): make the uniform-block cross-stage precision fix surgical - revert the global SPVC ES highp-default options (they changed emission for EVERY fragment shader: sampling code that used to inherit the effective highp default was suddenly printed as explicit mediump, regressing KHR-GL3x.texture_repeat_mode NPOT mip cases on device) and instead strip RelaxedPrecision member decorations from uniform-block-reachable structs in a DirectGLES-only SPIR-V pass, so matched blocks declare identical (highp) member precision in both stages and every other shader keeps its previous emission byte-for-byte 2026-07-16 16:36:28 -04:00
swung0x48 249c1ca574 [Fix] (MG_Backend/DirectVulkan): honor scissor in glClear 2026-07-16 12:39:55 -04:00
swung0x48 6a843b3088 [Fix] (MG_Backend/DirectVulkan): honor scissor in glClear 2026-07-16 12:39:55 -04:00
swung0x48 2e94314b78 Merge branch 'worktree-agent-a7bc533bede191e68' into dev 2026-07-16 12:12:47 -04:00
swung0x48 c59c15f66a Merge branch 'worktree-agent-a7bc533bede191e68' into dev 2026-07-16 12:12:47 -04:00
swung0x48 4d1613ba55 [Fix] (MG_Util/ShaderTranspiler, MG_State, MG_Impl/GLImpl, MG_Backend/DirectGLES): GL CTS uniform_block - coerce packed/shared block layouts to std140 at source preprocess (glslang rejects them when targeting SPIR-V; std140 is the only UBO layout the pipeline emits), GL-style block reflection (array "[0]" names, per-element struct-array expansion, unused members and declared-but-unread blocks stay active), vec4-padded GL_UNIFORM_BLOCK_DATA_SIZE, std140 array strides for struct-nested arrays (glslang reflects tight strides there), arrayed-block instances share the first instance member set, glDeleteShader-flagged names stay usable while attached, and backend ESSL emits against highp default precision so relaxed block members match across stages (KHR-GL33.shaders.uniform_block on llvmpipe: 659 Fail -> 828/828 Pass) 2026-07-16 12:09:11 -04:00
swung0x48 254cf1dc21 [Fix] (MG_Util/ShaderTranspiler, MG_State, MG_Impl/GLImpl, MG_Backend/DirectGLES): GL CTS uniform_block - coerce packed/shared block layouts to std140 at source preprocess (glslang rejects them when targeting SPIR-V; std140 is the only UBO layout the pipeline emits), GL-style block reflection (array "[0]" names, per-element struct-array expansion, unused members and declared-but-unread blocks stay active), vec4-padded GL_UNIFORM_BLOCK_DATA_SIZE, std140 array strides for struct-nested arrays (glslang reflects tight strides there), arrayed-block instances share the first instance member set, glDeleteShader-flagged names stay usable while attached, and backend ESSL emits against highp default precision so relaxed block members match across stages (KHR-GL33.shaders.uniform_block on llvmpipe: 659 Fail -> 828/828 Pass) 2026-07-16 12:09:11 -04:00
swung0x48 607e84deed Merge branch 'worktree-agent-af155789372f0003b' into dev 2026-07-16 11:30:03 -04:00
swung0x48 df0e9fca71 Merge branch 'worktree-agent-af155789372f0003b' into dev 2026-07-16 11:30:03 -04:00
swung0x48 b831dae8d5 [Feat] (MG_Backend/DirectGLES): packed-type readback encoding - repack wide RGBA reads into all GL 3.3 packed pixel types (3_3_2/2_3_3_REV, 5_6_5(_REV), 4_4_4_4(_REV), 5_5_5_1/1_5_5_5_REV, 8_8_8_8(_REV), 10_10_10_2/2_10_10_10_REV, packed-float 10F_11F_11F_REV and shared-exponent 5_9_9_9_REV) for ReadPixels/GetTexImage; conversion helpers extracted to context-free ReadbackImpl (Utils.cpp) with unit tests asserting exact packed words against the GL CTS pack_* oracle layouts 2026-07-16 11:08:02 -04:00
swung0x48 0005a50517 [Feat] (MG_Backend/DirectGLES): packed-type readback encoding - repack wide RGBA reads into all GL 3.3 packed pixel types (3_3_2/2_3_3_REV, 5_6_5(_REV), 4_4_4_4(_REV), 5_5_5_1/1_5_5_5_REV, 8_8_8_8(_REV), 10_10_10_2/2_10_10_10_REV, packed-float 10F_11F_11F_REV and shared-exponent 5_9_9_9_REV) for ReadPixels/GetTexImage; conversion helpers extracted to context-free ReadbackImpl (Utils.cpp) with unit tests asserting exact packed words against the GL CTS pack_* oracle layouts 2026-07-16 11:08:02 -04:00
swung0x48 9f302373d6 [Fix] (MG_Impl/GLImpl): TexImage3D - apply ConvertInternalFormatToSized like 2D/1D so unsized-internal 3D uploads get channel/type conversion, skip proxy shadow allocation, auto-generate mipmaps; TexSubImage3D - bound level and region against the target mip 2026-07-16 10:54:48 -04:00
swung0x48 f896c7396f [Fix] (MG_Impl/GLImpl): TexImage3D - apply ConvertInternalFormatToSized like 2D/1D so unsized-internal 3D uploads get channel/type conversion, skip proxy shadow allocation, auto-generate mipmaps; TexSubImage3D - bound level and region against the target mip 2026-07-16 10:54:48 -04:00
swung0x48 6ca48e40fe [Feat] (MG_State, MG_Util, MG_Impl/GLImpl, MG_Backend/DirectGLES): desktop-GL single-channel client formats GL_GREEN/GL_BLUE/GL_ALPHA and _INTEGER variants - validate and readback via wide-RGBA channel extraction (GL CTS packed_pixels rgba8_format_green/blue read with them), unpack GREEN/BLUE(_INTEGER) TexImage uploads into the named channel with 0/1 defaults per table 3.3; GL_ALPHA keeps the legacy Red upload mapping (R8 storage + 000R swizzle), its readback corrected at the backend to source channel 3 2026-07-16 06:47:33 -04:00
swung0x48 1cefb9780b [Feat] (MG_State, MG_Util, MG_Impl/GLImpl, MG_Backend/DirectGLES): desktop-GL single-channel client formats GL_GREEN/GL_BLUE/GL_ALPHA and _INTEGER variants - validate and readback via wide-RGBA channel extraction (GL CTS packed_pixels rgba8_format_green/blue read with them), unpack GREEN/BLUE(_INTEGER) TexImage uploads into the named channel with 0/1 defaults per table 3.3; GL_ALPHA keeps the legacy Red upload mapping (R8 storage + 000R swizzle), its readback corrected at the backend to source channel 3 2026-07-16 06:47:33 -04:00
swung0x48 a1a8a18575 [Fix] (MG_Backend/DirectGLES, MG_Impl/GLImpl): ReadPixels - fall back to wide-format conversion when the ES driver rejects a legacy native read combo (Adreno errors on e.g. GL_RED/GL_UNSIGNED_INT and leaves the buffer untouched), and enforce packed-type/format pairing at the state layer via shared ValidateClientFormatTypePairing (GL_RED + GL_UNSIGNED_SHORT_5_6_5 now raises GL_INVALID_OPERATION) 2026-07-16 06:11:53 -04:00
swung0x48 bae222227a [Fix] (MG_Backend/DirectGLES, MG_Impl/GLImpl): ReadPixels - fall back to wide-format conversion when the ES driver rejects a legacy native read combo (Adreno errors on e.g. GL_RED/GL_UNSIGNED_INT and leaves the buffer untouched), and enforce packed-type/format pairing at the state layer via shared ValidateClientFormatTypePairing (GL_RED + GL_UNSIGNED_SHORT_5_6_5 now raises GL_INVALID_OPERATION) 2026-07-16 06:11:53 -04:00
swung0x48 37a050b106 [Fix] (MG_Backend/DirectGLES): upload shadow mips with UNPACK_ALIGNMENT=1 - shadow rows are tightly packed but uploads ran with alignment 4, shifting every row of non-multiple-of-4-width textures by one pixel (R8 7-wide CTS gradients read back diagonally) 2026-07-16 05:52:01 -04:00
swung0x48 12a67f596c [Fix] (MG_Backend/DirectGLES): upload shadow mips with UNPACK_ALIGNMENT=1 - shadow rows are tightly packed but uploads ran with alignment 4, shifting every row of non-multiple-of-4-width textures by one pixel (R8 7-wide CTS gradients read back diagonally) 2026-07-16 05:52:01 -04:00
swung0x48 164bfd810b [Fix] (MG_State/ErrorState): GL error flags are sticky per error code, not an unbounded queue - repeated same-code errors accumulated and leaked into later unrelated glGetError checks (GL CTS "Texture state reset failed" deinit noise and false "Error during glGetTexImage" failures) 2026-07-16 05:29:46 -04:00
swung0x48 274c234aff [Fix] (MG_State/ErrorState): GL error flags are sticky per error code, not an unbounded queue - repeated same-code errors accumulated and leaked into later unrelated glGetError checks (GL CTS "Texture state reset failed" deinit noise and false "Error during glGetTexImage" failures) 2026-07-16 05:29:46 -04:00
swung0x48 b8dc4a6004 [Fix] (MG_Util/Texture): expand channels and convert component types on texture unpack to the internal shadow layout 2026-07-16 04:39:57 -04:00
swung0x48 20e1b417cc [Fix] (MG_Util/Texture): expand channels and convert component types on texture unpack to the internal shadow layout 2026-07-16 04:39:57 -04:00
swung0x48 3e8c8b756f Merge branch 'worktree-agent-acd555500daee6840' into dev 2026-07-16 04:13:25 -04:00
swung0x48 4dbdbd3bcd Merge branch 'worktree-agent-acd555500daee6840' into dev 2026-07-16 04:13:25 -04:00
swung0x48 763d4c3207 [Fix] (CI): resume interrupted fixture downloads 2026-07-16 04:11:48 -04:00
swung0x48 5eeba29579 [Fix] (CI): resume interrupted fixture downloads 2026-07-16 04:11:48 -04:00
swung0x48 9351d66dbc Merge branch 'worktree-agent-a08726da20d78531a' into dev
# Conflicts:
2026-07-16 03:57:12 -04:00
swung0x48 dae8a40af1 Merge branch 'worktree-agent-a08726da20d78531a' into dev
# Conflicts:
2026-07-16 03:57:12 -04:00
swung0x48 e9bd520d9b [Feat] (MG_Backend/DirectVulkan): implement combined depth-stencil texture upload via per-aspect de-interleaved staging copies 2026-07-16 03:54:33 -04:00
swung0x48 8b78379f6f [Feat] (MG_Backend/DirectVulkan): implement combined depth-stencil texture upload via per-aspect de-interleaved staging copies 2026-07-16 03:54:33 -04:00
swung0x48 1dc217b32c [Fix] (MG_Util/Metrics): correct Depth32FStencil8 shadow texel size (16->8) and FLOAT_32_UNSIGNED_INT_24_8_REV type size (4->8) to the GL client transfer layout 2026-07-16 03:54:33 -04:00
swung0x48 535e9f8b15 [Fix] (MG_Util/Metrics): correct Depth32FStencil8 shadow texel size (16->8) and FLOAT_32_UNSIGNED_INT_24_8_REV type size (4->8) to the GL client transfer layout 2026-07-16 03:54:33 -04:00
swung0x48 35626da5c4 [Fix] (MG_Util/Texture): infer RGBA8 for packed RGBA uploads
(cherry picked from commit 1146188ed4)
2026-07-16 03:41:29 -04:00
swung0x48 b9844ed7c1 [Fix] (MG_Util/Texture): infer RGBA8 for packed RGBA uploads
(cherry picked from commit 1146188ed4)
2026-07-16 03:41:29 -04:00
swung0x48 8496e7c7eb [Fix] (MG_Backend/DirectGLES): convert narrow client formats (RED/RG/RGB/BGR/BGRA + integer variants, byte/short/half/8888 types) in GetTexImage/ReadPixels via wide RGBA readback 2026-07-16 03:40:54 -04:00
swung0x48 f0ed5c1b8e [Fix] (MG_Backend/DirectGLES): convert narrow client formats (RED/RG/RGB/BGR/BGRA + integer variants, byte/short/half/8888 types) in GetTexImage/ReadPixels via wide RGBA readback 2026-07-16 03:40:54 -04:00
swung0x48 c247ad5807 Merge branch 'worktree-agent-aae96e27d8b07e982' into dev 2026-07-16 03:32:39 -04:00
swung0x48 b604188849 Merge branch 'worktree-agent-aae96e27d8b07e982' into dev 2026-07-16 03:32:39 -04:00
swung0x48 7514587b5a [Fix] (MG_Impl/GLImpl, MG_State): fallback UBO backing for optimizer-eliminated uniforms (null-MapUBO SIGSEGV in KHR-GL33 do_while loops) + per-element locations/offsets for array uniforms incl. nested struct arrays (size assert in KHR-GL33 struct.uniform); demote uniform write assert to log-and-clamp 2026-07-16 03:31:32 -04:00
swung0x48 cf8f928db8 [Fix] (MG_Impl/GLImpl, MG_State): fallback UBO backing for optimizer-eliminated uniforms (null-MapUBO SIGSEGV in KHR-GL33 do_while loops) + per-element locations/offsets for array uniforms incl. nested struct arrays (size assert in KHR-GL33 struct.uniform); demote uniform write assert to log-and-clamp 2026-07-16 03:31:32 -04:00
swung0x48 176d130f09 [Fix] (MG_Backend): GetTexImage level-range check was off-by-one (max level is inclusive; single-level textures asserted on level 0), demoted to logged skip; advertise GL_ARB_texture_storage_multisample (entry points already implemented - unadvertised extension left null glw pointers and CTS framebuffer_blit jumped to address 0) 2026-07-16 03:06:03 -04:00
swung0x48 0b94e02de5 [Fix] (MG_Backend/DirectVulkan): skip combined depth-stencil texture data uploads instead of recording invalid single-copy with multi-bit aspect mask (VK_INCOMPLETE at vkEndCommandBuffer killed the process); proper per-aspect de-interleave tracked separately 2026-07-16 02:36:07 -04:00
swung0x48 fe7a5ee1b2 [Fix] (MG_Impl/GLImpl, MG_State, MG_Backend/DirectGLES): eliminate packed_pixels SIGTRAPs - complete TexImage format/type/internalformat validation matrix (depth-stencil family, integer-ness, packed-type pairing, 3D depth rejection), fix inverted UpdateSubData assert with clamped copy, demote unimplemented readback asserts to logged no-ops 2026-07-16 02:23:18 -04:00
swung0x48 5331150cb9 [Fix] (MG_Util/ShaderTranspiler): stop stripping precision qualifiers - the strip corrupted "precision highp float;" into invalid syntax; glslang accepts and ignores them natively in 460 core (unblocks ~2000 GL CTS cases per backend) 2026-07-16 01:50:33 -04:00
swung0x48 dc2f3a477b [Chore] (MG_Backend/DirectGLES): demote UBO ring creation log to debug level 2026-07-16 00:10:14 -04:00
swung0x48 21753b0e4c [Fix] (MG_Backend/DirectGLES): UBO ring - preserve generation across ES context recreation, retire frame marks at Present to bound growth 2026-07-16 00:10:14 -04:00
swung0x48 f509b19b1d [Fix] (MG_Backend/DirectGLES): UBO ring review fixes - invalidate array-buffer bind cache on failed ring creation, bump generation on emergency drain, division-based alignment rounding 2026-07-16 00:10:13 -04:00
swung0x48 c7ac5de28e [Chore] (MG_Backend/DirectGLES): log global-UBO ring creation 2026-07-16 00:10:13 -04:00
swung0x48 2e7073a890 [Perf] (MG_Backend/DirectGLES): replace per-draw global-UBO glBufferSubData with a persistent-mapped ring allocator (fence-watermark reclaimed, MOBILEGL_DISABLE_UBO_RING opt-out); scrub stale indexed-binding shadow on glDeleteBuffers 2026-07-16 00:10:12 -04:00
swung0x48 305701326c [Fix] (MG_Util/SelfTest): preserve POST checks before format probing 2026-07-16 00:10:12 -04:00
swung0x48 8c89b1618a [Feat] (MG_Backend, android-plugin): show format capability tables in POST 2026-07-16 00:10:11 -04:00
129 changed files with 10718 additions and 740 deletions
+149 -8
View File
@@ -10,11 +10,22 @@ case_name="$1"
fixture_dir="${2:-tools/trace_replay/fixtures}"
python_bin="${PYTHON:-python3}"
mirror_base="${MOBILEGL_TRACE_FIXTURE_MIRROR_BASE:-https://repo.miawa.cn/mgl/tools/trace_replay/fixtures}"
download_attempts="${MOBILEGL_TRACE_FIXTURE_DOWNLOAD_ATTEMPTS:-5}"
retry_delay="${MOBILEGL_TRACE_FIXTURE_RETRY_DELAY:-2}"
if ! command -v "${python_bin}" >/dev/null 2>&1 && command -v python >/dev/null 2>&1; then
python_bin=python
fi
if ! [[ "${download_attempts}" =~ ^[1-9][0-9]*$ ]]; then
echo "MOBILEGL_TRACE_FIXTURE_DOWNLOAD_ATTEMPTS must be a positive integer: ${download_attempts}" >&2
exit 2
fi
if ! [[ "${retry_delay}" =~ ^[0-9]+$ ]]; then
echo "MOBILEGL_TRACE_FIXTURE_RETRY_DELAY must be a non-negative integer: ${retry_delay}" >&2
exit 2
fi
fixture_list="$("${python_bin}" tools/trace_replay/trace_cases.py \
--format fixture-files \
--case "${case_name}" \
@@ -34,6 +45,140 @@ if [ "${case_name}" = "OpenRA" ]; then
exit 0
fi
get_lfs_metadata() {
local file="$1"
local pointer
local expected_oid
local expected_size
if ! pointer="$(git show "HEAD:${file}" 2>/dev/null)"; then
echo "failed to read tracked fixture metadata: ${file}" >&2
return 1
fi
if ! grep -q '^version https://git-lfs.github.com/spec/v1$' <<< "${pointer}"; then
echo "tracked fixture is not a Git LFS pointer: ${file}" >&2
return 1
fi
expected_oid="$(awk '$1 == "oid" && $2 ~ /^sha256:/ { sub(/^sha256:/, "", $2); print $2 }' <<< "${pointer}")"
expected_size="$(awk '$1 == "size" { print $2 }' <<< "${pointer}")"
if ! [[ "${expected_oid}" =~ ^[0-9a-f]{64}$ ]] || ! [[ "${expected_size}" =~ ^[0-9]+$ ]]; then
echo "invalid Git LFS pointer metadata: ${file}" >&2
return 1
fi
printf '%s %s\n' "${expected_oid}" "${expected_size}"
}
verify_fixture_file() {
local downloaded_file="$1"
local display_name="$2"
local expected_oid="$3"
local expected_size="$4"
local actual_oid
local actual_size
if [ ! -f "${downloaded_file}" ]; then
echo "fixture file is missing: ${display_name}" >&2
return 1
fi
actual_size="$(wc -c < "${downloaded_file}" | tr -d '[:space:]')"
if [ "${actual_size}" != "${expected_size}" ]; then
echo "fixture size mismatch for ${display_name}: expected ${expected_size}, got ${actual_size}" >&2
return 1
fi
actual_oid="$(sha256sum "${downloaded_file}" | awk '{ print $1 }')"
if [ "${actual_oid}" != "${expected_oid}" ]; then
echo "fixture SHA-256 mismatch for ${display_name}: expected ${expected_oid}, got ${actual_oid}" >&2
return 1
fi
}
fetch_file_from_mirror() {
local file="$1"
local url="$2"
local metadata
local expected_oid
local expected_size
local tmp_file="${file}.tmp"
local attempt
local partial_size
local curl_status
metadata="$(get_lfs_metadata "${file}")" || return 1
read -r expected_oid expected_size <<< "${metadata}"
if [ -f "${tmp_file}" ]; then
partial_size="$(wc -c < "${tmp_file}" | tr -d '[:space:]')"
if [ "${partial_size}" -gt "${expected_size}" ]; then
echo "Discarding oversized partial fixture ${tmp_file}: ${partial_size} > ${expected_size}" >&2
rm -f "${tmp_file}"
elif [ "${partial_size}" = "${expected_size}" ]; then
if verify_fixture_file "${tmp_file}" "${file}" "${expected_oid}" "${expected_size}"; then
mv "${tmp_file}" "${file}"
return 0
fi
rm -f "${tmp_file}"
fi
fi
for ((attempt = 1; attempt <= download_attempts; attempt++)); do
partial_size=0
if [ -f "${tmp_file}" ]; then
partial_size="$(wc -c < "${tmp_file}" | tr -d '[:space:]')"
fi
if [ "${partial_size}" -gt 0 ]; then
echo "Resuming mirror download for ${file} at byte ${partial_size} (attempt ${attempt}/${download_attempts})"
else
echo "Starting mirror download for ${file} (attempt ${attempt}/${download_attempts})"
fi
if curl -L --fail --show-error --continue-at - --output "${tmp_file}" "${url}"; then
if verify_fixture_file "${tmp_file}" "${file}" "${expected_oid}" "${expected_size}"; then
mv "${tmp_file}" "${file}"
return 0
fi
echo "Mirror download failed integrity verification; retrying from the beginning: ${file}" >&2
rm -f "${tmp_file}"
else
curl_status=$?
partial_size=0
if [ -f "${tmp_file}" ]; then
partial_size="$(wc -c < "${tmp_file}" | tr -d '[:space:]')"
fi
if [ "${partial_size}" = "${expected_size}" ]; then
if verify_fixture_file "${tmp_file}" "${file}" "${expected_oid}" "${expected_size}"; then
mv "${tmp_file}" "${file}"
return 0
fi
rm -f "${tmp_file}"
partial_size=0
elif [ "${partial_size}" -gt "${expected_size}" ]; then
echo "Discarding oversized partial fixture ${tmp_file}: ${partial_size} > ${expected_size}" >&2
rm -f "${tmp_file}"
partial_size=0
elif [ "${curl_status}" -eq 33 ]; then
echo "Mirror refused the resume request; retrying from the beginning: ${file}" >&2
rm -f "${tmp_file}"
partial_size=0
fi
echo "Mirror download attempt ${attempt}/${download_attempts} failed with curl exit ${curl_status}; retained ${partial_size} bytes for resume: ${file}" >&2
fi
if [ "${attempt}" -lt "${download_attempts}" ]; then
sleep "${retry_delay}"
fi
done
rm -f "${tmp_file}"
return 1
}
fetch_from_mirror() {
mkdir -p "${fixture_dir}"
for file in "${files[@]}"; do
@@ -42,11 +187,9 @@ fetch_from_mirror() {
name="$(basename "${file}")"
url="${mirror_base%/}/${name}"
echo "Fetching trace fixture from mirror: ${url}"
if ! curl -L --fail --retry 3 --retry-delay 2 -o "${file}.tmp" "${url}"; then
rm -f "${file}.tmp"
if ! fetch_file_from_mirror "${file}" "${url}"; then
return 1
fi
mv "${file}.tmp" "${file}"
done
}
@@ -59,9 +202,7 @@ else
fi
for file in "${files[@]}"; do
test -s "${file}"
if head -n 1 "${file}" | grep -q "version https://git-lfs.github.com/spec/v1"; then
echo "failed to hydrate LFS fixture: ${file}" >&2
exit 1
fi
metadata="$(get_lfs_metadata "${file}")"
read -r expected_oid expected_size <<< "${metadata}"
verify_fixture_file "${file}" "${file}" "${expected_oid}" "${expected_size}"
done
+60 -32
View File
@@ -227,6 +227,8 @@ jobs:
env:
AVD_NAME: mobilegl-ci
ANDROID_AVD_HOME: ${{ github.workspace }}/.android/avd
ANDROID_HOME: ${{ github.workspace }}/.android/sdk
ANDROID_SDK_ROOT: ${{ github.workspace }}/.android/sdk
steps:
- name: Checkout repo
uses: actions/checkout@v6
@@ -245,11 +247,11 @@ jobs:
with:
path: |
${{ env.ANDROID_AVD_HOME }}
/usr/local/lib/android/sdk/emulator
/usr/local/lib/android/sdk/platform-tools
/usr/local/lib/android/sdk/platforms/android-35
/usr/local/lib/android/sdk/system-images/android-35/google_apis/x86_64
key: ${{ runner.os }}-mobilegl-avd-api35-google_apis-x86_64-pixel_6-v1-${{ hashFiles('android-plugin/run-avd-ci.sh') }}
${{ env.ANDROID_SDK_ROOT }}/emulator
${{ env.ANDROID_SDK_ROOT }}/platform-tools
${{ env.ANDROID_SDK_ROOT }}/platforms/android-35
${{ env.ANDROID_SDK_ROOT }}/system-images/android-35/google_apis/x86_64
key: ${{ runner.os }}-mobilegl-avd-api35-google_apis-x86_64-pixel_6-v2-${{ hashFiles('android-plugin/run-avd-ci.sh') }}
- name: Create AVD
if: steps.android-avd-cache.outputs.cache-hit != 'true'
@@ -274,6 +276,8 @@ jobs:
env:
AVD_NAME: mobilegl-ci
ANDROID_AVD_HOME: ${{ github.workspace }}/.android/avd
ANDROID_HOME: ${{ github.workspace }}/.android/sdk
ANDROID_SDK_ROOT: ${{ github.workspace }}/.android/sdk
strategy:
fail-fast: false
max-parallel: 4
@@ -288,7 +292,7 @@ jobs:
- name: Set Swap Space
uses: pierotofy/set-swap-space@v1.0
with:
swap-size-gb: 16
swap-size-gb: 8
- name: Checkout repo
uses: actions/checkout@v6
@@ -324,11 +328,11 @@ jobs:
with:
path: |
${{ env.ANDROID_AVD_HOME }}
/usr/local/lib/android/sdk/emulator
/usr/local/lib/android/sdk/platform-tools
/usr/local/lib/android/sdk/platforms/android-35
/usr/local/lib/android/sdk/system-images/android-35/google_apis/x86_64
key: ${{ runner.os }}-mobilegl-avd-api35-google_apis-x86_64-pixel_6-v1-${{ hashFiles('android-plugin/run-avd-ci.sh') }}
${{ env.ANDROID_SDK_ROOT }}/emulator
${{ env.ANDROID_SDK_ROOT }}/platform-tools
${{ env.ANDROID_SDK_ROOT }}/platforms/android-35
${{ env.ANDROID_SDK_ROOT }}/system-images/android-35/google_apis/x86_64
key: ${{ runner.os }}-mobilegl-avd-api35-google_apis-x86_64-pixel_6-v2-${{ hashFiles('android-plugin/run-avd-ci.sh') }}
- name: Download retrace APK
uses: actions/download-artifact@v8
@@ -378,27 +382,51 @@ jobs:
if [ "${{ matrix.case.coherent_as_flush || false }}" = "true" ]; then
extra_retrace_args+=(--coherent-as-flush)
fi
timeout "$(( ${{ matrix.case.timeout_seconds }} + 300 ))" sh android-plugin/trace-replay-ci.sh \
--apk-file "${apk_file}" \
--package top.mobilegl.plugin.trace \
--backend "${{ matrix.backend.name }}" \
--result-root android-retrace-result \
--fixture-root android-retrace-fixture \
--case "${{ matrix.case.name }}" \
--trace-archive "${{ matrix.case.trace_archive }}" \
--trace-file "${{ matrix.case.trace_file }}" \
--golden "${{ matrix.case.golden }}" \
--alternate-golden "${{ matrix.case.alternate_golden || '' }}" \
--target-call "${{ matrix.case.target_call }}" \
--width "${{ matrix.case.width }}" \
--height "${{ matrix.case.height }}" \
--ssim-threshold "${{ matrix.case.ssim_threshold || '0.99' }}" \
--crop-x "${{ matrix.case.crop_x }}" \
--crop-y "${{ matrix.case.crop_y }}" \
--crop-width "${{ matrix.case.crop_width }}" \
--crop-height "${{ matrix.case.crop_height }}" \
--timeout-seconds "${{ matrix.case.timeout_seconds }}" \
"${extra_retrace_args[@]}"
run_retrace() {
timeout "$(( ${{ matrix.case.timeout_seconds }} + 300 ))" sh android-plugin/trace-replay-ci.sh \
--apk-file "${apk_file}" \
--package top.mobilegl.plugin.trace \
--backend "${{ matrix.backend.name }}" \
--result-root android-retrace-result \
--fixture-root android-retrace-fixture \
--case "${{ matrix.case.name }}" \
--trace-archive "${{ matrix.case.trace_archive }}" \
--trace-file "${{ matrix.case.trace_file }}" \
--golden "${{ matrix.case.golden }}" \
--alternate-golden "${{ matrix.case.alternate_golden || '' }}" \
--target-call "${{ matrix.case.target_call }}" \
--width "${{ matrix.case.width }}" \
--height "${{ matrix.case.height }}" \
--ssim-threshold "${{ matrix.case.ssim_threshold || '0.99' }}" \
--crop-x "${{ matrix.case.crop_x }}" \
--crop-y "${{ matrix.case.crop_y }}" \
--crop-width "${{ matrix.case.crop_width }}" \
--crop-height "${{ matrix.case.crop_height }}" \
--timeout-seconds "${{ matrix.case.timeout_seconds }}" \
"${extra_retrace_args[@]}"
}
retrace_status=0
run_retrace || retrace_status=$?
if [ "${retrace_status}" -eq 75 ]; then
echo "::warning::Android emulator infrastructure failed; restarting it and retrying this retrace once."
sh android-plugin/run-avd-ci.sh stop \
--avd-name "${AVD_NAME}" \
--emulator-log "${EMULATOR_LOG}" \
--pid-file "${EMULATOR_PID_FILE}"
adb kill-server || true
sleep 2
sh android-plugin/run-avd-ci.sh start \
--avd-name "${AVD_NAME}" \
--gpu "${{ matrix.backend.gpu }}" \
--emulator-log "${EMULATOR_LOG}" \
--pid-file "${EMULATOR_PID_FILE}" \
--boot-timeout 300
run_retrace
elif [ "${retrace_status}" -ne 0 ]; then
exit "${retrace_status}"
fi
- name: Collect retrace summary inputs
if: always()
+6 -5
View File
@@ -107,6 +107,7 @@ jobs:
--exclude='build.ninja' \
--exclude='cmake_install.cmake' \
-czf ci-artifacts/mobilegl-linux-runtime.tgz \
"${BUILD_DIR}/CTestTestfile.cmake" \
"${BUILD_DIR}/MobileGL/MG_Test" \
"${BUILD_DIR}/MobileGL/MG_Benchmark" \
"${SHARED_LIBS[@]}"
@@ -156,12 +157,12 @@ jobs:
PY
- name: Test
working-directory: build-linux/MobileGL/MG_Test
working-directory: build-linux
run: |
if [ "${{ secrets.ACTIONS_STEP_DEBUG }}" = "true" ]; then
ctest -V
ctest -V -L unit --no-tests=error
else
ctest --output-on-failure
ctest --output-on-failure -L unit --no-tests=error
fi
benchmark:
@@ -202,8 +203,8 @@ jobs:
PY
- name: Benchmark
working-directory: build-linux/MobileGL/MG_Benchmark
run: ctest -V -C Release
working-directory: build-linux
run: ctest -V -C Release -L benchmark --no-tests=error
build-retrace:
runs-on: ubuntu-latest
+10 -1
View File
@@ -190,6 +190,7 @@ set(SOURCE_FILES
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecomposeWorkgroupVec3Pass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp
MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
@@ -475,6 +476,15 @@ if (NOT ANDROID AND NOT MOBILEGL_IOS)
endif ()
if (NOT ANDROID)
# Enable testing in the top-level scope so a CTestTestfile.cmake is emitted
# at the build-tree root. This lets `ctest` be invoked from the top-level
# build directory (IDE "run all tests", CI) and discover every test in the
# subdirectories below, instead of having to descend into each
# MG_Test/MG_Benchmark subdirectory. Tests are tagged with CTest labels
# (unit / benchmark / integration), so e.g. `ctest -L unit` selects just
# the unit suite.
enable_testing()
if (MOBILEGL_BUILD_TEST)
add_subdirectory(MobileGL/MG_Test)
endif()
@@ -484,7 +494,6 @@ if (NOT ANDROID)
endif()
if (MOBILEGL_BUILD_TRACE_REPLAY)
enable_testing()
add_subdirectory(tools/trace_replay)
endif()
endif()
+6
View File
@@ -61,6 +61,12 @@ 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_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
// explicitly request a core profile via EGL_CONTEXT_OPENGL_PROFILE_MASK / a >=3.1
// version request.
Bool RelaxedSemantics = false;
};
extern FeaturesTable Features;
} // namespace MobileGL::MG_Config
+1
View File
@@ -122,6 +122,7 @@ 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.RelaxedSemantics = QueryEnvFlag("MOBILEGL_RELAXED_SEMANTICS");
}
inline void InitBackendType() {
+3
View File
@@ -232,6 +232,9 @@ namespace MobileGL {
struct DynamicBackendParameters {
SizeT UniformBufferOffsetAlignment = 256;
// GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT. 1.0 means the backend cannot filter anisotropically,
// which is also why the extension is not advertised in that case.
Float MaxTextureMaxAnisotropy = 1.0f;
Float AliasedLineWidthRangeMin = 1.0f;
Float AliasedLineWidthRangeMax = 1.0f;
Float SmoothLineWidthRangeMin = 1.0f;
@@ -605,9 +605,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
{
.TargetGLVersion = {3, 3, 0}, // Target OpenGL Version
.TargetGLSLVersion = {4, 6, 0}, // Target Shading Language Version
// Baseline advertisement (no timer queries yet); reconciled once
// the ES capabilities exist, see UpdateAdvertisedTimerQueryExtension.
.Extensions = BuildAdvertisedExtensions(false),
// Baseline advertisement (no timer queries / anisotropy yet); reconciled
// once the ES capabilities exist, see UpdateAdvertisedCapabilityExtensions.
.Extensions = BuildAdvertisedExtensions(false, false),
.IsCompatibilityProfile = false // Is Compatibility Profile
},
.StaticBackendCapability = {.AllowVSOnlyPrograms = false} // Backend Capability
@@ -627,8 +627,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
// thread can only observe the extension string after the
// advertisement for its context has settled; rebuilding the whole
// list keeps the re-run after a context recreation idempotent.
void UpdateAdvertisedTimerQueryExtension() {
MutableRendererInfo().RendererGLInfo.Extensions = BuildAdvertisedExtensions(AreTimerQueriesSupported());
void UpdateAdvertisedCapabilityExtensions(Bool anisotropicFilteringSupported) {
MutableRendererInfo().RendererGLInfo.Extensions =
BuildAdvertisedExtensions(AreTimerQueriesSupported(), anisotropicFilteringSupported);
}
} // namespace
@@ -672,11 +673,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
return false;
}
DirectGLES::SetGLESCapabilities(m_GLESCapabilities);
// Now that g_GLESCapabilities knows about GL_EXT_disjoint_timer_query,
// reconcile the E_GL_ARB_timer_query advertisement (see the comment on
// UpdateAdvertisedTimerQueryExtension for why it cannot happen when
// the extension list is first built).
UpdateAdvertisedTimerQueryExtension();
// Now that g_GLESCapabilities knows about GL_EXT_disjoint_timer_query and
// GL_EXT_texture_filter_anisotropic, reconcile the advertisement (see the comment on
// UpdateAdvertisedCapabilityExtensions for why it cannot happen when the extension
// list is first built).
UpdateAdvertisedCapabilityExtensions(m_GLESCapabilities.SupportsTextureFilterAnisotropy);
UpdateDynamicBackendParameters();
PopulateFormatCapabilities(m_GLESFunctions, m_GLESCapabilities, MutableFormatCapabilities());
PrintFormatCapabilities(GetFormatCapabilities());
@@ -818,7 +819,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
return MutableRendererInfo();
}
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported) {
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported) {
Vector<GLExtension> extensions = {V_OpenGL30, V_OpenGL31, V_OpenGL32,
V_OpenGL33, E_GL_ARB_draw_buffers_blend, E_GL_ARB_compute_shader,
E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
@@ -836,6 +837,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (timerQueriesSupported && !MG_Config::Features.DisableTimerQuery) {
extensions.push_back(E_GL_ARB_timer_query);
}
// Only advertised when the host ES driver actually filters anisotropically: the sampler
// state is accepted regardless, but forwarding it would be a no-op without the extension,
// and an app that trusts the string (LWJGL builds GLCapabilities from it) would silently
// get plain trilinear.
if (anisotropicFilteringSupported) {
extensions.push_back(E_GL_EXT_texture_filter_anisotropic);
extensions.push_back(E_GL_ARB_texture_filter_anisotropic);
}
return extensions;
}
@@ -940,6 +949,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
void BackendObject_DirectGLES::UpdateDynamicBackendParameters() {
m_dynamicParameters.UniformBufferOffsetAlignment = m_GLESCapabilities.UniformBufferOffsetAlignment;
m_dynamicParameters.MaxTextureMaxAnisotropy = m_GLESCapabilities.MaxTextureMaxAnisotropy;
m_dynamicParameters.AliasedLineWidthRangeMin = m_GLESCapabilities.AliasedLineWidthRangeMin;
m_dynamicParameters.AliasedLineWidthRangeMax = m_GLESCapabilities.AliasedLineWidthRangeMax;
m_dynamicParameters.SmoothLineWidthRangeMin = m_GLESCapabilities.SmoothLineWidthRangeMin;
@@ -66,9 +66,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
const RendererInfo& GetRendererIdentity();
// The full OpenGL extension list Espryt advertises (glGetString(GL_EXTENSIONS))
// for a device whose timer queries are (or are not) usable. The
// MOBILEGL_DISABLE_TIMERQUERY escape hatch is applied inside.
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported);
// for a device whose timer queries / anisotropic filtering are (or are not) usable.
// The MOBILEGL_DISABLE_TIMERQUERY escape hatch is applied inside.
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported);
// Format: <OpenGL ES Renderer>, OpenGL ES <Major>.<Minor> — the exact string an
// initialized backend returns from GetBackendAPIVersionString (and that ends up
File diff suppressed because it is too large Load Diff
@@ -19,6 +19,10 @@
operation Utils::CheckGLESError();
namespace MobileGL::MG_Backend::DirectGLES {
// Re-establishes the frontend texture-unit bindings on the native ES context.
// Content uploads use scratch bindings, so draws and dispatches call this after
// texture synchronization.
void BindCurrentTextures();
void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value);
void ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value);
+125 -49
View File
@@ -1405,7 +1405,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
m_prevSkipPixels = s_skipPixels;
m_prevImageHeight = s_imageHeight;
m_prevSkipImages = s_skipImages;
Apply(4, 0, 0, 0, 0, 0);
// Shadow mip data is tightly packed (ProcessTexturePixelsDataUnpack emits
// width * bpp rows with no padding), so uploads must use UNPACK_ALIGNMENT = 1.
// Alignment 4 made the driver read e.g. 7-byte R8 rows at an 8-byte stride,
// shifting every row of a non-multiple-of-4 upload by one pixel.
Apply(1, 0, 0, 0, 0, 0);
}
~ScopedDefaultUnpackState() {
@@ -1472,10 +1476,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
return 2;
case TextureInternalFormat::RGB8Snorm:
case TextureInternalFormat::RGB16:
case TextureInternalFormat::RGB10: // stored as RGB16 (UNorm16 shadow)
case TextureInternalFormat::RGB12: // stored as RGB16 (UNorm16 shadow)
case TextureInternalFormat::RGB16Snorm:
return 3;
case TextureInternalFormat::RGBA8Snorm:
case TextureInternalFormat::RGBA16:
case TextureInternalFormat::RGBA12: // stored as RGBA16 (UNorm16 shadow)
case TextureInternalFormat::RGBA16Snorm:
return 4;
default:
@@ -1570,7 +1577,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
MGLOG_D("Syncing texture mipmaps with backend ID %u to backend for state ID %u", m_backendTextureId,
stateTextureObject->GetExternalIndex());
GLenum target = MG_Util::ConvertTextureTargetToGLEnum(stateTextureObject->GetTarget());
GLenum target = ConvertTextureTargetToBackendGLEnum(stateTextureObject->GetTarget());
auto targetInternal = stateTextureObject->GetTarget();
MGLOG_D(" Texture target for syncing is %s",
MG_Util::ConvertTextureTargetToString(targetInternal).c_str());
@@ -1691,7 +1698,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
auto levelTexelSize = textureMipmapObject->GetMipmapTexelSize(uploadTarget, level);
auto levelByteSize = textureMipmapObject->GetMipmapByteSize(uploadTarget, level);
bool levelDirty = textureMipmapObject->IsStorageDirty(uploadTarget, level);
auto glUploadTarget = MG_Util::ConvertTextureUploadTargetToGLEnum(uploadTarget);
auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget);
auto* pData = (levelDirty && levelByteSize != 0)
? textureMipmapObject->MapMipmapData(uploadTarget, level)
: nullptr;
@@ -1702,19 +1709,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
DebugImpl::ErrorLopper::Clear();
g_GLESFuncs.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
switch (stateTextureObject->GetTarget()) {
const IntVec3 uploadSize =
GetBackendUploadSize(stateTextureObject->GetTarget(), levelTexelSize);
switch (MapToBackendTextureTarget(stateTextureObject->GetTarget())) {
case TextureTarget::Texture2D:
case TextureTarget::TextureCubeMap:
g_GLESFuncs.glTexImage2D(
glUploadTarget, static_cast<GLint>(level), (GLint)glInternalFormat,
static_cast<GLsizei>(levelTexelSize.x()), static_cast<GLsizei>(levelTexelSize.y()),
static_cast<GLsizei>(uploadSize.x()), static_cast<GLsizei>(uploadSize.y()),
0, glFormat, glType, uploadData);
break;
case TextureTarget::Texture3D:
case TextureTarget::Texture2DArray:
g_GLESFuncs.glTexImage3D(
glUploadTarget, static_cast<GLint>(level), (GLint)glInternalFormat,
static_cast<GLsizei>(levelTexelSize.x()), static_cast<GLsizei>(levelTexelSize.y()),
static_cast<GLsizei>(levelTexelSize.z()), 0, glFormat, glType, uploadData);
static_cast<GLsizei>(uploadSize.x()), static_cast<GLsizei>(uploadSize.y()),
static_cast<GLsizei>(uploadSize.z()), 0, glFormat, glType, uploadData);
break;
default:
MGLOG_E("Unhandled texture target %s",
@@ -1778,18 +1788,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
} else if (stateTextureObject->IsImmutable() || m_imageBindableStorageRequired) {
DebugImpl::ErrorLopper::Clear();
g_GLESFuncs.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
switch (targetInternal) {
const IntVec3 storageSize = GetBackendUploadSize(targetInternal, baseSize);
switch (MapToBackendTextureTarget(targetInternal)) {
case TextureTarget::Texture2D:
case TextureTarget::TextureCubeMap:
g_GLESFuncs.glTexStorage2D(target, static_cast<GLsizei>(mipmapCount), glInternalFormat,
static_cast<GLsizei>(baseSize.x()),
static_cast<GLsizei>(baseSize.y()));
static_cast<GLsizei>(storageSize.x()),
static_cast<GLsizei>(storageSize.y()));
break;
case TextureTarget::Texture3D:
case TextureTarget::Texture2DArray:
g_GLESFuncs.glTexStorage3D(target, static_cast<GLsizei>(mipmapCount), glInternalFormat,
static_cast<GLsizei>(baseSize.x()),
static_cast<GLsizei>(baseSize.y()),
static_cast<GLsizei>(baseSize.z()));
static_cast<GLsizei>(storageSize.x()),
static_cast<GLsizei>(storageSize.y()),
static_cast<GLsizei>(storageSize.z()));
break;
default:
MGLOG_E("Unhandled immutable texture target %s",
@@ -1813,7 +1825,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (levelDirty && levelByteSize != 0) {
auto levelTexelSize =
textureMipmapObject->GetMipmapTexelSize(uploadTarget, level);
auto glUploadTarget = MG_Util::ConvertTextureUploadTargetToGLEnum(uploadTarget);
auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget);
auto* pData = textureMipmapObject->MapMipmapData(uploadTarget, level);
Vector<Float> convertedUploadData;
const void* uploadData = PrepareNormFloatFallbackUpload(
@@ -1822,20 +1834,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
DebugImpl::ErrorLopper::Clear();
g_GLESFuncs.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
switch (targetInternal) {
const IntVec3 uploadSize =
GetBackendUploadSize(targetInternal, levelTexelSize);
switch (MapToBackendTextureTarget(targetInternal)) {
case TextureTarget::Texture2D:
case TextureTarget::TextureCubeMap:
g_GLESFuncs.glTexSubImage2D(
glUploadTarget, static_cast<GLint>(level), 0, 0,
static_cast<GLsizei>(levelTexelSize.x()),
static_cast<GLsizei>(levelTexelSize.y()), glFormat, glType, uploadData);
static_cast<GLsizei>(uploadSize.x()),
static_cast<GLsizei>(uploadSize.y()), glFormat, glType, uploadData);
break;
case TextureTarget::Texture3D:
case TextureTarget::Texture2DArray:
g_GLESFuncs.glTexSubImage3D(
glUploadTarget, static_cast<GLint>(level), 0, 0, 0,
static_cast<GLsizei>(levelTexelSize.x()),
static_cast<GLsizei>(levelTexelSize.y()),
static_cast<GLsizei>(levelTexelSize.z()), glFormat, glType, uploadData);
static_cast<GLsizei>(uploadSize.x()),
static_cast<GLsizei>(uploadSize.y()),
static_cast<GLsizei>(uploadSize.z()), glFormat, glType, uploadData);
break;
default:
break;
@@ -1862,7 +1877,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
auto levelTexelSize = textureMipmapObject->GetMipmapTexelSize(uploadTarget, level);
auto levelByteSize = textureMipmapObject->GetMipmapByteSize(uploadTarget, level);
bool levelDirty = textureMipmapObject->IsStorageDirty(uploadTarget, level);
auto glUploadTarget = MG_Util::ConvertTextureUploadTargetToGLEnum(uploadTarget);
auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget);
auto* pData = (levelDirty && levelByteSize != 0)
? textureMipmapObject->MapMipmapData(uploadTarget, level)
: nullptr;
@@ -1879,22 +1894,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
DebugImpl::ErrorLopper::Clear();
g_GLESFuncs.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
auto textureTarget = stateTextureObject->GetTarget();
// TODO: handle more texture types
switch (textureTarget) {
const IntVec3 uploadSize = GetBackendUploadSize(textureTarget, levelTexelSize);
switch (MapToBackendTextureTarget(textureTarget)) {
case TextureTarget::Texture2D:
case TextureTarget::TextureCubeMap: {
g_GLESFuncs.glTexImage2D(
glUploadTarget, static_cast<GLint>(level), (GLint)glInternalFormat,
static_cast<GLsizei>(levelTexelSize.x()),
static_cast<GLsizei>(levelTexelSize.y()), 0, glFormat, glType, uploadData);
static_cast<GLsizei>(uploadSize.x()),
static_cast<GLsizei>(uploadSize.y()), 0, glFormat, glType, uploadData);
break;
}
case TextureTarget::Texture3D: {
case TextureTarget::Texture3D:
case TextureTarget::Texture2DArray: {
g_GLESFuncs.glTexImage3D(
glUploadTarget, static_cast<GLint>(level), (GLint)glInternalFormat,
static_cast<GLsizei>(levelTexelSize.x()),
static_cast<GLsizei>(levelTexelSize.y()),
static_cast<GLsizei>(levelTexelSize.z()), 0, glFormat, glType, uploadData);
static_cast<GLsizei>(uploadSize.x()),
static_cast<GLsizei>(uploadSize.y()),
static_cast<GLsizei>(uploadSize.z()), 0, glFormat, glType, uploadData);
break;
}
default: {
@@ -1961,7 +1977,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
textureMipmapObject->GetMipmapTexelSize(uploadTarget, level).x(),
textureMipmapObject->GetMipmapTexelSize(uploadTarget, level).y(), byteSize);
auto glUploadTarget = MG_Util::ConvertTextureUploadTargetToGLEnum(uploadTarget);
auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget);
g_GLESFuncs.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
DebugImpl::ErrorLopper::Loop(
[file = __FILE__, line = __LINE__, func = __func__](GLenum err) {
@@ -1974,19 +1990,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
const void* uploadData = PrepareNormFloatFallbackUpload(
textureMipmapObject->GetFormat(), texelSize, mipData, byteSize, glType,
convertedUploadData);
switch (stateTextureObject->GetTarget()) {
const IntVec3 uploadSize =
GetBackendUploadSize(stateTextureObject->GetTarget(), texelSize);
switch (MapToBackendTextureTarget(stateTextureObject->GetTarget())) {
case TextureTarget::Texture2D:
case TextureTarget::TextureCubeMap:
g_GLESFuncs.glTexSubImage2D(glUploadTarget, static_cast<GLint>(level), 0, 0,
static_cast<GLsizei>(texelSize.x()),
static_cast<GLsizei>(texelSize.y()), glFormat, glType,
static_cast<GLsizei>(uploadSize.x()),
static_cast<GLsizei>(uploadSize.y()), glFormat, glType,
uploadData);
break;
case TextureTarget::Texture3D:
case TextureTarget::Texture2DArray:
g_GLESFuncs.glTexSubImage3D(glUploadTarget, static_cast<GLint>(level), 0, 0, 0,
static_cast<GLsizei>(texelSize.x()),
static_cast<GLsizei>(texelSize.y()),
static_cast<GLsizei>(texelSize.z()), glFormat, glType,
static_cast<GLsizei>(uploadSize.x()),
static_cast<GLsizei>(uploadSize.y()),
static_cast<GLsizei>(uploadSize.z()), glFormat, glType,
uploadData);
break;
default:
@@ -2079,7 +2098,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
MGLOG_D("Syncing texture built-in sampler with backend ID %u to backend for state ID %u",
m_backendTextureId, stateTextureObject->GetExternalIndex());
GLenum target = MG_Util::ConvertTextureTargetToGLEnum(stateTextureObject->GetTarget());
GLenum target = ConvertTextureTargetToBackendGLEnum(stateTextureObject->GetTarget());
auto targetInternal = stateTextureObject->GetTarget();
MGLOG_D(" Texture target for syncing is %s",
MG_Util::ConvertTextureTargetToString(targetInternal).c_str());
@@ -2150,6 +2169,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_GLESFuncs.glTexParameterf(target, GL_TEXTURE_MAX_LOD, samplerParams.maxLod);
m_cacheSamplerParameters.maxLod = samplerParams.maxLod;
}
if (m_cacheSamplerParameters.maxAnisotropy != samplerParams.maxAnisotropy) {
if (g_GLESCapabilities.SupportsTextureFilterAnisotropy) {
g_GLESFuncs.glTexParameterf(target, GL_TEXTURE_MAX_ANISOTROPY_EXT,
samplerParams.maxAnisotropy);
}
// Unsupported GLES backends intentionally treat anisotropy as a
// frontend-only no-op; remember the observed value so the cache
// remains coherent without issuing an illegal enum every sync.
m_cacheSamplerParameters.maxAnisotropy = samplerParams.maxAnisotropy;
}
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) {
MGLOG_D("%s(%s:%d) ES error %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str());
});
@@ -2177,7 +2206,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
MGLOG_D("Syncing texture params with backend ID %u to backend for state ID %u", m_backendTextureId,
stateTextureObject->GetExternalIndex());
GLenum target = MG_Util::ConvertTextureTargetToGLEnum(stateTextureObject->GetTarget());
GLenum target = ConvertTextureTargetToBackendGLEnum(stateTextureObject->GetTarget());
auto targetInternal = stateTextureObject->GetTarget();
MGLOG_D(" Texture target for syncing is %s",
MG_Util::ConvertTextureTargetToString(targetInternal).c_str());
@@ -2256,14 +2285,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_activeTextureUnit = unit;
}
void UnbindTexture(Uint unit, GLenum target) { // Active unit will be modified
if (unit != g_activeTextureUnit) {
ActivateTextureUnit(unit);
}
void UnbindTexture(Uint unit, GLenum target) { // Activates `unit` when an unbind is issued
auto targetN = static_cast<SizeT>(MG_Util::ConvertGLEnumToTextureTarget(target));
if (g_boundTexturesCache[unit][targetN] == nullptr) return;
ActivateTextureUnit(unit);
g_GLESFuncs.glBindTexture(target, 0);
g_boundTexturesCache[unit][targetN] = nullptr;
}
@@ -2334,11 +2360,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_GLESFuncs.glFramebufferTexture(glFBOTarget, glBackendAttachment,
backendTextureObject->GetBackendTextureId(),
static_cast<GLint>(attachmentObject.GetTextureLevel()));
} else if (const auto uploadTarget = attachmentObject.GetTextureUploadTarget();
uploadTarget == TextureUploadTarget::Texture3D ||
uploadTarget == TextureUploadTarget::Texture2DArray ||
uploadTarget == TextureUploadTarget::Texture2DMultisampleArray) {
// Single slice/layer of a 3D or array texture: ES has no
// glFramebufferTexture3D, layers attach via glFramebufferTextureLayer.
g_GLESFuncs.glFramebufferTextureLayer(glFBOTarget, glBackendAttachment,
backendTextureObject->GetBackendTextureId(),
static_cast<GLint>(attachmentObject.GetTextureLevel()),
static_cast<GLint>(attachmentObject.GetTextureLayer()));
} else {
auto glTextureTarget =
MG_Util::ConvertTextureUploadTargetToGLEnum(attachmentObject.GetTextureUploadTarget());
auto glTextureTarget = TextureImpl::ConvertTextureUploadTargetToBackendGLEnum(
attachmentObject.GetTextureUploadTarget());
if (glTextureTarget == GL_UNKNOWN_MGL) {
glTextureTarget = MG_Util::ConvertTextureTargetToGLEnum(textureObject->GetTarget());
glTextureTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(textureObject->GetTarget());
}
backendTextureObject->Bind(glTextureTarget);
g_GLESFuncs.glFramebufferTexture2D(glFBOTarget, glBackendAttachment, glTextureTarget,
@@ -2452,7 +2488,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
drawBufferClean = true;
}
if (!drawBufferClean) {
// glDrawBuffers writes the state of the FBO bound to GL_DRAW_FRAMEBUFFER.
// When this object is only bound as the READ target the call would land on
// whatever framebuffer is draw-bound AND falsely stamp this object's memo,
// so the later draw-target sync skips as "clean" while the real state is
// stale (Minecraft 26.x OIT: the scratch clear-FBO kept draw buffers NONE
// from its blit-destination configuration, silently dropping every
// offscreen color clear).
if (!drawBufferClean && asTarget == FramebufferTarget::Draw) {
memcpy(m_frontendDrawBuffers, stateDrawBuffers.data(),
FramebufferObject::MAX_DRAW_BUFFERS * sizeof(FramebufferAttachmentType));
std::fill(m_backendDrawBuffers, m_backendDrawBuffers + FramebufferObject::MAX_DRAW_BUFFERS, GL_NONE);
@@ -2477,6 +2520,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
nEffectiveBuffers = i + 1;
}
g_GLESFuncs.glDrawBuffers(nEffectiveBuffers, m_backendDrawBuffers);
MGLOG_D("DBAPPLY beFbo=%u target=%d n=%d db0=0x%x feDb0=%d", m_backendFBOId, (int)asTarget,
nEffectiveBuffers, m_backendDrawBuffers[0], (int)stateDrawBuffers[0]);
}
if (asTarget == FramebufferTarget::Draw) {
@@ -2499,9 +2544,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
PrgramImpl::g_unormFallbackClampOutputMask = unormClampOutputMask;
}
// 2. Remap read buffer
// 2. Remap read buffer. glReadBuffer writes the READ-bound FBO's state, so
// only apply (and stamp the memo) when this object is bound as READ.
auto frontendReadBuf = stateFBOObject->GetReadBuffer();
if (frontendReadBuf != m_frontendReadBuffer) {
if (frontendReadBuf != m_frontendReadBuffer && asTarget == FramebufferTarget::Read) {
m_frontendReadBuffer = frontendReadBuf;
GLenum glBackendReadBuffer = GetBackendAttachmentType(frontendReadBuf);
@@ -2608,6 +2654,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
StateBackendObjectRegistry<MG_State::GLState::FramebufferObject, BackendFramebufferObject>
g_backendFramebufferObjects;
Array<Uint16, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fboBindVersions = {0};
// Tracks the bound FBO's object version (bumped on any attachment/drawbuffer change)
// per target: re-attaching textures or changing draw buffers on an already-bound FBO
// must re-sync it even when the binding-slot version has not moved.
Array<Uint16, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fboSyncedObjectVersions = {0};
Array<MG_State::GLState::FramebufferObject*, SizeT(FramebufferTarget::FramebufferTargetCount)>
g_fboSyncedObjects = {};
} // namespace FramebufferImpl
namespace PrgramImpl {
@@ -2718,6 +2770,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
effectiveSpirv = &loweredSpirv;
}
// ESSL stage-matches uniform blocks by member precision, but SPIRV-Cross prints
// a RelaxedPrecision member as explicit "mediump" in the vertex stage and as
// UNQUALIFIED (mediump-by-default) in the fragment stage; after
// ForceSupporterOutput swaps the fragment header to highp, that member reads
// back as highp and the ES driver refuses to link ("definitions of uniform
// block ... do not match"). Strip the hint from block structs so both stages
// declare the member highp; nothing else about emission changes.
Vector<unsigned int> uboPrecisionSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::StripUboMemberRelaxedPrecisionForEssl(
*effectiveSpirv, uboPrecisionSpirv) &&
!uboPrecisionSpirv.empty()) {
effectiveSpirv = &uboPrecisionSpirv;
}
MG_Util::ShaderTranspiler::SpvcSession spvcSession(*effectiveSpirv,
MG_Util::ShaderTranspiler::SessionUsageBit::Transpile);
@@ -2882,6 +2948,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
m_uniformBlockBackendIndices[static_cast<SizeT>(i)] = static_cast<Int>(backendBlkIdx);
g_GLESFuncs.glUniformBlockBinding(m_backendProgramId, backendBlkIdx, lastUBOBinding);
MGLOG_D("CACHE prog=%u beProg=%u blk[%d]='%s' beIdx=%u -> bePoint=%u",
stateProgramObject->GetExternalIndex(), m_backendProgramId, i, name.c_str(), backendBlkIdx,
lastUBOBinding);
}
m_samplerUniformBindings.clear();
@@ -3017,6 +3086,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_GLESFuncs.glSamplerParameterf(m_backendSamplerId, GL_TEXTURE_MAX_LOD, samplerParams.maxLod);
m_cacheSamplerParameters.maxLod = samplerParams.maxLod;
}
if (m_cacheSamplerParameters.maxAnisotropy != samplerParams.maxAnisotropy) {
if (g_GLESCapabilities.SupportsTextureFilterAnisotropy) {
g_GLESFuncs.glSamplerParameterf(m_backendSamplerId, GL_TEXTURE_MAX_ANISOTROPY_EXT,
samplerParams.maxAnisotropy);
}
m_cacheSamplerParameters.maxAnisotropy = samplerParams.maxAnisotropy;
}
#undef SYNC_SAMPLER_PARAM_IF_CHANGED
m_isInitialized = true;
}
+49 -4
View File
@@ -15,6 +15,7 @@
#include "MG_State/GLState/TextureState/TextureEnum.h"
#include <MG_State/GLState/TextureState/TextureObject.h>
#include <MG_State/GLState/Core.h>
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
namespace MobileGL::MG_Backend::DirectGLES {
String EmulateBaseInstanceInVertexShader(String source, GLenum shaderType);
@@ -254,10 +255,48 @@ namespace MobileGL::MG_Backend::DirectGLES {
namespace TextureImpl {
inline Bool IsSupportedTextureTarget(TextureTarget target) {
if (target == TextureTarget::Texture1D || target == TextureTarget::TextureRectangle ||
target == TextureTarget::Texture1DArray || target == TextureTarget::Texture2DArray)
return false;
return true;
// Rectangle textures need non-normalized sampling ES cannot express; everything else is
// either native or emulated (1D -> 2D with height 1, 1D array -> 2D array, see
// MapToBackendTextureTarget). SPIRV-Cross already emits the matching ESSL samplers and
// coordinate padding for 1D/1D-array shaders.
return target != TextureTarget::TextureRectangle;
}
// ES has no 1D targets: 1D textures are stored as 2D (height 1) and 1D arrays as 2D arrays
// (height 1, layers in depth). Must match SPIRV-Cross's ES 1D-as-2D shader emulation.
inline TextureTarget MapToBackendTextureTarget(TextureTarget target) {
switch (target) {
case TextureTarget::Texture1D:
return TextureTarget::Texture2D;
case TextureTarget::Texture1DArray:
return TextureTarget::Texture2DArray;
default:
return target;
}
}
inline GLenum ConvertTextureTargetToBackendGLEnum(TextureTarget target) {
return MG_Util::ConvertTextureTargetToGLEnum(MapToBackendTextureTarget(target));
}
inline GLenum ConvertTextureUploadTargetToBackendGLEnum(TextureUploadTarget uploadTarget) {
switch (uploadTarget) {
case TextureUploadTarget::Texture1D:
return GL_TEXTURE_2D;
case TextureUploadTarget::Texture1DArray:
return GL_TEXTURE_2D_ARRAY;
default:
return MG_Util::ConvertTextureUploadTargetToGLEnum(uploadTarget);
}
}
// 1D arrays store layers in the state-side height; the ES 2D-array image keeps height 1 and
// moves the layer count into depth.
inline IntVec3 GetBackendUploadSize(TextureTarget stateTarget, const IntVec3& texelSize) {
if (stateTarget == TextureTarget::Texture1DArray) {
return {texelSize.x(), 1, texelSize.y()};
}
return texelSize;
}
inline Bool IsMultisampleTextureTarget(TextureTarget target) {
@@ -369,6 +408,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
extern StateBackendObjectRegistry<MG_State::GLState::FramebufferObject, BackendFramebufferObject>
g_backendFramebufferObjects;
extern Array<Uint16, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fboBindVersions;
// Tracks the bound FBO's object version (bumped on any attachment/drawbuffer change)
// per target: re-attaching textures or changing draw buffers on an already-bound FBO
// must re-sync it even when the binding-slot version has not moved.
extern Array<Uint16, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fboSyncedObjectVersions;
extern Array<MG_State::GLState::FramebufferObject*, SizeT(FramebufferTarget::FramebufferTargetCount)>
g_fboSyncedObjects;
} // namespace FramebufferImpl
// Image uniforms take their unit from the layout(binding=N) qualifier baked into
+317
View File
@@ -18,6 +18,10 @@
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
#include <MG_Util/Converters/MGToGL/FramebufferEnumConverter.h>
#include <MG_Util/Math/HalfFloat.h>
#include <MG_Util/Math/SmallFloat.h>
#include <cmath>
namespace MobileGL::MG_Backend::DirectGLES {
namespace {
@@ -448,4 +452,317 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
}
} // namespace Utils
// ---- Client-format readback conversion helpers -------------------------------------------------
// ReadPixels/GetTexImage read a guaranteed wide RGBA(_INTEGER) layout from the ES driver and repack
// it on the CPU into the client's (format, type) layout. Everything here is pure byte shuffling so
// unit tests can assert the exact packed words; field positions follow GL 3.3 table 3.6 and mirror
// the GL CTS packed_pixels oracle (glcPackedPixelsTests.cpp pack_UNSIGNED_* helpers).
namespace ReadbackImpl {
using MG_Util::DecodeHalfBitsToFloat;
using MG_Util::EncodeFloatToHalfBits;
Bool GetReadbackChannelMapping(GLenum format, ReadbackChannelMapping& outMapping) {
switch (format) {
case GL_RED: outMapping = {{0, 0, 0, 0}, 1, false}; return true;
case GL_RED_INTEGER: outMapping = {{0, 0, 0, 0}, 1, true}; return true;
// Desktop-GL single-channel client formats (GL CTS packed_pixels rgba8_format_green/blue):
// the destination holds one component sourced from the named channel of the wide RGBA read.
// GL_ALPHA is mapped here from the raw enum because the state layer folds it into Red for the
// legacy alpha-texture upload hack.
case GL_GREEN: outMapping = {{1, 0, 0, 0}, 1, false}; return true;
case GL_GREEN_INTEGER: outMapping = {{1, 0, 0, 0}, 1, true}; return true;
case GL_BLUE: outMapping = {{2, 0, 0, 0}, 1, false}; return true;
case GL_BLUE_INTEGER: outMapping = {{2, 0, 0, 0}, 1, true}; return true;
case GL_ALPHA: outMapping = {{3, 0, 0, 0}, 1, false}; return true;
case GL_ALPHA_INTEGER: outMapping = {{3, 0, 0, 0}, 1, true}; return true;
case GL_RG: outMapping = {{0, 1, 0, 0}, 2, false}; return true;
case GL_RG_INTEGER: outMapping = {{0, 1, 0, 0}, 2, true}; return true;
case GL_RGB: outMapping = {{0, 1, 2, 0}, 3, false}; return true;
case GL_RGB_INTEGER: outMapping = {{0, 1, 2, 0}, 3, true}; return true;
case GL_BGR: outMapping = {{2, 1, 0, 0}, 3, false}; return true;
case GL_BGR_INTEGER: outMapping = {{2, 1, 0, 0}, 3, true}; return true;
case GL_RGBA: outMapping = {{0, 1, 2, 3}, 4, false}; return true;
case GL_RGBA_INTEGER: outMapping = {{0, 1, 2, 3}, 4, true}; return true;
case GL_BGRA: outMapping = {{2, 1, 0, 3}, 4, false}; return true;
case GL_BGRA_INTEGER: outMapping = {{2, 1, 0, 3}, 4, true}; return true;
default:
return false;
}
}
Bool GetPackedReadbackLayout(GLenum type, PackedReadbackLayout& out) {
switch (type) {
// Non-REV types pack the first format component starting at the most significant bit,
// *_REV types starting at the least significant bit (GL CTS pack_UNSIGNED_SHORT_5_6_5:
// R bits 15-11; pack_UNSIGNED_SHORT_1_5_5_5_REV: R bits 4-0, A bit 15).
case GL_UNSIGNED_BYTE_3_3_2: out = {3, {3, 3, 2, 0}, {5, 2, 0, 0}, 1, false}; return true;
case GL_UNSIGNED_BYTE_2_3_3_REV: out = {3, {3, 3, 2, 0}, {0, 3, 6, 0}, 1, false}; return true;
case GL_UNSIGNED_SHORT_5_6_5: out = {3, {5, 6, 5, 0}, {11, 5, 0, 0}, 2, false}; return true;
case GL_UNSIGNED_SHORT_5_6_5_REV: out = {3, {5, 6, 5, 0}, {0, 5, 11, 0}, 2, false}; return true;
case GL_UNSIGNED_SHORT_4_4_4_4: out = {4, {4, 4, 4, 4}, {12, 8, 4, 0}, 2, false}; return true;
case GL_UNSIGNED_SHORT_4_4_4_4_REV: out = {4, {4, 4, 4, 4}, {0, 4, 8, 12}, 2, false}; return true;
case GL_UNSIGNED_SHORT_5_5_5_1: out = {4, {5, 5, 5, 1}, {11, 6, 1, 0}, 2, false}; return true;
case GL_UNSIGNED_SHORT_1_5_5_5_REV: out = {4, {5, 5, 5, 1}, {0, 5, 10, 15}, 2, false}; return true;
case GL_UNSIGNED_INT_8_8_8_8: out = {4, {8, 8, 8, 8}, {24, 16, 8, 0}, 4, false}; return true;
case GL_UNSIGNED_INT_8_8_8_8_REV: out = {4, {8, 8, 8, 8}, {0, 8, 16, 24}, 4, false}; return true;
case GL_UNSIGNED_INT_10_10_10_2: out = {4, {10, 10, 10, 2}, {22, 12, 2, 0}, 4, false}; return true;
case GL_UNSIGNED_INT_2_10_10_10_REV: out = {4, {10, 10, 10, 2}, {0, 10, 20, 30}, 4, false}; return true;
// Packed-float RGB types: fields hold unsigned small floats; 5_9_9_9_REV's shared 5-bit
// exponent (bits 31-27) is emitted by EncodeSharedExponentRGB9E5, not a component field.
case GL_UNSIGNED_INT_10F_11F_11F_REV: out = {3, {11, 11, 10, 0}, {0, 11, 22, 0}, 4, true}; return true;
case GL_UNSIGNED_INT_5_9_9_9_REV: out = {3, {9, 9, 9, 0}, {0, 9, 18, 0}, 4, true}; return true;
default:
return false;
}
}
SizeT GetReadbackComponentSize(GLenum type) {
PackedReadbackLayout packedLayout{};
if (GetPackedReadbackLayout(type, packedLayout)) {
return packedLayout.byteSize;
}
switch (type) {
case GL_UNSIGNED_BYTE:
case GL_BYTE:
return 1;
case GL_UNSIGNED_SHORT:
case GL_SHORT:
case GL_HALF_FLOAT:
return 2;
case GL_UNSIGNED_INT:
case GL_INT:
case GL_FLOAT:
return 4;
default:
return 0;
}
}
SizeT GetReadbackDstPixelSize(const ReadbackChannelMapping& mapping, GLenum type) {
PackedReadbackLayout packedLayout{};
if (GetPackedReadbackLayout(type, packedLayout)) {
if (packedLayout.fieldCount != mapping.channelCount) {
return 0; // 3-field packed types pair with 3-component formats only, 4 with 4
}
if (mapping.isInteger && packedLayout.isFloatPacked) {
return 0; // packed-float RGB types never pair with integer formats
}
return packedLayout.byteSize;
}
if (mapping.isInteger && (type == GL_FLOAT || type == GL_HALF_FLOAT)) {
return 0;
}
const SizeT componentSize = GetReadbackComponentSize(type);
return componentSize == 0 ? 0 : static_cast<SizeT>(mapping.channelCount) * componentSize;
}
namespace {
void WritePackedReadbackWord(Uint8* dst, Uint32 word, SizeT byteSize) {
switch (byteSize) {
case 1: {
const auto out = static_cast<Uint8>(word);
Memcpy(dst, &out, sizeof(out));
break;
}
case 2: {
const auto out = static_cast<Uint16>(word);
Memcpy(dst, &out, sizeof(out));
break;
}
default:
Memcpy(dst, &word, sizeof(word));
break;
}
}
} // namespace
// Shared encoders live in MG_Util/Math/SmallFloat.h so the upload conversion
// (PixelStoreProcessor) uses byte-identical packing; kept exported here for unit tests.
Uint32 EncodeFloatToUnsignedF11(Float value) { return MG_Util::EncodeFloatToUnsignedF11(value); }
Uint32 EncodeFloatToUnsignedF10(Float value) { return MG_Util::EncodeFloatToUnsignedF10(value); }
Uint32 EncodeSharedExponentRGB9E5(const Float rgb[3]) { return MG_Util::EncodeSharedExponentRGB9E5(rgb); }
void ConvertWideReadbackRow(const Uint8* src, Uint8* dst, SizeT width, GLenum wideType,
const ReadbackChannelMapping& mapping, GLenum type) {
PackedReadbackLayout packedLayout{};
const Bool isPacked = GetPackedReadbackLayout(type, packedLayout);
const SizeT dstComponentSize = GetReadbackComponentSize(type);
const SizeT dstPixelBytes = GetReadbackDstPixelSize(mapping, type);
const SizeT srcPixelBytes = 4 * GetReadbackComponentSize(wideType);
for (SizeT col = 0; col < width; ++col) {
const Uint8* srcPixel = src + col * srcPixelBytes;
Uint8* dstPixel = dst + col * dstPixelBytes;
if (mapping.isInteger) {
Int64 srcValues[4];
for (Int c = 0; c < 4; ++c) {
srcValues[c] = wideType == GL_INT
? static_cast<Int64>(reinterpret_cast<const Int32*>(srcPixel)[c])
: static_cast<Int64>(reinterpret_cast<const Uint32*>(srcPixel)[c]);
}
if (isPacked) {
// Integer sources clamp each component to the unsigned range of its field
// (GL 3.3 section 4.3.1 final conversion).
Uint32 word = 0;
for (Int ch = 0; ch < packedLayout.fieldCount; ++ch) {
const Int64 fieldMax = (Int64{1} << packedLayout.width[ch]) - 1;
const auto v = static_cast<Uint32>(
std::clamp<Int64>(srcValues[mapping.sourceChannel[ch]], 0, fieldMax));
word |= v << packedLayout.shift[ch];
}
WritePackedReadbackWord(dstPixel, word, packedLayout.byteSize);
} else {
for (Int ch = 0; ch < mapping.channelCount; ++ch) {
const Int64 v = srcValues[mapping.sourceChannel[ch]];
Uint8* dstComponent = dstPixel + static_cast<SizeT>(ch) * dstComponentSize;
switch (type) {
case GL_UNSIGNED_BYTE:
*dstComponent = static_cast<Uint8>(std::clamp<Int64>(v, 0, 255));
break;
case GL_BYTE: {
const auto out = static_cast<Int8>(std::clamp<Int64>(v, -128, 127));
Memcpy(dstComponent, &out, sizeof(out));
break;
}
case GL_UNSIGNED_SHORT: {
const auto out = static_cast<Uint16>(std::clamp<Int64>(v, 0, 65535));
Memcpy(dstComponent, &out, sizeof(out));
break;
}
case GL_SHORT: {
const auto out = static_cast<Int16>(std::clamp<Int64>(v, -32768, 32767));
Memcpy(dstComponent, &out, sizeof(out));
break;
}
case GL_UNSIGNED_INT: {
const auto out = static_cast<Uint32>(std::clamp<Int64>(v, 0, 4294967295LL));
Memcpy(dstComponent, &out, sizeof(out));
break;
}
case GL_INT: {
const auto out =
static_cast<Int32>(std::clamp<Int64>(v, -2147483648LL, 2147483647LL));
Memcpy(dstComponent, &out, sizeof(out));
break;
}
default:
break;
}
}
}
} else {
Float srcValues[4];
switch (wideType) {
case GL_UNSIGNED_BYTE:
for (Int c = 0; c < 4; ++c) {
srcValues[c] = static_cast<Float>(srcPixel[c]) / 255.0f;
}
break;
case GL_BYTE:
for (Int c = 0; c < 4; ++c) {
srcValues[c] = std::max(
static_cast<Float>(reinterpret_cast<const Int8*>(srcPixel)[c]) / 127.0f, -1.0f);
}
break;
case GL_UNSIGNED_SHORT:
for (Int c = 0; c < 4; ++c) {
srcValues[c] =
static_cast<Float>(reinterpret_cast<const Uint16*>(srcPixel)[c]) / 65535.0f;
}
break;
case GL_SHORT:
for (Int c = 0; c < 4; ++c) {
srcValues[c] = std::max(
static_cast<Float>(reinterpret_cast<const Int16*>(srcPixel)[c]) / 32767.0f, -1.0f);
}
break;
case GL_HALF_FLOAT:
for (Int c = 0; c < 4; ++c) {
srcValues[c] = DecodeHalfBitsToFloat(reinterpret_cast<const Uint16*>(srcPixel)[c]);
}
break;
default: // GL_FLOAT
for (Int c = 0; c < 4; ++c) {
srcValues[c] = reinterpret_cast<const Float*>(srcPixel)[c];
}
break;
}
if (isPacked) {
Uint32 word = 0;
if (packedLayout.isFloatPacked) {
const Float fields[3] = {srcValues[mapping.sourceChannel[0]],
srcValues[mapping.sourceChannel[1]],
srcValues[mapping.sourceChannel[2]]};
word = type == GL_UNSIGNED_INT_5_9_9_9_REV
? EncodeSharedExponentRGB9E5(fields)
: (EncodeFloatToUnsignedF11(fields[0]) << packedLayout.shift[0]) |
(EncodeFloatToUnsignedF11(fields[1]) << packedLayout.shift[1]) |
(EncodeFloatToUnsignedF10(fields[2]) << packedLayout.shift[2]);
} else {
// Normalized encode: round(clamp(v, 0, 1) * (2^bits - 1)) into each field.
for (Int ch = 0; ch < packedLayout.fieldCount; ++ch) {
const auto fieldMax = static_cast<Float>((1u << packedLayout.width[ch]) - 1u);
const auto v = static_cast<Uint32>(std::llround(
std::clamp(srcValues[mapping.sourceChannel[ch]], 0.0f, 1.0f) * fieldMax));
word |= v << packedLayout.shift[ch];
}
}
WritePackedReadbackWord(dstPixel, word, packedLayout.byteSize);
} else {
for (Int ch = 0; ch < mapping.channelCount; ++ch) {
const Float v = srcValues[mapping.sourceChannel[ch]];
Uint8* dstComponent = dstPixel + static_cast<SizeT>(ch) * dstComponentSize;
switch (type) {
case GL_UNSIGNED_BYTE:
*dstComponent =
static_cast<Uint8>(std::llround(std::clamp(v, 0.0f, 1.0f) * 255.0));
break;
case GL_BYTE: {
const auto out =
static_cast<Int8>(std::llround(std::clamp(v, -1.0f, 1.0f) * 127.0));
Memcpy(dstComponent, &out, sizeof(out));
break;
}
case GL_UNSIGNED_SHORT: {
const auto out =
static_cast<Uint16>(std::llround(std::clamp(v, 0.0f, 1.0f) * 65535.0));
Memcpy(dstComponent, &out, sizeof(out));
break;
}
case GL_SHORT: {
const auto out =
static_cast<Int16>(std::llround(std::clamp(v, -1.0f, 1.0f) * 32767.0));
Memcpy(dstComponent, &out, sizeof(out));
break;
}
case GL_UNSIGNED_INT: {
const auto out = static_cast<Uint32>(
std::llround(static_cast<Double>(std::clamp(v, 0.0f, 1.0f)) * 4294967295.0));
Memcpy(dstComponent, &out, sizeof(out));
break;
}
case GL_INT: {
const auto out = static_cast<Int32>(
std::llround(static_cast<Double>(std::clamp(v, -1.0f, 1.0f)) * 2147483647.0));
Memcpy(dstComponent, &out, sizeof(out));
break;
}
case GL_FLOAT:
Memcpy(dstComponent, &v, sizeof(v));
break;
case GL_HALF_FLOAT: {
const Uint16 out = EncodeFloatToHalfBits(v);
Memcpy(dstComponent, &out, sizeof(out));
break;
}
default:
break;
}
}
}
}
}
}
} // namespace ReadbackImpl
} // namespace MobileGL::MG_Backend::DirectGLES
+45
View File
@@ -45,6 +45,51 @@ namespace MobileGL::MG_Backend::DirectGLES {
namespace FramebufferImpl {} // namespace FramebufferImpl
// Pure CPU helpers of the client-format readback conversion (ReadPixels/GetTexImage repack a wide
// RGBA(_INTEGER) read into the caller's (format, type) layout). Kept context-free so unit tests can
// exercise the exact packing the GL CTS packed_pixels oracle compares against.
namespace ReadbackImpl {
struct ReadbackChannelMapping {
Int sourceChannel[4]; // RGBA source channel feeding each destination component
Int channelCount; // destination component count
Bool isInteger;
};
Bool GetReadbackChannelMapping(GLenum format, ReadbackChannelMapping& outMapping);
// Byte size of one destination component of `type`; packed types report the packed word size.
// 0 = type not supported by the conversion path.
SizeT GetReadbackComponentSize(GLenum type);
// Bit-field layout of a GL packed pixel type. width/shift are indexed in the client format's
// component order (matching ReadbackChannelMapping); shift is the LSB position of the field in
// the packed word: non-REV types pack the first component from the MSB, *_REV types from the
// LSB (GL 3.3 table 3.6; field positions mirror the GL CTS glcPackedPixelsTests pack_* oracle).
struct PackedReadbackLayout {
Int fieldCount; // format components stored in the packed word
Int width[4]; // bit width of each component's field
Int shift[4]; // LSB bit position of each component's field
SizeT byteSize; // packed word size in bytes (1, 2 or 4)
Bool isFloatPacked; // 10F_11F_11F_REV / 5_9_9_9_REV: fields hold unsigned small floats
};
Bool GetPackedReadbackLayout(GLenum type, PackedReadbackLayout& out);
// Unsigned small-float encoders (EXT_packed_float / EXT_texture_shared_exponent semantics).
Uint32 EncodeFloatToUnsignedF11(Float value);
Uint32 EncodeFloatToUnsignedF10(Float value);
Uint32 EncodeSharedExponentRGB9E5(const Float rgb[3]);
// Destination bytes per pixel for a (format mapping, type) readback pair; 0 when the pair is
// not convertible (unknown type, packed field count != format component count, floating-point
// or packed-float type with an integer format).
SizeT GetReadbackDstPixelSize(const ReadbackChannelMapping& mapping, GLenum type);
// Repacks one row of wide RGBA(_INTEGER) texels (4 components of wideType each) into the
// client's (format, type) layout. src holds width * 4 * GetReadbackComponentSize(wideType)
// bytes, dst receives width * GetReadbackDstPixelSize(mapping, type) bytes.
void ConvertWideReadbackRow(const Uint8* src, Uint8* dst, SizeT width, GLenum wideType,
const ReadbackChannelMapping& mapping, GLenum type);
} // namespace ReadbackImpl
namespace PrgramImpl {
String ProcessOutColorLocations(const String& glslCode);
String ForceSupporterOutput(const String& glslCode);
@@ -488,14 +488,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
.TargetGLSLVersion = {4, 6, 0},
// Baseline advertisement (no shader subgroup, no timer queries); a
// live backend reconciles its copy in UpdateAdvertisedExtensions.
.Extensions = BuildAdvertisedExtensions(false, false),
.Extensions = BuildAdvertisedExtensions(false, false, false),
.IsCompatibilityProfile = false
},
.StaticBackendCapability = {.AllowVSOnlyPrograms = false}};
return rendererInfo;
}
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported) {
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
Bool anisotropicFilteringSupported) {
Vector<GLExtension> extensions = {V_OpenGL30, V_OpenGL31, V_OpenGL32,
V_OpenGL33, E_GL_ARB_draw_buffers_blend, E_GL_ARB_compute_shader,
E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
@@ -516,6 +517,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (timerQueriesSupported && !MG_Config::Features.DisableTimerQuery) {
extensions.push_back(E_GL_ARB_timer_query);
}
// Only advertised when the samplerAnisotropy device feature was granted: without it the
// sampler state is accepted but never applied, and an app trusting the string (LWJGL builds
// GLCapabilities from it) would think it enabled anisotropic filtering.
if (anisotropicFilteringSupported) {
extensions.push_back(E_GL_EXT_texture_filter_anisotropic);
extensions.push_back(E_GL_ARB_texture_filter_anisotropic);
}
return extensions;
}
@@ -630,7 +638,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// run without a renderer; no timer query is advertised then. Rebuilding
// the whole list keeps re-runs idempotent.
m_rendererInfo.RendererGLInfo.Extensions = BuildAdvertisedExtensions(
m_vulkanCaps.SupportsShaderSubgroup, pVulkanRenderer && pVulkanRenderer->IsTimerQuerySupported());
m_vulkanCaps.SupportsShaderSubgroup, pVulkanRenderer && pVulkanRenderer->IsTimerQuerySupported(),
pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported());
}
void BackendObject_DirectVulkan::UpdateDynamicBackendParameters() {
@@ -680,6 +689,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_dynamicParameters.UniformBufferOffsetAlignment = m_vulkanCaps.UniformBufferOffsetAlignment;
m_dynamicParameters.AliasedLineWidthRangeMin = m_vulkanCaps.AliasedLineWidthRangeMin;
m_dynamicParameters.AliasedLineWidthRangeMax = m_vulkanCaps.AliasedLineWidthRangeMax;
// Without the samplerAnisotropy feature the limit is unusable, so report 1.0 (no anisotropy)
// rather than a maximum the sampler manager will never apply.
m_dynamicParameters.MaxTextureMaxAnisotropy =
(pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported()) ? m_vulkanCaps.MaxSamplerAnisotropy
: 1.0f;
m_dynamicParameters.SmoothLineWidthRangeMin = m_vulkanCaps.SmoothLineWidthRangeMin;
m_dynamicParameters.SmoothLineWidthRangeMax = m_vulkanCaps.SmoothLineWidthRangeMax;
m_dynamicParameters.SmoothLineWidthGranularity = m_vulkanCaps.SmoothLineWidthGranularity;
@@ -73,7 +73,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// a device with the given raw capabilities. The MOBILEGL_DISABLE_SUBGROUP and
// MOBILEGL_DISABLE_TIMERQUERY escape hatches are applied inside, so callers pass
// the detected device support (passing an already-gated value is harmless).
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported);
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
Bool anisotropicFilteringSupported);
// Format: <GPU Name>, Vulkan <Vulkan Version>, Driver <Driver Version> — the exact
// string an initialized backend returns from GetBackendAPIVersionString (and that
@@ -8,6 +8,7 @@
#include "PipelineFactory.h"
namespace MobileGL::MG_Backend::DirectVulkan {
static const char* PrimitiveTopologyToString(VkPrimitiveTopology topology) {
switch (topology) {
@@ -108,6 +109,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
"vkCreatePipelineCache");
}
void PipelineFactory::SetSuppressBlendedDepthWrite(Bool enabled) {
s_suppressBlendedDepthWrite = enabled;
}
PipelineFactory::~PipelineFactory() {
DestroyAll();
if (m_pipelineCache != VK_NULL_HANDLE) {
@@ -258,6 +263,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
for (Uint32 i = 0; i < payload.colorAttachmentCount; ++i) {
colorAttachments[i] = payload.colorBlendAttachments[i];
}
// Suppress depth writes on blended pipelines when the active driver cannot keep
// vertex positions invariant across the pipelines of a multi-pass depth-equality
// chain (see SetSuppressBlendedDepthWrite). Blended draws that write depth are rare
// and the equality-dependent prepass pattern is exactly the case that breaks.
if (s_suppressBlendedDepthWrite && depthStencil.depthWriteEnable == VK_TRUE) {
for (Uint32 i = 0; i < payload.colorAttachmentCount; ++i) {
if (colorAttachments[i].blendEnable == VK_TRUE) {
depthStencil.depthWriteEnable = VK_FALSE;
break;
}
}
}
VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO};
blend.logicOpEnable = payload.logicOpEnable ? VK_TRUE : VK_FALSE;
blend.logicOp = payload.logicOp;
@@ -62,6 +62,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkPipeline GetOrCreatePipeline(const PipelineCreatePayload& payload);
void DestroyAll();
// Driver quirk: suppress depth writes on blended pipelines. Multi-pass depth-equality
// rendering (a blended prepass writes depth that later passes re-test with an
// equality-inclusive compare on the re-rasterized geometry) requires cross-pipeline
// position invariance that some mobile compilers do not provide, even with the
// SPIR-V Invariant decoration; whole primitives then drop out of the later passes.
// Set at renderer initialization based on the active driver.
static void SetSuppressBlendedDepthWrite(Bool enabled);
private:
VkPipeline CreatePipeline(const PipelineCreatePayload& payload) const;
@@ -70,5 +78,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkPipelineCache m_pipelineCache = VK_NULL_HANDLE;
UnorderedMap<HashType, VkPipeline> m_cache;
static inline XXH64_state_t* m_hashState = XXH64_createState();
static inline Bool s_suppressBlendedDepthWrite = false;
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -1697,6 +1697,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
moduleSpirvs[i] = spv;
}
// GL apps depend on cross-program position invariance for multi-pass equality
// depth tests (MC 26.3's OIT re-draws the cloud geometry with GEQUAL against the
// depth its own first pass wrote); decorate Position outputs Invariant so
// per-pipeline compilers cannot vary the position math between passes.
{
Vector<Uint> invariantSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::DecoratePositionInvariantForVulkan(
moduleSpirvs[i], invariantSpirv)) {
moduleSpirvs[i] = std::move(invariantSpirv);
} else {
MGLOG_W("ProgramFactory: position-invariant decoration failed for program %u; "
"keeping the original module",
program.GetExternalIndex());
}
}
// glslang's relaxed-Vulkan mode aliases GL's zero-based gl_InstanceID to Vulkan's
// gl_InstanceIndex, which wrongly includes the draw's baseInstance. Rebase vertex-stage
// loads to (InstanceIndex - BaseInstance) so shaders observe GL semantics. Reflection
@@ -360,6 +360,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
const TextureTarget preferredTarget = programObj.samplerTextureTargetByBinding[binding];
outTexture = textureUnit.GetBindingSlot(preferredTarget).GetBoundObject();
// The slot always holds at least the target's default texture (name 0). While that
// default has no image it is unsampleable; report it as "unbound" so callers keep
// taking their fallback paths instead of trying to sync a storage-less texture.
if (MG_State::GLState::IsUndefinedDefaultTexture(outTexture.get())) {
outTexture.reset();
}
return true;
}
@@ -380,7 +386,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const TextureTarget preferredTarget = programObj.samplerTextureTargetByBinding[binding];
// GetBoundObject() returns the SharedPtr by const ref; .get() reads the pointer without
// touching the refcount (no atomic inc/dec per binding per draw).
return textureUnit.GetBindingSlot(preferredTarget).GetBoundObject().get();
MG_State::GLState::ITextureObject* texture =
textureUnit.GetBindingSlot(preferredTarget).GetBoundObject().get();
// The slot always holds at least the target's default texture (name 0). While that
// default has no image it is unsampleable; report it as "unbound" so the caller
// substitutes its fallback texture exactly like it did for the old null slot.
if (MG_State::GLState::IsUndefinedDefaultTexture(texture)) {
return nullptr;
}
return texture;
}
Bool UniformManager::ResolveTexelBufferDescriptor(const MG_State::GLState::ProgramObject& program,
@@ -599,7 +613,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MG_State::GLState::ITextureObject* texture = ResolveSamplerTextureRaw(program, programObj, binding);
if (!texture) {
continue;
// ResolveSamplerDescriptor will substitute the fallback texture for this binding;
// include it in the sampled set so the pre-render-pass sync/transition pass covers
// its first use instead of leaving that work to happen inside an active pass.
const TextureTarget preferredTarget = programObj.samplerTextureTargetByBinding[binding];
if (preferredTarget != TextureTarget::Texture2D &&
preferredTarget != TextureTarget::TextureRectangle) {
continue;
}
texture = GetFallbackTexture(preferredTarget).get();
}
auto found = std::find(outTextures.begin(), outTextures.end(), texture);
@@ -197,16 +197,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return static_cast<VkBufferResource*>(bufferObject.GetBackendResource().get());
}
SharedPtr<VkBufferResource> VkBufferManager::GetOrCreateResource(
VkBufferResource* VkBufferManager::GetOrCreateResource(
const SharedPtr<MG_State::GLState::BufferObject>& bufferObject) {
auto existing = std::static_pointer_cast<VkBufferResource>(bufferObject->GetBackendResource());
// Return by raw pointer: the resource is owned for its whole lifetime by the BufferObject's
// backend-resource SharedPtr (already set, or set below), so callers that only dereference
// it avoid a static_pointer_cast + SharedPtr refcount inc/dec on every per-draw buffer bind.
const auto& existing = bufferObject->GetBackendResource();
if (existing) {
return existing;
return static_cast<VkBufferResource*>(existing.get());
}
auto resource = MakeShared<VkBufferResource>();
VkBufferResource* raw = resource.get();
bufferObject->SetBackendResource(resource);
TrackLiveResource(resource);
return resource;
return raw;
}
void VkBufferManager::TrackLiveResource(const SharedPtr<VkBufferResource>& resource) {
@@ -124,7 +124,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
private:
Bool InitializeTransientArenas();
static VkBufferUsageFlags GetVkBufferUsage(BufferKind kind);
SharedPtr<VkBufferResource> GetOrCreateResource(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject);
VkBufferResource* GetOrCreateResource(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject);
static VkBufferResource* ResourceOf(MG_State::GLState::BufferObject& bufferObject);
Bool CreateResidentStorage(VkBufferResource& resource, VkDeviceSize size, VkBufferUsageFlags usage,
VkMemoryPropertyFlags requiredFlags = 0);
@@ -602,6 +602,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_rpFastRenderPassHash == activeRenderPass->hash && !hasPendingClearOnFramebuffer()) {
auto activeIt = m_renderPasses.find(activeRenderPass->hash);
if (activeIt != m_renderPasses.end()) {
activeIt->second.lastUsedFrame = m_frameCounter;
return activeIt->second;
}
}
@@ -623,12 +624,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_rpFastTexEpoch = m_textureManager.GetTextureImageEpoch();
m_rpFastRbEpoch = m_renderbufferImageEpoch;
m_rpFastRenderPassHash = activeRenderPass->hash;
activeIt->second.lastUsedFrame = m_frameCounter;
return activeIt->second;
}
auto hash = ComputeHash(fbo, swapchainImageIndex, true);
auto it = m_renderPasses.find(hash);
if (it != m_renderPasses.end())
if (it != m_renderPasses.end()) {
it->second.lastUsedFrame = m_frameCounter;
return it->second;
}
Bool isDefaultFbo = fbo.IsDefaultFramebuffer();
// Color attachment
@@ -718,6 +722,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (hasClear) {
pendingClearAttachments.emplace_back(PendingClearAttachmentInfo {
.attachmentIndex = attachmentIndex,
.colorAttachmentSlot = i,
.key = VkClearManager::MakePendingClearKey(att)
});
}
@@ -974,6 +979,52 @@ namespace MobileGL::MG_Backend::DirectVulkan {
subpassDesc.preserveAttachmentCount = 0;
subpassDesc.pPreserveAttachments = nullptr;
// External subpass dependencies. Without them there is NO execution/memory
// dependency between consecutive render passes (or a pass and a transfer)
// touching the same attachments when the image layout does not change — the
// layout-transition helper no-ops on identical layouts and no other barrier
// exists. Tile-based GPUs then race tile loads against the previous pass's
// stores (multi-pass FBO chains like MC 26.3's OIT flicker on Adreno).
// Conservative both-ways dependencies: prior writes (attachment output,
// depth/stencil, transfer, shader) are made visible to this pass's loads,
// and this pass's attachment writes to subsequent sampling/transfer/loads.
VkSubpassDependency subpassDependencies[2];
subpassDependencies[0] = {};
subpassDependencies[0].srcSubpass = VK_SUBPASS_EXTERNAL;
subpassDependencies[0].dstSubpass = 0;
subpassDependencies[0].srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT |
VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT |
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT;
subpassDependencies[0].srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT |
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT |
VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT;
subpassDependencies[0].dstStageMask = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT |
VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT |
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT |
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
subpassDependencies[0].dstAccessMask =
VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT |
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT |
VK_ACCESS_SHADER_READ_BIT;
subpassDependencies[0].dependencyFlags = 0;
subpassDependencies[1] = {};
subpassDependencies[1].srcSubpass = 0;
subpassDependencies[1].dstSubpass = VK_SUBPASS_EXTERNAL;
subpassDependencies[1].srcStageMask =
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
subpassDependencies[1].srcAccessMask =
VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
subpassDependencies[1].dstStageMask = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT |
VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT |
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT |
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT |
VK_PIPELINE_STAGE_TRANSFER_BIT;
subpassDependencies[1].dstAccessMask =
VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT |
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT |
VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT;
subpassDependencies[1].dependencyFlags = 0;
// Render Pass
VkRenderPassCreateInfo renderPassCreateInfo;
renderPassCreateInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
@@ -983,8 +1034,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
renderPassCreateInfo.pAttachments = attachmentDescriptions.data();
renderPassCreateInfo.subpassCount = 1;
renderPassCreateInfo.pSubpasses = &subpassDesc;
renderPassCreateInfo.dependencyCount = 0;
renderPassCreateInfo.pDependencies = nullptr;
renderPassCreateInfo.dependencyCount = 2;
renderPassCreateInfo.pDependencies = subpassDependencies;
VkRenderPass renderPass = VK_NULL_HANDLE;
VK_VERIFY(vkCreateRenderPass(m_device, &renderPassCreateInfo, nullptr, &renderPass));
@@ -1015,7 +1066,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
hasDepthStencilAttachment,
renderPassSampleCount,
extent,
static_cast<Int>(framebufferLayers) };
framebufferLayers };
MGLOG_D("VkRenderPassManager::GetOrCreateRenderPass: hash=0x%llx compatibilityHash=0x%llx attachmentCount=%u colorAttachmentCount=%u samples=%d extent=%dx%d",
static_cast<unsigned long long>(hash),
static_cast<unsigned long long>(compatibilityHash),
@@ -1025,9 +1076,36 @@ namespace MobileGL::MG_Backend::DirectVulkan {
extent.x(),
extent.y());
auto [insertedIt, _] = m_renderPasses.emplace(hash, Move(renderPassEntry));
insertedIt->second.lastUsedFrame = m_frameCounter;
return insertedIt->second;
}
void VkRenderPassManager::OnPresent() {
++m_frameCounter;
// Sweep occasionally; evict entries whose last use is far past every
// in-flight frame so their VkRenderPass/VkFramebuffer can be destroyed
// safely (RenderPassEntry's destructor releases the handles).
constexpr Uint64 kSweepInterval = 256;
constexpr Uint64 kRetireAgeFrames = 1024;
if ((m_frameCounter % kSweepInterval) != 0) {
return;
}
const Uint64 activeHash = s_hasActiveRenderPass ? s_activeRenderPass.hash : 0;
for (auto it = m_renderPasses.begin(); it != m_renderPasses.end();) {
const Bool isActive = s_hasActiveRenderPass && it->first == activeHash;
if (!isActive && m_frameCounter - it->second.lastUsedFrame > kRetireAgeFrames) {
if (m_rpFastValid && m_rpFastRenderPassHash == it->first) {
m_rpFastValid = false;
}
it = m_renderPasses.erase(it);
} else {
++it;
}
}
}
Bool VkRenderPassManager::BeginRenderPass(VkCommandBuffer commandBuffer, RenderPassEntry& renderPassEntry) {
// TODO: Transition all the attachments into proper layout before starting the render pass
VkRenderPassBeginInfo renderPassBeginInfo;
@@ -27,7 +27,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
};
struct PendingClearAttachmentInfo {
// Index into the render pass attachment descriptions (VkRenderPassBeginInfo::pClearValues space).
Uint32 attachmentIndex = 0;
// Index into the subpass pColorAttachments (VkClearAttachment::colorAttachment space) — the GL
// draw-buffer slot. Differs from attachmentIndex when earlier slots are GL_NONE/incomplete.
// Only meaningful for color clears.
Uint32 colorAttachmentSlot = 0;
PendingClearKey key{};
MG_State::GLState::RenderbufferObject* renderbuffer = nullptr;
Bool hasInlinePayload = false;
@@ -68,7 +73,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool hasDepthStencilAttachment = false;
VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
IntVec2 extent = {0, 0};
Uint32 subpass = 0;
// VkFramebufferCreateInfo::layers of the entry's framebuffer (>1 for layered GL attachments).
Uint32 layers = 1;
// Frame counter value of the last GetOrCreateRenderPass hit; drives cache eviction.
Uint64 lastUsedFrame = 0;
RenderPassEntry() = default;
RenderPassEntry(const RenderPassEntry&) = delete;
@@ -84,7 +92,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
std::swap(hasDepthStencilAttachment, that.hasDepthStencilAttachment);
std::swap(sampleCount, that.sampleCount);
std::swap(extent, that.extent);
std::swap(subpass, that.subpass);
std::swap(layers, that.layers);
std::swap(lastUsedFrame, that.lastUsedFrame);
}
RenderPassEntry(
Uint64 hash,
@@ -97,7 +106,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 colorAttachmentCount,
Bool hasDepthStencilAttachment,
VkSampleCountFlagBits sampleCount,
IntVec2 extent, int subpass):
IntVec2 extent, Uint32 layers):
hash(hash),
renderPass(renderpass),
framebuffer(framebuffer),
@@ -109,7 +118,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
hasDepthStencilAttachment(hasDepthStencilAttachment),
sampleCount(sampleCount),
extent(extent),
subpass(subpass)
layers(layers)
{}
~RenderPassEntry() {
@@ -166,6 +175,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void QueueRenderbufferClear(const ClearAttachmentPayload& clearPayload,
const MG_State::GLState::FramebufferAttachmentObject& attachment);
void PopPendingRenderbufferClear(MG_State::GLState::RenderbufferObject* renderbuffer);
// Frame boundary hook: ages the render-pass cache and evicts long-unused
// entries (their command buffers retired many frames ago).
void OnPresent();
static Bool BeginRenderPass(VkCommandBuffer commandBuffer, RenderPassEntry& renderPassEntry);
static Bool EndRenderPass(VkCommandBuffer commandBuffer);
static ActiveRenderPassInfo* GetActiveRenderPass();
@@ -178,6 +190,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkTextureManager& m_textureManager;
SwapchainObject& m_swapchainObject;
UnorderedMap<Uint64, RenderPassEntry> m_renderPasses;
// Monotonic frame counter (bumped in OnPresent) for render-pass cache aging.
Uint64 m_frameCounter = 0;
// Bumped whenever a renderbuffer VkImage is (re)created; together with the texture
// manager's image epoch this invalidates the render-pass fast path on any attachment
@@ -58,11 +58,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_device = initInfo.device;
m_config = initInfo.config;
m_samplerAnisotropySupported = initInfo.samplerAnisotropySupported;
m_maxSamplerAnisotropy = std::max(initInfo.maxSamplerAnisotropy, 1.0f);
MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE && m_config != nullptr,
"VkSamplerManager::Initialize failed: invalid initialization info");
return true;
}
Float VkSamplerManager::ResolveEffectiveMaxAnisotropy(const MG_State::GLState::SamplerObject& sampler) const {
if (!m_samplerAnisotropySupported) return 1.0f;
// VUID-VkSamplerCreateInfo-anisotropyEnable-01071/01072: anisotropy requires both filters to
// be LINEAR and the value to sit within [1, limits.maxSamplerAnisotropy].
if (sampler.GetMinFilter() != SamplerFilterMode::Linear ||
sampler.GetMagFilter() != SamplerFilterMode::Linear) {
return 1.0f;
}
return std::clamp(sampler.GetMaxAnisotropy(), 1.0f, m_maxSamplerAnisotropy);
}
void VkSamplerManager::Shutdown() {
for (auto& [_, sampler] : m_samplers) {
if (m_device != VK_NULL_HANDLE && sampler.handle != VK_NULL_HANDLE) {
@@ -99,6 +112,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
XXHASH_VERIFY(XXH64_update(m_hashState, &maxLod, sizeof(maxLod)));
const auto lodBias = sampler.GetLodBias();
XXHASH_VERIFY(XXH64_update(m_hashState, &lodBias, sizeof(lodBias)));
// The RESOLVED value, not the GL request: samplers that only differ in an anisotropy Vulkan
// will not apply (NEAREST filtering, or requests past the device limit) must still share one
// VkSampler, while two samplers that really do differ must not collide onto the first one's.
const auto maxAnisotropy = ResolveEffectiveMaxAnisotropy(sampler);
XXHASH_VERIFY(XXH64_update(m_hashState, &maxAnisotropy, sizeof(maxAnisotropy)));
const auto compareMode = sampler.GetCompareMode();
XXHASH_VERIFY(XXH64_update(m_hashState, &compareMode, sizeof(compareMode)));
const auto compareFunc = ResolveCompareFunc(sampler, texture);
@@ -125,8 +143,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
samplerInfo.addressModeV = ToVkAddressMode(sampler.GetWrapT());
samplerInfo.addressModeW = ToVkAddressMode(sampler.GetWrapR());
samplerInfo.mipLodBias = sampler.GetLodBias();
samplerInfo.anisotropyEnable = VK_FALSE;
samplerInfo.maxAnisotropy = 1.0f;
// Must use the same resolver as BuildSamplerKey - a divergence would either collide two
// different samplers or silently create duplicates.
const Float maxAnisotropy = ResolveEffectiveMaxAnisotropy(sampler);
samplerInfo.anisotropyEnable = maxAnisotropy > 1.0f ? VK_TRUE : VK_FALSE;
samplerInfo.maxAnisotropy = maxAnisotropy;
samplerInfo.compareEnable = sampler.GetCompareMode() == SamplerCompareMode::CompareToTexture ? VK_TRUE : VK_FALSE;
samplerInfo.compareOp = ToVkCompareOp(ResolveCompareFunc(sampler, texture));
samplerInfo.maxLod = ResolveEffectiveMaxLod(sampler);
@@ -24,6 +24,10 @@ public:
struct InitInfo {
VkDevice device = VK_NULL_HANDLE;
const VulkanRendererConfig* config = nullptr;
// The samplerAnisotropy device feature was requested and granted at vkCreateDevice.
Bool samplerAnisotropySupported = false;
// VkPhysicalDeviceLimits::maxSamplerAnisotropy.
Float maxSamplerAnisotropy = 1.0f;
};
Bool Initialize(const InitInfo& initInfo);
@@ -49,9 +53,16 @@ private:
const MG_State::GLState::ITextureObject& texture);
static VkBorderColor ResolveVkBorderColor(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture);
// The anisotropy Vulkan will actually apply: 1.0 (i.e. disabled) unless the feature is on and
// the sampler filters linearly both ways, otherwise the GL request clamped to the device limit.
// GL happily carries GL_TEXTURE_MAX_ANISOTROPY on a NEAREST sampler (Blaze3D's blocks do exactly
// that) while Vulkan forbids anisotropyEnable there, so the GL value must never be forwarded raw.
Float ResolveEffectiveMaxAnisotropy(const MG_State::GLState::SamplerObject& sampler) const;
VkDevice m_device = VK_NULL_HANDLE;
const VulkanRendererConfig* m_config = nullptr;
Bool m_samplerAnisotropySupported = false;
Float m_maxSamplerAnisotropy = 1.0f;
UnorderedMap<Uint64, SamplerCacheEntry> m_samplers;
static inline XXH64_state_t* m_hashState = XXH64_createState();
};
@@ -21,9 +21,13 @@
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
#include "MG_Util/Metrics/TextureMetrics.h"
#include <Config.h>
#include <algorithm>
#include <cstdlib>
#include <cstring>
#include <vulkan/vulkan_core.h>
#ifdef __ANDROID__
#include <sys/system_properties.h>
#endif
#if defined(__APPLE__)
#include <CoreGraphics/CoreGraphics.h>
@@ -946,6 +950,7 @@ void main() {
}
)";
static Uint32 ComputeFullMipLevelCount(const IntVec3& baseTexelSize) {
Int maxDimension = std::max<Int>(
baseTexelSize.x(),
@@ -1883,13 +1888,36 @@ void main() {
m_pipelineFactory = MakeUnique<PipelineFactory>(m_device, m_config);
MOBILEGL_ASSERT(m_pipelineFactory != nullptr, "PipelineFactory creation failed.");
{
// Qualcomm's pipeline compiler does not keep vertex positions invariant across
// the pipelines of a multi-pass depth-equality chain (even with the SPIR-V
// Invariant decoration), so a blended depth-writing prepass makes later
// equality-compare passes drop whole primitives (MC 26.3 improved-transparency
// clouds flicker black). Suppress blended depth writes there; the env variable
// forces the quirk on ("0") or off ("1") on any driver.
static constexpr Uint32 kVendorIdQualcomm = 0x5143;
Bool suppressBlendedDepthWrite = m_physicalDevice.properties.vendorID == kVendorIdQualcomm;
if (const char* env = getenv("MOBILEGL_MAGMA_BLENDED_DEPTH_WRITE")) {
if (env[0] == '0') {
suppressBlendedDepthWrite = true;
} else if (env[0] == '1') {
suppressBlendedDepthWrite = false;
}
}
if (suppressBlendedDepthWrite) {
MGLOG_I("DirectVulkan: suppressing depth writes on blended pipelines "
"(driver lacks cross-pipeline position invariance)");
}
PipelineFactory::SetSuppressBlendedDepthWrite(suppressBlendedDepthWrite);
}
m_programFactory = MakeUnique<ProgramFactory>(m_device, m_config, maxProgramBindings,
m_shaderDrawParametersFeatureEnabled);
MOBILEGL_ASSERT(m_programFactory != nullptr, "ProgramFactory creation failed.");
m_samplerManager = MakeUnique<VkSamplerManager>();
MOBILEGL_ASSERT(m_samplerManager != nullptr, "VkSamplerManager creation failed.");
succeeded = m_samplerManager->Initialize({m_device, &m_config});
succeeded = m_samplerManager->Initialize({m_device, &m_config, m_samplerAnisotropyFeatureEnabled,
m_physicalDevice.properties.limits.maxSamplerAnisotropy});
MOBILEGL_ASSERT(succeeded, "VkSamplerManager initialization failed.");
succeeded = InitializeBlitResources();
MOBILEGL_ASSERT(succeeded, "Blit pipeline resource initialization failed.");
@@ -2150,8 +2178,8 @@ void main() {
if (!supported) {
// SetupDraw's pre-flight should have rejected this already; never upload a null payload.
MGLOG_E("UploadAndBindVertexStreams skipped: unsupported current generic vertex attribute type: "
"program=%u location=%u type=0x%x",
program.GetExternalIndex(), location, glType);
"programHash=%llu location=%u type=0x%x",
static_cast<unsigned long long>(programObj.hash), location, glType);
return false;
}
@@ -3181,17 +3209,30 @@ void main() {
colorAttachmentFormat = textureResource->format;
}
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
VkFormatProperties formatProperties{};
vkGetPhysicalDeviceFormatProperties(m_physicalDevice.handle, colorAttachmentFormat, &formatProperties);
MOBILEGL_ASSERT(
(formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BLEND_BIT) != 0,
"GetOrCreatePipeline: blend is enabled on color attachment %u for format=%d textureId=%d, but the format lacks VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BLEND_BIT (program=%u)",
i,
static_cast<Int>(colorAttachmentFormat),
textureExternalIndex,
program.GetExternalIndex());
#endif
// Blending on an attachment whose format lacks
// VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BLEND_BIT is invalid pipeline state
// (blend support is optional for e.g. 32-bit float formats on some GPUs);
// force-disable it instead of baking undefined behavior into the pipeline.
static UnorderedMap<Int, Bool> formatBlendSupport;
auto blendSupportIt = formatBlendSupport.find(static_cast<Int>(colorAttachmentFormat));
if (blendSupportIt == formatBlendSupport.end()) {
VkFormatProperties formatProperties{};
vkGetPhysicalDeviceFormatProperties(m_physicalDevice.handle, colorAttachmentFormat,
&formatProperties);
const Bool blendable =
(formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BLEND_BIT) != 0;
blendSupportIt =
formatBlendSupport.emplace(static_cast<Int>(colorAttachmentFormat), blendable).first;
if (!blendable) {
MGLOG_E("GetOrCreatePipeline: format=%d lacks VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BLEND_BIT; "
"disabling blending on attachments with this format (first hit: attachment %u textureId=%d program=%u)",
static_cast<Int>(colorAttachmentFormat), i, textureExternalIndex,
program.GetExternalIndex());
}
}
if (!blendSupportIt->second) {
effectiveBlendEnabled = false;
}
}
// Dual-source blending (GL_SRC1_* factors from glBlendFunc paired with
// glBindFragDataLocationIndexed) requires the dualSrcBlend device feature. It is detected at
@@ -3584,8 +3625,73 @@ void main() {
1, &memoryBarrier, 0, nullptr, 0, nullptr);
}
VulkanRenderer::ScissoredClearPrep VulkanRenderer::PrepareScissoredClear(
const MG_State::GLState::FramebufferObject& framebuffer, VkClearRect& outClearRect) {
auto& frame = m_frameContext.GetCurrent();
if (!frame.isCommandRecording) {
m_frameContext.BeginCommandRecording();
}
auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass();
auto* renderPassEntry = &m_renderPassManager->GetOrCreateRenderPass(framebuffer, m_imageIndexAcquired);
if (renderPassEntry->attachmentCount == 0 ||
renderPassEntry->extent.x() <= 0 || renderPassEntry->extent.y() <= 0) {
return ScissoredClearPrep::NoOp;
}
VkClearRect clearRect{};
clearRect.rect = framebuffer.IsDefaultFramebuffer()
? MakeDefaultFramebufferScissorRect(MG_State::pGLContext->GetScissorBox(),
renderPassEntry->extent,
m_swapchainObject.GetPreTransform())
: MakeClampedScissorRect(MG_State::pGLContext->GetScissorBox(), renderPassEntry->extent);
clearRect.baseArrayLayer = 0;
// GL 3.3 §4.4.7: clearing a layered framebuffer clears every layer.
clearRect.layerCount = renderPassEntry->layers;
if (clearRect.rect.extent.width == 0 || clearRect.rect.extent.height == 0) {
return ScissoredClearPrep::NoOp;
}
// A scissor that covers the whole target is a whole-surface clear; the deferred loadOp
// path is equivalent and cheaper (no render pass churn, loadOp=CLEAR on tilers).
if (clearRect.rect.offset.x == 0 && clearRect.rect.offset.y == 0 &&
clearRect.rect.extent.width == static_cast<Uint32>(renderPassEntry->extent.x()) &&
clearRect.rect.extent.height == static_cast<Uint32>(renderPassEntry->extent.y())) {
return ScissoredClearPrep::NotNeeded;
}
if (activeRenderPass && !activeRenderPass->CompatibleWith(*renderPassEntry)) {
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
activeRenderPass = nullptr;
// Re-resolve: ending the pass updates tracked attachment layouts, which feed the
// entry's load ops and initial layouts.
renderPassEntry = &m_renderPassManager->GetOrCreateRenderPass(framebuffer, m_imageIndexAcquired);
}
// A still-active pass is necessarily compatible here: the block above ended any
// incompatible one and nothing since can change the active pass.
if (activeRenderPass) {
// Materialize any older whole-attachment clear before applying this
// ordered, scissored clear.
ClearAttachmentsOnActiveRenderPass(frame.commandBuffer, *renderPassEntry);
} else {
const Bool began = VkRenderPassManager::BeginRenderPass(frame.commandBuffer, *renderPassEntry);
MOBILEGL_ASSERT(began, "%s: BeginRenderPass failed", __func__);
if (!began) {
return ScissoredClearPrep::NoOp;
}
}
outClearRect = clearRect;
return ScissoredClearPrep::Ready;
}
void VulkanRenderer::Clear(GLbitfield mask) {
m_clearManager->CollectGarbage();
if ((mask & (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT)) == 0) {
return;
}
// GL 3.3 §3.1: when RASTERIZER_DISCARD is enabled, Clear and ClearBuffer* are ignored.
if (MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard)) {
return;
}
auto* fbo = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject().get();
MOBILEGL_ASSERT(fbo, "VulkanRenderer::Clear: draw framebuffer not found (fbo == nullptr)");
if (IsUnsupportedFramebufferForDirectVulkan(*fbo)) {
@@ -3598,70 +3704,358 @@ void main() {
.depth = MG_State::pGLContext->GetClearDepth(),
.stencil = MG_State::pGLContext->GetClearStencil()
};
m_clearManager->QueueClear(mask, payload, *fbo);
m_renderPassManager->QueueRenderbufferClear(mask, payload, *fbo);
// A render-pass loadOp clear always covers the complete attachment, while
// OpenGL glClear is clipped by GL_SCISSOR_TEST. Blaze3D relies on this for
// GuiItemAtlas: animated items clear only their atlas slot before being
// redrawn. Queueing that clear as a loadOp erases every cached static item.
if (MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::ScissorTest)) {
VkClearRect clearRect{};
switch (PrepareScissoredClear(*fbo, clearRect)) {
case ScissoredClearPrep::NoOp:
return;
case ScissoredClearPrep::NotNeeded:
break; // full-coverage scissor: the deferred whole-surface path below is equivalent
case ScissoredClearPrep::Ready: {
VkClearAttachment clearAttachments[MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS + 1];
Uint32 clearAttachmentCount = 0;
if ((mask & GL_COLOR_BUFFER_BIT) != 0) {
const auto& drawBuffers = fbo->GetDrawBuffers();
for (Uint32 drawBufferIndex = 0; drawBufferIndex < drawBuffers.size(); ++drawBufferIndex) {
const auto attachmentType = drawBuffers[drawBufferIndex];
if (attachmentType == FramebufferAttachmentType::None) {
continue;
}
const auto& attachment = fbo->GetAttachment(attachmentType);
if (!attachment.IsComplete()) {
continue;
}
const BoolVec4 colorMask = MG_State::pGLContext->GetColorMaskIndexed(drawBufferIndex);
if (!colorMask.r() && !colorMask.g() && !colorMask.b() && !colorMask.a()) {
continue;
}
if (!colorMask.r() || !colorMask.g() || !colorMask.b() || !colorMask.a()) {
MGLOG_W("DirectVulkan: scissored glClear with a partial color mask is not supported");
continue;
}
MG_State::GLState::ITextureObject* colorTexture = nullptr;
if (attachment.IsTexture()) {
colorTexture = attachment.GetTexture().get();
}
VkClearAttachment clearAttachment{};
clearAttachment.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
clearAttachment.colorAttachment = drawBufferIndex;
clearAttachment.clearValue.color = {
payload.color.x(), payload.color.y(), payload.color.z(),
ResolveColorClearAlpha(colorTexture, payload.color.w())
};
clearAttachments[clearAttachmentCount++] = clearAttachment;
}
}
VkImageAspectFlags depthStencilAspects = 0;
if ((mask & GL_DEPTH_BUFFER_BIT) != 0 && MG_State::pGLContext->GetDepthMask()) {
const auto& depthAttachment = fbo->GetAttachment(FramebufferAttachmentType::Depth);
if (depthAttachment.IsComplete()) {
depthStencilAspects |= VK_IMAGE_ASPECT_DEPTH_BIT;
}
}
if ((mask & GL_STENCIL_BUFFER_BIT) != 0) {
const auto& stencilAttachment = fbo->GetAttachment(FramebufferAttachmentType::Stencil);
if (stencilAttachment.IsComplete()) {
// GL 3.3 §4.2.3: the clear is masked by the front stencil write mask.
// vkCmdClearAttachments writes every bit, so only a full (8-bit stencil) or
// zero mask can be expressed; treat a partial mask like a partial color mask.
const Uint32 stencilWriteMask =
MG_State::pGLContext->GetStencilState(StencilFace::Front).WriteMask;
if ((stencilWriteMask & 0xFFu) == 0xFFu) {
depthStencilAspects |= VK_IMAGE_ASPECT_STENCIL_BIT;
} else if (stencilWriteMask != 0) {
MGLOG_W("DirectVulkan: scissored glClear with a partial stencil write mask is not supported");
}
}
}
if (depthStencilAspects != 0) {
VkClearAttachment clearAttachment{};
clearAttachment.aspectMask = depthStencilAspects;
clearAttachment.clearValue.depthStencil = {payload.depth, payload.stencil};
clearAttachments[clearAttachmentCount++] = clearAttachment;
}
if (clearAttachmentCount != 0) {
vkCmdClearAttachments(m_frameContext.GetCurrent().commandBuffer,
clearAttachmentCount, clearAttachments,
1, &clearRect);
}
return;
}
}
}
// GL 3.3 §4.2.3: glClear honors the write masks. Mirror the scissored path's
// gating for the deferred path: drop fully-masked planes, warn on partial
// masks vkCmdClear*/loadOp clears cannot express.
GLbitfield deferredMask = mask;
if ((deferredMask & GL_DEPTH_BUFFER_BIT) != 0 && !MG_State::pGLContext->GetDepthMask()) {
deferredMask &= ~static_cast<GLbitfield>(GL_DEPTH_BUFFER_BIT);
}
if ((deferredMask & GL_STENCIL_BUFFER_BIT) != 0) {
const Uint32 stencilWriteMask = MG_State::pGLContext->GetStencilState(StencilFace::Front).WriteMask;
if ((stencilWriteMask & 0xFFu) != 0xFFu) {
if (stencilWriteMask != 0) {
MGLOG_W("DirectVulkan: deferred glClear with a partial stencil write mask is not supported");
}
deferredMask &= ~static_cast<GLbitfield>(GL_STENCIL_BUFFER_BIT);
}
}
if ((deferredMask & GL_COLOR_BUFFER_BIT) != 0) {
const auto& drawBuffers = fbo->GetDrawBuffers();
Bool anyFullMask = false;
Bool anyRestrictedMask = false;
for (Uint32 drawBufferIndex = 0; drawBufferIndex < drawBuffers.size(); ++drawBufferIndex) {
if (drawBuffers[drawBufferIndex] == FramebufferAttachmentType::None) {
continue;
}
const BoolVec4 colorMask = MG_State::pGLContext->GetColorMaskIndexed(drawBufferIndex);
const Bool full = colorMask.r() && colorMask.g() && colorMask.b() && colorMask.a();
if (full) {
anyFullMask = true;
} else {
anyRestrictedMask = true;
if (colorMask.r() || colorMask.g() || colorMask.b() || colorMask.a()) {
MGLOG_W("DirectVulkan: deferred glClear with a partial color mask is not supported");
}
}
}
if (!anyFullMask) {
deferredMask &= ~static_cast<GLbitfield>(GL_COLOR_BUFFER_BIT);
} else if (anyRestrictedMask) {
// Mixed per-buffer masks: queue only the fully-writable texture targets
// individually and drop the framebuffer-level color clear.
for (Uint32 drawBufferIndex = 0; drawBufferIndex < drawBuffers.size(); ++drawBufferIndex) {
const auto attachmentType = drawBuffers[drawBufferIndex];
if (attachmentType == FramebufferAttachmentType::None) {
continue;
}
const BoolVec4 colorMask = MG_State::pGLContext->GetColorMaskIndexed(drawBufferIndex);
if (!(colorMask.r() && colorMask.g() && colorMask.b() && colorMask.a())) {
continue;
}
const auto& attachment = fbo->GetAttachment(attachmentType);
if (attachment.IsRenderbuffer()) {
m_renderPassManager->QueueRenderbufferClear(
{.mask = GL_COLOR_BUFFER_BIT, .color = payload.color}, attachment);
} else if (attachment.IsTexture()) {
m_clearManager->QueueClear({.mask = GL_COLOR_BUFFER_BIT, .color = payload.color},
attachment);
}
}
deferredMask &= ~static_cast<GLbitfield>(GL_COLOR_BUFFER_BIT);
}
}
if (deferredMask == 0) {
return;
}
m_clearManager->QueueClear(deferredMask, payload, *fbo);
m_renderPassManager->QueueRenderbufferClear(deferredMask, payload, *fbo);
}
void VulkanRenderer::QueueClearBufferPayloadForFramebuffer(
const MG_State::GLState::FramebufferObject& framebuffer, GLenum buffer, GLint drawbuffer,
const ClearAttachmentPayload& clearPayload) {
m_clearManager->CollectGarbage();
// GL 3.3 §3.1: when RASTERIZER_DISCARD is enabled, Clear and ClearBuffer* are ignored.
if (MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard)) {
return;
}
if (IsUnsupportedFramebufferForDirectVulkan(framebuffer)) {
RecordUnsupportedFramebufferError(__func__);
return;
}
auto queueAttachmentClear = [&](FramebufferAttachmentType attachmentType) {
if (attachmentType == FramebufferAttachmentType::None) {
return;
}
const auto& attachment = framebuffer.GetAttachment(attachmentType);
if (attachment.IsRenderbuffer()) {
m_renderPassManager->QueueRenderbufferClear(clearPayload, attachment);
return;
}
if (!attachment.IsTexture()) {
return;
}
m_clearManager->QueueClear(clearPayload, attachment);
};
// Validate (buffer, drawbuffer) up front so GL errors fire regardless of which clear
// path is taken below.
switch (buffer) {
case GL_COLOR: {
case GL_COLOR:
if (drawbuffer < 0 ||
drawbuffer >= static_cast<GLint>(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS)) {
RecordClearBufferError(__func__, ErrorCode::InvalidValue, "color drawbuffer index is out of range");
return;
}
queueAttachmentClear(framebuffer.GetDrawBuffers()[drawbuffer]);
return;
}
break;
case GL_DEPTH:
if (drawbuffer != 0) {
RecordClearBufferError(__func__, ErrorCode::InvalidValue, "depth clear requires drawbuffer 0");
return;
}
queueAttachmentClear(FramebufferAttachmentType::Depth);
return;
break;
case GL_STENCIL:
if (drawbuffer != 0) {
RecordClearBufferError(__func__, ErrorCode::InvalidValue, "stencil clear requires drawbuffer 0");
return;
}
queueAttachmentClear(FramebufferAttachmentType::Stencil);
return;
break;
case GL_DEPTH_STENCIL:
if (drawbuffer != 0) {
RecordClearBufferError(__func__, ErrorCode::InvalidValue, "depth/stencil clear requires drawbuffer 0");
return;
}
queueAttachmentClear(FramebufferAttachmentType::Depth);
queueAttachmentClear(FramebufferAttachmentType::Stencil);
return;
break;
default:
RecordClearBufferError(__func__, ErrorCode::InvalidEnum, "unsupported clear buffer target");
return;
}
// GL 3.3 §4.2.3: ClearBuffer* is clipped by GL_SCISSOR_TEST exactly like Clear.
if (MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::ScissorTest)) {
VkClearRect clearRect{};
switch (PrepareScissoredClear(framebuffer, clearRect)) {
case ScissoredClearPrep::NoOp:
return;
case ScissoredClearPrep::NotNeeded:
break; // full-coverage scissor: the deferred whole-surface path below is equivalent
case ScissoredClearPrep::Ready:
RecordScissoredClearBuffer(framebuffer, buffer, drawbuffer, clearPayload, clearRect);
return;
}
}
auto queueAttachmentClear = [&](FramebufferAttachmentType attachmentType,
const ClearAttachmentPayload& payload) {
if (attachmentType == FramebufferAttachmentType::None || payload.mask == 0) {
return;
}
const auto& attachment = framebuffer.GetAttachment(attachmentType);
if (attachment.IsRenderbuffer()) {
m_renderPassManager->QueueRenderbufferClear(payload, attachment);
return;
}
if (!attachment.IsTexture()) {
return;
}
m_clearManager->QueueClear(payload, attachment);
};
// GL 3.3 §4.2.3: ClearBuffer* honors the write masks like Clear. Deferred
// clears cannot express partial masks; warn and skip those.
const auto depthClearAllowed = [&]() -> Bool { return MG_State::pGLContext->GetDepthMask(); };
const auto stencilClearAllowed = [&]() -> Bool {
const Uint32 stencilWriteMask = MG_State::pGLContext->GetStencilState(StencilFace::Front).WriteMask;
if ((stencilWriteMask & 0xFFu) == 0xFFu) {
return true;
}
if (stencilWriteMask != 0) {
MGLOG_W("DirectVulkan: deferred glClearBuffer with a partial stencil write mask is not supported");
}
return false;
};
switch (buffer) {
case GL_COLOR: {
const BoolVec4 colorMask = MG_State::pGLContext->GetColorMaskIndexed(static_cast<Uint32>(drawbuffer));
if (!colorMask.r() && !colorMask.g() && !colorMask.b() && !colorMask.a()) {
return;
}
if (!(colorMask.r() && colorMask.g() && colorMask.b() && colorMask.a())) {
MGLOG_W("DirectVulkan: deferred glClearBuffer with a partial color mask is not supported");
return;
}
queueAttachmentClear(framebuffer.GetDrawBuffers()[drawbuffer], clearPayload);
return;
}
case GL_DEPTH:
if (depthClearAllowed()) {
queueAttachmentClear(FramebufferAttachmentType::Depth, clearPayload);
}
return;
case GL_STENCIL:
if (stencilClearAllowed()) {
queueAttachmentClear(FramebufferAttachmentType::Stencil, clearPayload);
}
return;
case GL_DEPTH_STENCIL: {
ClearAttachmentPayload allowedPayload = clearPayload;
if (!depthClearAllowed()) {
allowedPayload.mask &= ~static_cast<GLbitfield>(GL_DEPTH_BUFFER_BIT);
}
if (!stencilClearAllowed()) {
allowedPayload.mask &= ~static_cast<GLbitfield>(GL_STENCIL_BUFFER_BIT);
}
if ((allowedPayload.mask & GL_DEPTH_BUFFER_BIT) != 0) {
queueAttachmentClear(FramebufferAttachmentType::Depth, allowedPayload);
}
if ((allowedPayload.mask & GL_STENCIL_BUFFER_BIT) != 0) {
queueAttachmentClear(FramebufferAttachmentType::Stencil, allowedPayload);
}
return;
}
default:
return;
}
}
void VulkanRenderer::RecordScissoredClearBuffer(const MG_State::GLState::FramebufferObject& framebuffer,
GLenum buffer, GLint drawbuffer,
const ClearAttachmentPayload& clearPayload,
const VkClearRect& clearRect) {
VkClearAttachment clearAttachment{};
if (buffer == GL_COLOR) {
const auto attachmentType = framebuffer.GetDrawBuffers()[drawbuffer];
if (attachmentType == FramebufferAttachmentType::None) {
return;
}
const auto& attachment = framebuffer.GetAttachment(attachmentType);
if (!attachment.IsComplete()) {
return;
}
const BoolVec4 colorMask = MG_State::pGLContext->GetColorMaskIndexed(static_cast<Uint>(drawbuffer));
if (!colorMask.r() && !colorMask.g() && !colorMask.b() && !colorMask.a()) {
return;
}
if (!colorMask.r() || !colorMask.g() || !colorMask.b() || !colorMask.a()) {
MGLOG_W("DirectVulkan: scissored glClearBuffer with a partial color mask is not supported");
return;
}
MG_State::GLState::ITextureObject* colorTexture = nullptr;
if (attachment.IsTexture()) {
colorTexture = attachment.GetTexture().get();
}
clearAttachment.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
clearAttachment.colorAttachment = static_cast<Uint32>(drawbuffer);
clearAttachment.clearValue.color = {
clearPayload.color.x(), clearPayload.color.y(), clearPayload.color.z(),
ResolveColorClearAlpha(colorTexture, clearPayload.color.w())
};
} else {
VkImageAspectFlags aspects = 0;
if ((clearPayload.mask & GL_DEPTH_BUFFER_BIT) != 0 && MG_State::pGLContext->GetDepthMask() &&
framebuffer.GetAttachment(FramebufferAttachmentType::Depth).IsComplete()) {
aspects |= VK_IMAGE_ASPECT_DEPTH_BIT;
}
if ((clearPayload.mask & GL_STENCIL_BUFFER_BIT) != 0 &&
framebuffer.GetAttachment(FramebufferAttachmentType::Stencil).IsComplete()) {
// GL 3.3 §4.2.3: the clear is masked by the front stencil write mask (see Clear).
const Uint32 stencilWriteMask =
MG_State::pGLContext->GetStencilState(StencilFace::Front).WriteMask;
if ((stencilWriteMask & 0xFFu) == 0xFFu) {
aspects |= VK_IMAGE_ASPECT_STENCIL_BIT;
} else if (stencilWriteMask != 0) {
MGLOG_W("DirectVulkan: scissored glClearBuffer with a partial stencil write mask is not supported");
}
}
if (aspects == 0) {
return;
}
clearAttachment.aspectMask = aspects;
clearAttachment.clearValue.depthStencil = {clearPayload.depth, clearPayload.stencil};
}
vkCmdClearAttachments(m_frameContext.GetCurrent().commandBuffer, 1, &clearAttachment, 1, &clearRect);
}
void VulkanRenderer::QueueClearBufferPayload(GLenum buffer, GLint drawbuffer,
@@ -3676,7 +4070,8 @@ void main() {
void VulkanRenderer::ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) {
ClearAttachmentPayload payload{};
payload.mask = GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT;
payload.depth = depth;
// Vulkan clear values require depth in [0,1] (VUID-VkClearDepthStencilValue-depth-00022).
payload.depth = std::clamp(depth, 0.0f, 1.0f);
payload.stencil = static_cast<Uint32>(stencil);
QueueClearBufferPayload(buffer, drawbuffer, payload);
}
@@ -3693,7 +4088,7 @@ void main() {
break;
case GL_DEPTH:
payload.mask = GL_DEPTH_BUFFER_BIT;
payload.depth = value[0];
payload.depth = std::clamp(value[0], 0.0f, 1.0f);
break;
default:
break;
@@ -3715,7 +4110,7 @@ void main() {
break;
case GL_DEPTH:
payload.mask = GL_DEPTH_BUFFER_BIT;
payload.depth = value[0];
payload.depth = std::clamp(value[0], 0.0f, 1.0f);
break;
default:
break;
@@ -3731,7 +4126,8 @@ void main() {
}
ClearAttachmentPayload payload{};
payload.mask = GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT;
payload.depth = depth;
// Vulkan clear values require depth in [0,1] (VUID-VkClearDepthStencilValue-depth-00022).
payload.depth = std::clamp(depth, 0.0f, 1.0f);
payload.stencil = static_cast<Uint32>(stencil);
QueueClearBufferPayloadForFramebuffer(*framebuffer, buffer, drawbuffer, payload);
}
@@ -4101,6 +4497,21 @@ void main() {
sourceTexture->GetExternalIndex());
}
if (!drawIsDefaultFbo) {
// A clear queued for the destination predates this blit in API order;
// execute it now, or its deferred materialization would later stomp the
// copied contents (MC 26.3 OIT clears cloud_depth, then blits the main
// depth into it - the stale loadOp=CLEAR erased the copy).
const auto destAttachmentType = ResolveFramebufferCopyAttachmentType(*drawFbo, false, dstBinding.aspectMask);
const auto& destAttachment = drawFbo->GetAttachment(destAttachmentType);
auto destTexture = destAttachment.GetTexture();
MOBILEGL_ASSERT(destTexture != nullptr, "BlitFramebuffer: depth destination texture attachment is null");
const Bool dstClearReady = MaterializePendingClearForTexture(frame.commandBuffer, *destTexture);
MOBILEGL_ASSERT(dstClearReady,
"BlitFramebuffer: failed to materialize pending clear for depth destination textureId=%d",
destTexture->GetExternalIndex());
}
const VkImageLayout srcOriginalLayout = readIsDefaultFbo
? m_swapchainObject.GetDepthStencilImageLayout(m_imageIndexAcquired)
: *srcBinding.trackedLayout;
@@ -4237,6 +4648,19 @@ void main() {
sourceTexture->GetExternalIndex());
}
if (!drawIsDefaultFbo) {
// A clear queued for the destination predates this blit in API order; execute
// it now, or its deferred materialization would later stomp the blitted color.
const auto& destAttachment = drawFbo->GetAttachment(drawFbo->GetDrawBuffers()[0]);
auto destTexture = destAttachment.GetTexture();
if (destTexture != nullptr) {
const Bool dstClearReady = MaterializePendingClearForTexture(frame.commandBuffer, *destTexture);
MOBILEGL_ASSERT(dstClearReady,
"BlitFramebuffer: failed to materialize pending clear for destination textureId=%d",
destTexture->GetExternalIndex());
}
}
VkImageLayout srcLayout = readIsDefaultFbo
? m_swapchainObject.GetImageLayout(m_imageIndexAcquired)
: *srcBinding.trackedLayout;
@@ -4431,6 +4855,15 @@ void main() {
sourceTexture->GetExternalIndex());
}
{
// A clear queued for the destination predates this copy in API order;
// execute it now so the deferred materialization cannot stomp the copy.
const Bool dstClearReady = MaterializePendingClearForTexture(frame.commandBuffer, *destinationTexture);
MOBILEGL_ASSERT(dstClearReady,
"CopyTexSubImage2D: failed to materialize pending clear for destination textureId=%d",
destinationTexture->GetExternalIndex());
}
const Bool srcUsesSwapchainDepth = readIsDefaultFbo && (srcBinding.aspectMask & VK_IMAGE_ASPECT_COLOR_BIT) == 0;
const VkImageLayout srcOriginalLayout = readIsDefaultFbo
? (srcUsesSwapchainDepth
@@ -5861,6 +6294,7 @@ void main() {
void VulkanRenderer::Present() {
MOBILEGL_ASSERT(m_imageIndexAcquired < m_swapchainObject.GetImageCount(),
"Present, acquired image index out of range");
m_renderPassManager->OnPresent();
auto& frame = m_frameContext.GetCurrent();
auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass();
if (activeRenderPass)
@@ -6257,6 +6691,10 @@ void main() {
deviceFeatures.multiDrawIndirect = supportedDeviceFeatures.multiDrawIndirect;
m_multiDrawIndirectFeatureEnabled = deviceFeatures.multiDrawIndirect == VK_TRUE;
m_logicOpFeatureEnabled = deviceFeatures.logicOp == VK_TRUE;
// Backs GL_TEXTURE_MAX_ANISOTROPY_EXT; optional in Vulkan, so the sampler manager falls back
// to isotropic filtering (and the extension goes unadvertised) when the device lacks it.
deviceFeatures.samplerAnisotropy = supportedDeviceFeatures.samplerAnisotropy;
m_samplerAnisotropyFeatureEnabled = deviceFeatures.samplerAnisotropy == VK_TRUE;
VkDeviceCreateInfo deviceCreateInfo{};
deviceCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
@@ -6752,7 +7190,8 @@ void main() {
static_cast<Uint32>(activeRenderPass->extent.y())
};
clearRect.baseArrayLayer = 0;
clearRect.layerCount = 1;
// Compatible entries share the framebuffer layer count; layered attachments clear every layer.
clearRect.layerCount = compatibleRenderPassEntry.layers;
for (const auto& pending : compatibleRenderPassEntry.pendingClearAttachments) {
if (!pending.hasInlinePayload && pending.key.texture == nullptr) {
@@ -6773,7 +7212,9 @@ void main() {
clearAttachment.clearValue.depthStencil = {1.0f, 0};
if ((clearPayload.mask & GL_COLOR_BUFFER_BIT) != 0) {
clearAttachment.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
clearAttachment.colorAttachment = pending.attachmentIndex;
// VkClearAttachment::colorAttachment indexes the subpass pColorAttachments (draw-buffer
// slot space, with UNUSED holes), not the compacted attachment descriptions.
clearAttachment.colorAttachment = pending.colorAttachmentSlot;
clearAttachment.clearValue.color = {
clearPayload.color.x(),
clearPayload.color.y(),
@@ -131,6 +131,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void ClearAttachmentsOnActiveRenderPass(VkCommandBuffer commandBuffer,
const RenderPassEntry& compatibleRenderPassEntry);
enum class ScissoredClearPrep {
NotNeeded, // scissor covers the whole target — take the deferred whole-surface path instead
NoOp, // nothing to clear (degenerate target or empty scissor rect)
Ready, // a render pass is active; record vkCmdClearAttachments with the returned rect
};
ScissoredClearPrep PrepareScissoredClear(const MG_State::GLState::FramebufferObject& framebuffer,
VkClearRect& outClearRect);
void Clear(GLbitfield mask);
void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value);
@@ -219,6 +227,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// frontend. Timestamp support (queue timestampValidBits > 0 and a
// non-zero timestampPeriod) is cached at device creation.
Bool IsTimerQuerySupported() const;
// The samplerAnisotropy device feature was granted, so GL_TEXTURE_MAX_ANISOTROPY_EXT is
// honored rather than accepted-and-ignored.
Bool IsSamplerAnisotropySupported() const { return m_samplerAnisotropyFeatureEnabled; }
// Ensures the frame command buffer is recording (same lazy pattern as
// SetupDraw) and writes a bottom-of-pipe timestamp into the current
// frame's pool. Null when unsupported or the pool is exhausted.
@@ -275,6 +286,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void QueueClearBufferPayloadForFramebuffer(const MG_State::GLState::FramebufferObject& framebuffer,
GLenum buffer, GLint drawbuffer,
const ClearAttachmentPayload& clearPayload);
void RecordScissoredClearBuffer(const MG_State::GLState::FramebufferObject& framebuffer,
GLenum buffer, GLint drawbuffer,
const ClearAttachmentPayload& clearPayload,
const VkClearRect& clearRect);
// ---- Submission fence tracking (GL sync objects) ----
// One record per vkQueueSubmit still in flight, in ascending submit
@@ -347,6 +362,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool m_indexTypeUint8ExtensionEnabled = false;
Bool m_logicOpFeatureEnabled = false;
Bool m_multiDrawIndirectFeatureEnabled = false;
Bool m_samplerAnisotropyFeatureEnabled = false;
Bool m_shaderDrawParametersExtensionEnabled = false;
Bool m_shaderDrawParametersFeatureEnabled = false;
// fillModeNonSolid gates VK_POLYGON_MODE_LINE/_POINT (glPolygonMode); independentBlend gates
@@ -17,3 +17,4 @@ target_link_libraries(
)
add_test(NAME BufferBench COMMAND BufferBench --benchmark_counters_tabular=true)
set_tests_properties(BufferBench PROPERTIES LABELS benchmark)
+1
View File
@@ -38,6 +38,7 @@ target_link_libraries(
)
add_test(NAME SanityBench COMMAND SanityBench --benchmark_counters_tabular=true)
set_tests_properties(SanityBench PROPERTIES LABELS benchmark)
add_subdirectory(Program)
add_subdirectory(Buffer)
@@ -17,3 +17,4 @@ target_link_libraries(
)
add_test(NAME ProgramBench COMMAND ProgramBench --benchmark_counters_tabular=true)
set_tests_properties(ProgramBench PROPERTIES LABELS benchmark)
+4 -1
View File
@@ -441,7 +441,10 @@ namespace MobileGL::MG_Impl::GLImpl {
for (SizeT i = 0; i < static_cast<SizeT>(n); ++i) {
Uint bufferName = buffers[i];
if (bufferName == 0) continue;
if (!BufferImpl::ValidateBufferName(bufferName, true)) continue;
// GL 3.3 core 2.9: names that do not correspond to an existing buffer are silently
// ignored here, so probe with the non-recording query - the shared validator would
// record INVALID_OPERATION, which is only correct on the bind path.
if (!MG_State::pGLContext->ValidateBufferName(bufferName)) continue;
MG_State::pGLContext->MarkBufferObjectForDeletion(bufferName);
}
}
@@ -67,7 +67,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
const auto& vao = MG_State::pGLContext->GetBoundVertexArray();
if (MG_State::pEGLContext->IsCurrentContextOpenGLCoreProfile() && vao && vao->GetExternalIndex() == 0) {
if (vao && vao->GetExternalIndex() == 0 && !MG_State::IsRelaxedSemanticsActive()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
@@ -60,6 +60,87 @@ namespace MobileGL::MG_Impl::GLImpl {
return false;
}
// Whether the backend can actually attach this color format to a framebuffer. Preferred
// source of truth is the backend's probed format-capability cache (real glCheckFramebufferStatus
// probes, so extensions like EXT_render_snorm are respected). Formats a probe-less backend
// cannot answer for fall back to a conservative static list of formats no ES driver renders to:
// shared-exponent, SNORM, three-channel norm16/float32/sRGB and three-channel integer formats.
// Desktop GL treats those as texture-only too (not in the GL 3.3 required-renderable list), so
// reporting GL_FRAMEBUFFER_UNSUPPORTED for them is legal.
Bool IsColorInternalFormatRenderable(TextureInternalFormat format) {
const SizeT formatIndex = static_cast<SizeT>(format);
if (MG_Backend::pActiveBackendObject && formatIndex < MG_Backend::kFormatCapabilityFormatCount) {
const auto& cache = MG_Backend::pActiveBackendObject->GetFormatCapabilities();
const SizeT sentinelFormat = static_cast<SizeT>(TextureInternalFormat::RGBA8);
Bool cachePopulated = false;
for (SizeT targetIndex = 0; targetIndex < MG_Backend::kFormatCapabilityTargetCount && !cachePopulated;
++targetIndex) {
cachePopulated = MG_Backend::HasFormatCapability(cache.FullCaps[targetIndex][sentinelFormat],
MG_Backend::FormatCapability::Creatable);
}
if (cachePopulated) {
for (SizeT targetIndex = 0; targetIndex < MG_Backend::kFormatCapabilityTargetCount;
++targetIndex) {
if (MG_Backend::HasFormatCapability(cache.FullCaps[targetIndex][formatIndex],
MG_Backend::FormatCapability::FramebufferRenderable) ||
MG_Backend::HasFormatCapability(cache.CaveatCaps[targetIndex][formatIndex],
MG_Backend::FormatCapability::FramebufferRenderable)) {
return true;
}
}
return false;
}
}
switch (format) {
case TextureInternalFormat::RGB9E5:
case TextureInternalFormat::R8Snorm:
case TextureInternalFormat::RG8Snorm:
case TextureInternalFormat::RGB8Snorm:
case TextureInternalFormat::RGBA8Snorm:
case TextureInternalFormat::R16Snorm:
case TextureInternalFormat::RG16Snorm:
case TextureInternalFormat::RGB16Snorm:
case TextureInternalFormat::RGBA16Snorm:
case TextureInternalFormat::RGB16:
case TextureInternalFormat::RGB10: // stored as RGB16
case TextureInternalFormat::RGB12: // stored as RGB16
case TextureInternalFormat::RGB16F:
case TextureInternalFormat::RGB32F:
case TextureInternalFormat::RGB8I:
case TextureInternalFormat::RGB8UI:
case TextureInternalFormat::RGB16I:
case TextureInternalFormat::RGB16UI:
case TextureInternalFormat::RGB32I:
case TextureInternalFormat::RGB32UI:
case TextureInternalFormat::SRGB8:
return false;
default:
return true;
}
}
Bool HasNonRenderableColorAttachment(const MG_State::GLState::FramebufferObject& framebufferObject) {
const auto& attachments = framebufferObject.GetAllAttachmentObjects();
for (SizeT i = 0; i < attachments.size(); ++i) {
const auto type = static_cast<FramebufferAttachmentType>(i);
if (type < FramebufferAttachmentType::Color0 || type > FramebufferAttachmentType::Color31) {
continue;
}
const auto& attachment = attachments[i];
if (!attachment.IsValid()) continue;
TextureInternalFormat format = TextureInternalFormat::Unknown;
if (attachment.IsTexture() && attachment.GetTexture()) {
format = attachment.GetTexture()->GetFormat();
} else if (attachment.IsRenderbuffer() && attachment.GetRenderbuffer()) {
format = attachment.GetRenderbuffer()->GetInternalFormat();
}
if (format != TextureInternalFormat::Unknown && !IsColorInternalFormatRenderable(format)) {
return true;
}
}
return false;
}
void RecordUnsupportedFramebufferTextureAttachmentError(const char* functionName, const char* detail) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
@@ -556,6 +637,62 @@ namespace MobileGL::MG_Impl::GLImpl {
return framebufferObject;
}
// Attaches a single layer/slice of a 3D or array texture. The attachment model stores the layer
// index; the DirectGLES backend attaches it with glFramebufferTextureLayer.
static void AttachFramebufferTextureLayer(const char* functionName, GLenum target, GLenum attachment,
GLuint texture, GLint level, GLint layer,
TextureUploadTarget textureUploadTarget) {
if (target == GL_FRAMEBUFFER) {
target = GL_DRAW_FRAMEBUFFER;
}
if (attachment == GL_DEPTH_STENCIL_ATTACHMENT) {
AttachFramebufferTextureLayer(functionName, target, GL_DEPTH_ATTACHMENT, texture, level, layer,
textureUploadTarget);
AttachFramebufferTextureLayer(functionName, target, GL_STENCIL_ATTACHMENT, texture, level, layer,
textureUploadTarget);
return;
}
const FramebufferAttachmentType attachmentType = MG_Util::ConvertGLEnumToFramebufferAttachmentType(attachment);
const FramebufferTarget framebufferTarget = MG_Util::ConvertGLEnumToFramebufferTarget(target);
if (!FramebufferImpl::ValidateFramebufferAttachmentType(attachmentType)) return;
if (!FramebufferImpl::ValidateFramebufferTarget(framebufferTarget)) return;
if (!TextureImpl::ValidateTextureName(texture, true)) return;
auto& bindingSlot = MG_State::pGLContext->GetFramebufferBindingSlot(framebufferTarget);
auto& framebufferObject = bindingSlot.GetBoundObject();
if (!framebufferObject) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
"Framebuffer target is bound to no framebuffer object."));
return;
}
if (texture == 0) {
framebufferObject->Detach(attachmentType);
return;
}
auto& textureObject = MG_State::pGLContext->GetTextureObject(texture);
if (!textureObject) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
std::format("Texture object {} is not valid.", texture)));
return;
}
if (layer < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "Layer must be non-negative."));
return;
}
framebufferObject->AttachTexture(attachmentType, textureObject, textureUploadTarget, level, layer,
/*layered=*/false);
}
void FramebufferTextureLayer_State(GLenum target, GLenum attachment, GLuint texture, GLint level, GLint layer) {
if (texture == 0) {
const TextureUploadTarget detachTarget = TextureUploadTarget::Texture2D;
@@ -563,10 +700,31 @@ namespace MobileGL::MG_Impl::GLImpl {
return;
}
static_cast<void>(layer);
RecordUnsupportedFramebufferTextureAttachmentError(
__func__,
"Layered framebuffer texture attachments are not represented by the current framebuffer attachment model.");
auto& textureObject = MG_State::pGLContext->GetTextureObject(texture);
if (!textureObject) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
std::format("Texture object {} is not valid.", texture)));
return;
}
TextureUploadTarget textureUploadTarget = TextureUploadTarget::Unknown;
switch (textureObject->GetTarget()) {
case TextureTarget::Texture3D:
textureUploadTarget = TextureUploadTarget::Texture3D;
break;
case TextureTarget::Texture2DArray:
textureUploadTarget = TextureUploadTarget::Texture2DArray;
break;
case TextureTarget::Texture2DMultisampleArray:
textureUploadTarget = TextureUploadTarget::Texture2DMultisampleArray;
break;
default:
RecordUnsupportedFramebufferTextureAttachmentError(
__func__, "FramebufferTextureLayer requires a 3D, 2D array or 2D multisample array texture.");
return;
}
AttachFramebufferTextureLayer(__func__, target, attachment, texture, level, layer, textureUploadTarget);
}
void FramebufferTexture3D_State(GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level,
@@ -578,10 +736,15 @@ namespace MobileGL::MG_Impl::GLImpl {
return;
}
static_cast<void>(zoffset);
RecordUnsupportedFramebufferTextureAttachmentError(
__func__,
"3D framebuffer texture slice attachments are not represented by the current framebuffer attachment model.");
if (textarget != GL_TEXTURE_3D) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"FramebufferTexture3D requires GL_TEXTURE_3D."));
return;
}
AttachFramebufferTextureLayer(__func__, target, attachment, texture, level, zoffset,
TextureUploadTarget::Texture3D);
}
void FramebufferTexture2D_State(GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level) {
@@ -1014,7 +1177,7 @@ namespace MobileGL::MG_Impl::GLImpl {
auto& bindingSlot = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw);
auto& fbo = bindingSlot.GetBoundObject();
const bool isDefaultFBO = (fbo == FramebufferImpl::pDefaultFramebufferInfo->defaultFBO);
static GLenum bufs[] = {buf};
const GLenum bufs[] = {buf};
DrawBuffersForFramebuffer_State(fbo, isDefaultFBO, 1, bufs, true);
}
}
@@ -1201,7 +1364,9 @@ namespace MobileGL::MG_Impl::GLImpl {
for (SizeT i = 0; i < static_cast<SizeT>(n); ++i) {
Uint bufferName = renderbuffers[i];
if (bufferName == 0) continue;
if (!FramebufferImpl::ValidateRenderbufferName(bufferName)) continue;
// GL 3.3 core 4.4.2: unknown names are silently ignored on delete; the shared bind-path
// validator would record INVALID_OPERATION instead.
if (!MG_State::pGLContext->ValidateRenderbufferName(bufferName)) continue;
MG_State::pGLContext->MarkRenderbufferObjectForDeletion(bufferName);
}
}
@@ -1224,7 +1389,9 @@ namespace MobileGL::MG_Impl::GLImpl {
for (SizeT i = 0; i < static_cast<SizeT>(n); ++i) {
Uint bufferName = framebuffers[i];
if (bufferName == 0) continue;
if (!FramebufferImpl::ValidateFramebufferName(bufferName)) continue;
// GL 3.3 core 4.4.1: unknown names are silently ignored on delete; the shared bind-path
// validator would record INVALID_OPERATION instead.
if (!MG_State::pGLContext->ValidateFramebufferName(bufferName)) continue;
MG_State::pGLContext->MarkFramebufferObjectForDeletion(bufferName);
}
}
@@ -1252,6 +1419,9 @@ namespace MobileGL::MG_Impl::GLImpl {
GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT :
GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT;
}
if (HasNonRenderableColorAttachment(*framebufferObject)) {
return GL_FRAMEBUFFER_UNSUPPORTED;
}
if (IsActiveBackendDirectVulkan() &&
IsUnsupportedFramebufferForDirectVulkan(*framebufferObject)) {
return GL_FRAMEBUFFER_UNSUPPORTED;
@@ -1277,6 +1447,9 @@ namespace MobileGL::MG_Impl::GLImpl {
GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT :
GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT;
}
if (HasNonRenderableColorAttachment(*framebufferObject)) {
return GL_FRAMEBUFFER_UNSUPPORTED;
}
if (IsActiveBackendDirectVulkan() &&
IsUnsupportedFramebufferForDirectVulkan(*framebufferObject)) {
return GL_FRAMEBUFFER_UNSUPPORTED;
@@ -1626,8 +1799,8 @@ namespace MobileGL::MG_Impl::GLImpl {
return false;
}
// Check framebuffer completeness
if (!framebufferObject->CheckCompleteness()) {
// Check framebuffer completeness (including formats the ES pipeline cannot attach)
if (!framebufferObject->CheckCompleteness() || HasNonRenderableColorAttachment(*framebufferObject)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidFramebufferOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ReadPixels_State", "Framebuffer is incomplete"));
@@ -1679,6 +1852,40 @@ namespace MobileGL::MG_Impl::GLImpl {
"No color buffer for color format"));
return false;
}
// GL 3.3 section 4.3.1: GL_INVALID_OPERATION if format is an integer format and the read
// buffer is not an integer format, or vice versa (GL CTS packed_pixels expects the error
// for every *_INTEGER readback from a normalized attachment).
const auto& readAttachment = framebufferObject->GetAttachment(readBuffer);
TextureInternalFormat attachmentFormat = TextureInternalFormat::Unknown;
if (readAttachment.IsTexture() && readAttachment.GetTexture()) {
attachmentFormat = readAttachment.GetTexture()->GetFormat();
} else if (readAttachment.IsRenderbuffer() && readAttachment.GetRenderbuffer()) {
attachmentFormat = readAttachment.GetRenderbuffer()->GetInternalFormat();
}
if (attachmentFormat != TextureInternalFormat::Unknown &&
TextureImpl::IsIntegerColorInputFormat(textureInputFormat) !=
TextureImpl::IsIntegerColorInternalFormat(attachmentFormat)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ReadPixels_State",
"Integer-ness of format does not match the read buffer"));
return false;
}
}
// Packed-type/format pairing (GL CTS packed_pixels: e.g. GL_RED with GL_UNSIGNED_SHORT_5_6_5 must
// raise an error instead of reaching the backend). Shared with the TexImage/GetTexImage validators;
// runs after the depth-stencil branch above so DEPTH_STENCIL with a wrong type keeps GL_INVALID_ENUM.
if (!TextureImpl::ValidateClientFormatTypePairing(textureInputFormat, texturePixelDataType)) {
return false;
}
// Packed-type/format pairing (GL CTS packed_pixels: e.g. GL_RED with GL_UNSIGNED_SHORT_5_6_5 must
// raise an error instead of reaching the backend). Shared with the TexImage/GetTexImage validators;
// runs after the depth-stencil branch above so DEPTH_STENCIL with a wrong type keeps GL_INVALID_ENUM.
if (!TextureImpl::ValidateClientFormatTypePairing(textureInputFormat, texturePixelDataType)) {
return false;
}
// Check PBO state
@@ -76,7 +76,13 @@ namespace MobileGL::MG_Impl::GLImpl::FramebufferImpl {
}
Bool ValidateRenderbufferName(Uint index, Bool allowZero) {
if (index == 0 && !allowZero) {
if (index == 0) {
// Zero is never a GenRenderbuffers name, so it must not reach the name-table lookup
// below: where it is allowed (glBindRenderbuffer / FramebufferRenderbuffer detach) it
// means "unbind", and looking it up would record a bogus INVALID_OPERATION - GL CTS's
// per-case state reset calls glBindRenderbuffer(GL_RENDERBUFFER, 0) after every case.
if (allowZero) return true;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl/FramebufferImpl", "ValidateRenderbufferName",
+23 -2
View File
@@ -529,6 +529,13 @@ namespace MobileGL::MG_Impl::GLImpl {
params[1] = dynamicParameters.AliasedLineWidthRangeMax;
return;
}
case GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT: {
// EXT_texture_filter_anisotropic queries this as a float; the integer path below widens
// from here, so this case is the authoritative one.
const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
params[0] = dynamicParameters.MaxTextureMaxAnisotropy;
return;
}
case GL_ALIASED_POINT_SIZE_RANGE:
case GL_POINT_SIZE_RANGE: {
const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
@@ -1727,7 +1734,11 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = 1024 * 1024; // TODO
return;
case GL_CONTEXT_PROFILE_MASK:
*params = GL_CONTEXT_CORE_PROFILE_BIT;
// Reports the requested context profile (EGL defaults 3.x contexts to core);
// MOBILEGL_RELAXED_SEMANTICS loosens behavior without changing the identity.
*params = MG_State::pEGLContext && MG_State::pEGLContext->IsCurrentContextOpenGLCompatibilityProfile()
? GL_CONTEXT_COMPATIBILITY_PROFILE_BIT
: GL_CONTEXT_CORE_PROFILE_BIT;
return;
default:
break;
@@ -1894,7 +1905,13 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = dynamicParameters.MaxTextureSize;
break;
case GL_MAX_UNIFORM_BUFFER_BINDINGS:
*params = std::max(dynamicParameters.MaxUniformBufferBindings, kFrontendMinUniformBufferBindings);
// Never advertise more bindings than the state layer's indexed-binding array can track
// (BufferState::BufferBindingPointCount): glBindBufferBase rejects indices past that
// capacity, and the GL CTS per-case state reset calls glBindBufferBase on every
// advertised index and expects no error. The floor equals the GL 3.3 core minimum
// (36), so the clamp never under-advertises.
*params = std::clamp(dynamicParameters.MaxUniformBufferBindings, kFrontendMinUniformBufferBindings,
static_cast<GLint>(MG_State::GLState::BufferBindingPointCount));
break;
case GL_MAX_UNIFORM_BLOCK_SIZE:
*params = dynamicParameters.MaxUniformBlockSize;
@@ -1955,6 +1972,10 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_MAX_SAMPLES:
*params = std::max(dynamicParameters.MaxSamples, kFrontendMaxSamples);
break;
case GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT:
// Float state (see GetFloatv); rounded to nearest for the integer query per GL 3.3 6.1.2.
*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);
MG_State::pGLContext->RecordError(ErrorCode::InvalidEnum,
+78 -7
View File
@@ -339,6 +339,9 @@ namespace MobileGL::MG_Impl::GLImpl {
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Shader is not attached to program."));
return;
}
// A shader flagged with glDeleteShader lives on while attached; this detach may
// have been its last GL-visible attachment.
MG_State::pGLContext->ReleaseShaderNameIfOrphaned(shader);
}
void GetActiveAttrib_State(GLuint program, GLuint index, GLsizei bufSize, GLsizei* length, GLint* size,
@@ -735,6 +738,12 @@ namespace MobileGL::MG_Impl::GLImpl {
auto size = programObject->GetUniformSizesInBytes(location);
char* pUBO = (char*)programObject->MapUBO();
auto* ttype = programObject->GetUniformTType(location);
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
offset + size > programObject->GetUBOSize()) {
MGLOG_E("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__,
program, location);
return;
}
if (!ttype->isMatrix() || ttype->getMatrixCols() != 3)
Memcpy(params, pUBO + offset, size);
@@ -784,6 +793,12 @@ namespace MobileGL::MG_Impl::GLImpl {
auto size = programObject->GetUniformSizesInBytes(location);
char* pUBO = static_cast<char*>(programObject->MapUBO());
auto* ttype = programObject->GetUniformTType(location);
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
offset + size > programObject->GetUBOSize()) {
MGLOG_E("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__,
program, location);
return;
}
if constexpr (std::is_same_v<T, GLfloat>) {
if (ttype->isMatrix() && ttype->getMatrixCols() == 3) {
@@ -900,14 +915,31 @@ namespace MobileGL::MG_Impl::GLImpl {
if (!programObject.IsUniformOpaqueAtLocation(location)) {
MGLOG_D("%s: program = %d, location = %d, maxLocation = %d", __func__, programObject.GetExternalIndex(),
location, programObject.GetMaxUniformLocation());
auto size = programObject.GetUniformSizesInBytes(location);
auto offset = programObject.GetUniformOffset(location);
MOBILEGL_ASSERT(size >= ItemCount * sizeof(T),
"Uniform size mismatch, expected at least %zu bytes, got %zu bytes.", ItemCount * sizeof(T),
size);
const SizeT size = programObject.GetUniformSizesInBytes(location);
const Uint offset = programObject.GetUniformOffset(location);
char* pUBO = static_cast<char*>(programObject.MapUBO());
const SizeT uboSize = programObject.GetUBOSize();
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 "
"bytes; clamping",
__func__, programObject.GetExternalIndex(), location, ItemCount * sizeof(T), size);
writeSize = size;
}
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
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 "
"uboSize=%zu); dropping write",
__func__, programObject.GetExternalIndex(), location, static_cast<void*>(pUBO), offset,
writeSize, uboSize);
return;
}
MGLOG_D("%s: program = %d, location = %d, byteOffset = %d", __func__, programObject.GetExternalIndex(),
location, offset + byteOffsetInsideUniform);
Memcpy((char*)programObject.MapUBO() + offset + byteOffsetInsideUniform, value, ItemCount * sizeof(T));
Memcpy(pUBO + offset + byteOffsetInsideUniform, value, writeSize);
programObject.MarkUBOContentDirty();
} else {
auto* ttype = programObject.GetUniformTType(location);
@@ -940,6 +972,11 @@ namespace MobileGL::MG_Impl::GLImpl {
}
for (GLint offset = 0; offset < count; offset++) {
if (offset > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + offset)) {
// GL 3.3 §2.11.4: values for elements beyond the end of the uniform
// array are ignored. Never step onto a neighboring uniform's location.
break;
}
if (!programObject->IsValidUniformLocation(location + offset)) {
RecordInvalidUniformLocationError(__func__, location + offset, "the current program object");
return;
@@ -964,6 +1001,10 @@ namespace MobileGL::MG_Impl::GLImpl {
}
for (GLint offset = 0; offset < count; offset++) {
if (offset > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + offset)) {
// Values for elements beyond the end of the uniform array are ignored.
break;
}
if (!programObject->IsValidUniformLocation(location + offset)) {
RecordInvalidUniformLocationError(__func__, location + offset,
"program " + std::to_string(program));
@@ -1092,6 +1133,10 @@ namespace MobileGL::MG_Impl::GLImpl {
// For matrix uniforms, we handle each matrix individually
for (GLint i = 0; i < count; i++) {
if (i > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + i)) {
// Values for elements beyond the end of the uniform array are ignored.
break;
}
if (!programObject->IsValidUniformLocation(location + i)) {
RecordInvalidUniformLocationError(__func__, location + i, "the current program object");
return;
@@ -1124,6 +1169,10 @@ namespace MobileGL::MG_Impl::GLImpl {
// For matrix uniforms, we handle each matrix individually
// Handle padding in mat3 correctly!!
for (GLint i = 0; i < count; i++) {
if (i > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + i)) {
// Values for elements beyond the end of the uniform array are ignored.
break;
}
if (!programObject->IsValidUniformLocation(location + i)) {
RecordInvalidUniformLocationError(__func__, location + i, "the current program object");
return;
@@ -1159,6 +1208,10 @@ namespace MobileGL::MG_Impl::GLImpl {
// For matrix uniforms, we handle each matrix individually
for (GLint i = 0; i < count; i++) {
if (i > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + i)) {
// Values for elements beyond the end of the uniform array are ignored.
break;
}
if (!programObject->IsValidUniformLocation(location + i)) {
RecordInvalidUniformLocationError(__func__, location + i, "the current program object");
return;
@@ -1219,6 +1272,10 @@ namespace MobileGL::MG_Impl::GLImpl {
}
for (GLint i = 0; i < count; i++) {
if (i > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + i)) {
// Values for elements beyond the end of the uniform array are ignored.
break;
}
if (!programObject->IsValidUniformLocation(location + i)) {
RecordInvalidUniformLocationError(__func__, location + i, "program " + std::to_string(program));
return;
@@ -1249,6 +1306,10 @@ namespace MobileGL::MG_Impl::GLImpl {
}
for (GLint i = 0; i < count; i++) {
if (i > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + i)) {
// Values for elements beyond the end of the uniform array are ignored.
break;
}
if (!programObject->IsValidUniformLocation(location + i)) {
RecordInvalidUniformLocationError(__func__, location + i, "program " + std::to_string(program));
return;
@@ -1283,6 +1344,10 @@ namespace MobileGL::MG_Impl::GLImpl {
}
for (GLint i = 0; i < count; i++) {
if (i > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + i)) {
// Values for elements beyond the end of the uniform array are ignored.
break;
}
if (!programObject->IsValidUniformLocation(location + i)) {
RecordInvalidUniformLocationError(__func__, location + i, "program " + std::to_string(program));
return;
@@ -1341,6 +1406,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
const auto& index = programObject->GetUniformBlockIndex(uniformBlockName);
MGLOG_D("GBI prog=%u name='%s' -> %d", program, uniformBlockName ? uniformBlockName : "(null)", (Int)index);
return index;
}
@@ -1364,6 +1430,7 @@ namespace MobileGL::MG_Impl::GLImpl {
std::to_string(program) + "."));
return;
}
MGLOG_D("UBB prog=%u idx=%u binding=%u", program, uniformBlockIndex, uniformBlockBinding);
programObject->SetUniformBlockBinding(uniformBlockIndex, uniformBlockBinding);
}
@@ -1435,9 +1502,13 @@ namespace MobileGL::MG_Impl::GLImpl {
MGLOG_D("%s: GL_UNIFORM_BLOCK_REFERENCED_BY_COMPUTE_SHADER = %d", __func__, *params);
break;
case GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES: {
// Member entries of an arrayed block are recorded against the first instance;
// every instance of the array reports that shared member set (matches
// GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS, which scans with the same owner index).
const Int ownerIndex = static_cast<Int>(programObject->GetUniformBlockMemberOwnerIndex(uniformBlockIndex));
GLint uniformIndexCount = 0;
for (Uint uniformIndex = 0; uniformIndex < programObject->GetUniformCount(); ++uniformIndex) {
if (programObject->GetActiveUniformBlockIndex(uniformIndex) != static_cast<Int>(uniformBlockIndex)) {
if (programObject->GetActiveUniformBlockIndex(uniformIndex) != ownerIndex) {
continue;
}
params[uniformIndexCount++] = static_cast<GLint>(uniformIndex);
@@ -649,7 +649,9 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void ClearDepth_State(GLclampd depth) {
MG_State::pGLContext->SetClearDepth(static_cast<Float>(depth));
// GL 3.3 §4.2.3: the clear depth is clamped to [0,1] at specification time (Vulkan clear
// values additionally require it: VUID-VkClearDepthStencilValue-depth-00022).
MG_State::pGLContext->SetClearDepth(ClampUnitFloat(static_cast<Float>(depth)));
}
void ClearColor_State(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha) {
+44 -8
View File
@@ -14,7 +14,13 @@
namespace MobileGL::MG_Impl::GLImpl {
namespace {
Bool ValidateSamplerParameterValue(GLenum pname, const void* param, Bool isFloat, Bool isInteger) {
Float ReadSamplerScalar(const void* param, Bool isFloat, Bool isUnsignedInteger) {
if (isFloat) return *(const GLfloat*)param;
if (isUnsignedInteger) return static_cast<Float>(*(const GLuint*)param);
return static_cast<Float>(*(const GLint*)param);
}
Bool ValidateSamplerParameterValue(GLenum pname, const void* param, Bool isFloat, Bool isUnsignedInteger) {
if (param == nullptr) return false;
switch (pname) {
@@ -22,6 +28,13 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_TEXTURE_MAX_LOD:
case GL_TEXTURE_LOD_BIAS:
return true;
case GL_TEXTURE_MAX_ANISOTROPY_EXT:
if (ReadSamplerScalar(param, isFloat, isUnsignedInteger) >= 1.0f) return true;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "SetSamplerParam_State",
"GL_TEXTURE_MAX_ANISOTROPY_EXT must be at least 1.0."));
return false;
default:
break;
}
@@ -29,14 +42,15 @@ namespace MobileGL::MG_Impl::GLImpl {
if (isFloat) {
return SamplerImpl::ValidateSamplerFloatParam(pname, *(const GLfloat*)param);
}
if (isInteger) {
if (isUnsignedInteger) {
return SamplerImpl::ValidateSamplerIntParam(pname, static_cast<GLint>(*(const GLuint*)param));
}
return SamplerImpl::ValidateSamplerIntParam(pname, *(const GLint*)param);
}
} // namespace
void SetSamplerParam_State(GLuint sampler, GLenum pname, const void* param, bool isFloat, bool isInteger) {
void SetSamplerParam_State(GLuint sampler, GLenum pname, const void* param, bool isFloat,
bool isUnsignedInteger) {
if (param == nullptr) return;
if (!SamplerImpl::ValidateSamplerName(sampler)) return;
@@ -47,7 +61,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
auto& samplerObj = MG_State::pGLContext->GetSamplerObject(sampler);
if (!SamplerImpl::ValidateSamplerObject(sampler)) return;
if (!ValidateSamplerParameterValue(pname, param, isFloat, isInteger)) return;
if (!ValidateSamplerParameterValue(pname, param, isFloat, isUnsignedInteger)) return;
using namespace MG_Util;
switch (pname) {
@@ -76,6 +90,9 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_TEXTURE_LOD_BIAS:
samplerObj->SetLodBias(*(const GLfloat*)param);
break;
case GL_TEXTURE_MAX_ANISOTROPY_EXT:
samplerObj->SetMaxAnisotropy(ReadSamplerScalar(param, isFloat, isUnsignedInteger));
break;
case GL_TEXTURE_COMPARE_MODE:
samplerObj->SetCompareMode(MG_Util::ConvertGLEnumToSamplerCompareMode(*(const GLint*)param));
break;
@@ -89,7 +106,8 @@ namespace MobileGL::MG_Impl::GLImpl {
}
}
void GetSamplerParam_State(GLuint sampler, GLenum pname, void* params, bool isFloat, bool isInteger) {
void GetSamplerParam_State(GLuint sampler, GLenum pname, void* params, bool isFloat,
bool isUnsignedInteger) {
if (params == nullptr) return;
if (!SamplerImpl::ValidateSamplerName(sampler)) return;
@@ -129,6 +147,15 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_TEXTURE_LOD_BIAS:
*(GLfloat*)params = samplerObj->GetLodBias();
break;
case GL_TEXTURE_MAX_ANISOTROPY_EXT:
if (isFloat) {
*(GLfloat*)params = samplerObj->GetMaxAnisotropy();
} else if (isUnsignedInteger) {
*(GLuint*)params = static_cast<GLuint>(samplerObj->GetMaxAnisotropy());
} else {
*(GLint*)params = static_cast<GLint>(samplerObj->GetMaxAnisotropy());
}
break;
case GL_TEXTURE_COMPARE_MODE:
*(GLuint*)params = MG_Util::ConvertSamplerCompareModeToGLEnum(samplerObj->GetCompareMode());
break;
@@ -206,7 +233,16 @@ namespace MobileGL::MG_Impl::GLImpl {
if (sampler == 0) {
textureUnit.SetSamplerObject(nullptr);
} else {
if (!SamplerImpl::ValidateSamplerName(sampler)) return;
// GL 3.3 core 3.8.2: BindSampler on a name GenSamplers never returned - or one already
// deleted - is INVALID_OPERATION. SamplerParameter* raises INVALID_VALUE for the same
// name, which is why this cannot go through the shared SamplerImpl validator.
if (!MG_State::pGLContext->ValidateSamplerName(sampler)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "BindSampler_State",
std::format("Invalid sampler name {}", sampler)));
return;
}
Bool doesSamplerObjectCreated = MG_State::pGLContext->ValidateSamplerObject(sampler);
if (!doesSamplerObjectCreated) {
MG_State::pGLContext->CreateSamplerObject(sampler);
@@ -240,7 +276,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void SamplerParameterIiv(GLuint sampler, GLenum pname, const GLint* param) {
SetSamplerParam_State(sampler, pname, param, false, true);
SetSamplerParam_State(sampler, pname, param, false, false);
}
void SamplerParameteriv(GLuint sampler, GLenum pname, const GLint* param) {
@@ -268,7 +304,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void GetSamplerParameterIiv(GLuint sampler, GLenum pname, GLint* params) {
GetSamplerParam_State(sampler, pname, params, false, true);
GetSamplerParam_State(sampler, pname, params, false, false);
}
void GetSamplerParameterfv(GLuint sampler, GLenum pname, GLfloat* params) {
@@ -99,6 +99,16 @@ namespace MobileGL::MG_Impl::GLImpl::SamplerImpl {
case GL_TEXTURE_LOD_BIAS:
return true;
case GL_TEXTURE_MAX_ANISOTROPY_EXT:
if (!(param >= 1.0f)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateSamplerFloatParam",
"GL_TEXTURE_MAX_ANISOTROPY_EXT must be at least 1.0."));
return false;
}
return true;
case GL_TEXTURE_BORDER_COLOR:
if (param < 0.0f || param > 1.0f) {
MG_State::pGLContext->RecordError(
@@ -125,6 +135,16 @@ namespace MobileGL::MG_Impl::GLImpl::SamplerImpl {
}
return true;
case GL_TEXTURE_MAX_ANISOTROPY_EXT:
if (param < 1) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateSamplerIntParam",
"GL_TEXTURE_MAX_ANISOTROPY_EXT must be at least 1."));
return false;
}
return true;
default:
return ValidateSamplerParam(pname, static_cast<GLenum>(param));
}
+176 -39
View File
@@ -74,6 +74,16 @@ namespace MobileGL::MG_Impl::GLImpl {
return true;
}
Bool ValidateMaxAnisotropy(Float maxAnisotropy, const char* caller) {
if (maxAnisotropy >= 1.0f) return true;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"GL_TEXTURE_MAX_ANISOTROPY_EXT must be at least 1.0."));
return false;
}
template <typename Fn>
void WithTemporarilyBoundNamedTexture(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
Fn&& fn) {
@@ -284,10 +294,16 @@ namespace MobileGL::MG_Impl::GLImpl {
MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS));
}
Uint ComputeFullMipmapLevelCount(const IntVec3& baseTexelSize) {
// Array targets store their layer count in z; layers never participate in mip
// reduction (GL 3.3 §3.8.14), only true 3D textures halve their depth per level.
Bool DepthParticipatesInMipmapping(TextureTarget target) {
return target == TextureTarget::Texture3D;
}
Uint ComputeFullMipmapLevelCount(const IntVec3& baseTexelSize, Bool depthMips) {
Int maxDimension = std::max<Int>(
baseTexelSize.x(),
std::max<Int>(baseTexelSize.y(), std::max<Int>(baseTexelSize.z(), 1)));
std::max<Int>(baseTexelSize.y(), depthMips ? std::max<Int>(baseTexelSize.z(), 1) : 1));
Uint mipLevelCount = 1;
while (maxDimension > 1) {
maxDimension = std::max<Int>(maxDimension / 2, 1);
@@ -296,11 +312,12 @@ namespace MobileGL::MG_Impl::GLImpl {
return mipLevelCount;
}
IntVec3 ComputeMipmapTexelSize(const IntVec3& baseTexelSize, Uint relativeLevel) {
IntVec3 ComputeMipmapTexelSize(const IntVec3& baseTexelSize, Uint relativeLevel, Bool depthMips) {
return {
std::max<Int>(baseTexelSize.x() >> static_cast<Int>(relativeLevel), 1),
std::max<Int>(baseTexelSize.y() >> static_cast<Int>(relativeLevel), 1),
std::max<Int>(baseTexelSize.z() >> static_cast<Int>(relativeLevel), 1),
depthMips ? std::max<Int>(baseTexelSize.z() >> static_cast<Int>(relativeLevel), 1)
: std::max<Int>(baseTexelSize.z(), 1),
};
}
@@ -323,9 +340,10 @@ namespace MobileGL::MG_Impl::GLImpl {
}
const SizeT bytesPerTexel = baseByteSize / baseTexelCount;
const Uint requiredLevelCount = ComputeFullMipmapLevelCount(baseTexelSize);
const Bool depthMips = DepthParticipatesInMipmapping(texture.GetTarget());
const Uint requiredLevelCount = ComputeFullMipmapLevelCount(baseTexelSize, depthMips);
for (Uint level = 1; level < requiredLevelCount; ++level) {
const IntVec3 levelTexelSize = ComputeMipmapTexelSize(baseTexelSize, level);
const IntVec3 levelTexelSize = ComputeMipmapTexelSize(baseTexelSize, level, depthMips);
const SizeT levelByteSize = bytesPerTexel * static_cast<SizeT>(levelTexelSize.x()) *
static_cast<SizeT>(levelTexelSize.y()) *
static_cast<SizeT>(levelTexelSize.z());
@@ -404,13 +422,10 @@ namespace MobileGL::MG_Impl::GLImpl {
"2D multisample textures must use depth 1."));
return false;
}
if (textureTarget == TextureTarget::Texture2DMultisampleArray && depth == 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"2D multisample array textures must have at least one layer."));
return false;
}
// Zero layers is NOT an error for multisample arrays: depth == 0 (like width/height
// == 0) deallocates the image - GL 4.5 8.8 only raises INVALID_VALUE for negative
// dimensions, and GL CTS's per-case state reset (gluStateReset) clears the default
// GL_TEXTURE_2D_MULTISAMPLE_ARRAY texture with glTexImage3DMultisample(..., 0, 0, 0).
const Int maxSamples = GetMaxSupportedTextureSamples(textureInternalFormat);
if (samples > maxSamples) {
@@ -458,6 +473,9 @@ namespace MobileGL::MG_Impl::GLImpl {
Bool ValidateTextureParameterForTarget(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
GLenum pname, GLint param, const char* caller) {
const auto target = textureObject->GetTarget();
if (pname == GL_TEXTURE_MAX_ANISOTROPY_EXT && !ValidateMaxAnisotropy(param, caller)) {
return false;
}
if ((pname == GL_TEXTURE_BASE_LEVEL || pname == GL_TEXTURE_MAX_LEVEL) && param < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
@@ -487,7 +505,8 @@ namespace MobileGL::MG_Impl::GLImpl {
(pname == GL_TEXTURE_WRAP_S || pname == GL_TEXTURE_WRAP_T || pname == GL_TEXTURE_WRAP_R ||
pname == GL_TEXTURE_MIN_FILTER || pname == GL_TEXTURE_MAG_FILTER || pname == GL_TEXTURE_MIN_LOD ||
pname == GL_TEXTURE_MAX_LOD || pname == GL_TEXTURE_LOD_BIAS || pname == GL_TEXTURE_COMPARE_MODE ||
pname == GL_TEXTURE_COMPARE_FUNC || pname == GL_TEXTURE_BORDER_COLOR)) {
pname == GL_TEXTURE_COMPARE_FUNC || pname == GL_TEXTURE_BORDER_COLOR ||
pname == GL_TEXTURE_MAX_ANISOTROPY_EXT)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
@@ -557,6 +576,14 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_TEXTURE_SWIZZLE_A: {
auto swizzleParam = MG_Util::ConvertGLEnumPnameToTextureSwizzleParam(pname);
auto swizzleValue = MG_Util::ConvertGLEnumToTextureSwizzleParam(param);
if (swizzleValue == TextureSwizzleParam::Unknown) {
// GL CTS texture_swizzle.api_errors: single-value TexParameter* with a value outside
// [RED, GREEN, BLUE, ALPHA, ZERO, ONE] must raise GL_INVALID_ENUM.
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Invalid texture swizzle value."));
return;
}
textureObject->SetSwizzleParam(swizzleParam, swizzleValue);
break;
}
@@ -578,6 +605,9 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_TEXTURE_LOD_BIAS:
textureObject->GetSamplerObject()->SetLodBias((GLfloat)param);
break;
case GL_TEXTURE_MAX_ANISOTROPY_EXT:
textureObject->GetSamplerObject()->SetMaxAnisotropy(static_cast<GLfloat>(param));
break;
case GL_GENERATE_MIPMAP:
g_autoGenerateMipmapByTextureId[textureObject->GetExternalIndex()] = (param != GL_FALSE);
break;
@@ -594,7 +624,10 @@ namespace MobileGL::MG_Impl::GLImpl {
void TextureParameterObjectf_State(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, GLenum pname,
GLfloat param, const char* caller) {
if (!textureObject) return;
if (!ValidateTextureParameterForTarget(textureObject, pname, static_cast<GLint>(param), caller)) return;
if (pname == GL_TEXTURE_MAX_ANISOTROPY_EXT && !ValidateMaxAnisotropy(param, caller)) return;
const GLint validationParam =
pname == GL_TEXTURE_MAX_ANISOTROPY_EXT ? 1 : static_cast<GLint>(param);
if (!ValidateTextureParameterForTarget(textureObject, pname, validationParam, caller)) return;
switch (pname) {
case GL_TEXTURE_MAG_FILTER:
@@ -640,6 +673,9 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_TEXTURE_LOD_BIAS:
textureObject->GetSamplerObject()->SetLodBias(param);
break;
case GL_TEXTURE_MAX_ANISOTROPY_EXT:
textureObject->GetSamplerObject()->SetMaxAnisotropy(param);
break;
case GL_GENERATE_MIPMAP:
g_autoGenerateMipmapByTextureId[textureObject->GetExternalIndex()] = (param != 0.0f);
break;
@@ -709,6 +745,9 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = (GLint)MG_Util::ConvertSamplerCompareFuncToGLEnum(
textureObject->GetSamplerObject()->GetSamplerCompareFunc());
break;
case GL_TEXTURE_MAX_ANISOTROPY_EXT:
*params = static_cast<GLint>(textureObject->GetSamplerObject()->GetMaxAnisotropy());
break;
default:
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
@@ -734,6 +773,23 @@ namespace MobileGL::MG_Impl::GLImpl {
}
}
// Texture-parameter lookups must not raise GL_INVALID_OPERATION when the default texture
// (name 0) is bound: glTexParameter* on default textures is legal GL (the GL CTS state reset
// sets swizzles/levels on texture 0 for every unit x target and expects glGetError() to stay
// clean). Name 0 resolves to the target's real default texture object, so parameters set on
// it are stored and queryable like on any texture.
const SharedPtr<MG_State::GLState::ITextureObject>& GetTextureObjectByTargetForParameter(
TextureUploadTarget textureUploadTarget, TextureTarget textureTarget) {
if (TextureImpl::IsProxyTextureTarget(textureUploadTarget)) {
return TextureImpl::pProxyTextureManager->GetProxyTextureObject(textureUploadTarget);
}
if (textureTarget == TextureTarget::Unknown) {
return nullTextureObject;
}
auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
return activeUnit.GetBindingSlot(textureTarget).GetBoundObject();
}
void GenerateMipmap_Backend(GLenum target) {
MG_Backend::gBackendFunctionsTable.GL.GenerateMipmap(target);
}
@@ -779,6 +835,12 @@ namespace MobileGL::MG_Impl::GLImpl {
MOBILEGL_ASSERT(nullptr != static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get()),
"Texture object here should always be an object with mipmap");
auto textureMipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
if (static_cast<Uint>(level) >= textureMipmapObject->GetMipmapLevelCount()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Texture level is out of range."));
return;
}
const void* originalPixels = pixels;
const auto& pixelUnpackBufferObject =
@@ -811,6 +873,14 @@ namespace MobileGL::MG_Impl::GLImpl {
const SizeT destRowSize = static_cast<SizeT>(texelSize.x()) * internalBpp;
const SizeT destSliceSize = static_cast<SizeT>(texelSize.y()) * destRowSize;
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");
free(processedPixels);
return;
}
const auto* srcData = static_cast<const Uint8*>(processedPixels);
Uint8* destData = static_cast<Uint8*>(textureMipmapObject->MapMipmapData(textureUploadTarget, level));
if (destData) {
@@ -865,11 +935,11 @@ namespace MobileGL::MG_Impl::GLImpl {
auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget);
auto& textureObject = bindingSlot.GetBoundObject();
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
TextureInternalFormat textureInternalFormat = textureObject->GetFormat();
MGLOG_D("%s: working on texture %d", __func__, textureObject->GetExternalIndex());
// ===================== Error Checking ==============================
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
if (!TextureImpl::ValidateTextureSubImageOffsets(textureObject, xoffset, width, yoffset, height)) return;
if (!TextureImpl::ValidateTextureInternalFormatCompatibleWithInput(textureInputFormat, textureInternalFormat,
texturePixelDataType))
@@ -1027,7 +1097,7 @@ namespace MobileGL::MG_Impl::GLImpl {
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
// ======================= Processing ================================
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
if (!textureObject) return;
switch (pname) {
@@ -1060,6 +1130,12 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_TEXTURE_SWIZZLE_A: {
auto swizzleParam = MG_Util::ConvertGLEnumPnameToTextureSwizzleParam(pname);
auto swizzleValue = MG_Util::ConvertGLEnumToTextureSwizzleParam((GLenum)param);
if (swizzleValue == TextureSwizzleParam::Unknown) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Invalid texture swizzle value."));
return;
}
textureObject->SetSwizzleParam(swizzleParam, swizzleValue);
break;
}
@@ -1083,6 +1159,10 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_TEXTURE_LOD_BIAS:
textureObject->GetSamplerObject()->SetLodBias(param);
break;
case GL_TEXTURE_MAX_ANISOTROPY_EXT:
if (!ValidateMaxAnisotropy(param, __func__)) return;
textureObject->GetSamplerObject()->SetMaxAnisotropy(param);
break;
case GL_GENERATE_MIPMAP:
g_autoGenerateMipmapByTextureId[textureObject->GetExternalIndex()] = (param != 0.0f);
break;
@@ -1106,7 +1186,7 @@ namespace MobileGL::MG_Impl::GLImpl {
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
// ======================= Processing ================================
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
if (!textureObject) return;
TextureParameterObject_State(textureObject, pname, param, __func__);
@@ -1122,7 +1202,7 @@ namespace MobileGL::MG_Impl::GLImpl {
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
// ======================= Processing ================================
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
if (!textureObject) return;
SetTextureBorderColorFromFloats(textureObject, params);
break;
@@ -1130,7 +1210,7 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_TEXTURE_SWIZZLE_RGBA: {
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
if (!textureObject) return;
GLint signedParams[4] = {static_cast<GLint>(params[0]), static_cast<GLint>(params[1]),
static_cast<GLint>(params[2]), static_cast<GLint>(params[3])};
@@ -1153,7 +1233,7 @@ namespace MobileGL::MG_Impl::GLImpl {
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
// ======================= Processing ================================
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
if (!textureObject) return;
SetTextureBorderColorFromInts(textureObject, params);
break;
@@ -1161,7 +1241,7 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_TEXTURE_SWIZZLE_RGBA: {
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
if (!textureObject) return;
if (!SetTextureSwizzleParamsFromInts(textureObject, params, __func__)) {
return;
@@ -1179,7 +1259,7 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_TEXTURE_BORDER_COLOR: {
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
if (!textureObject) return;
SetTextureBorderColorFromIntegerInts(textureObject, params);
break;
@@ -1187,7 +1267,7 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_TEXTURE_SWIZZLE_RGBA: {
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
if (!textureObject) return;
GLint signedParams[4] = {static_cast<GLint>(params[0]), static_cast<GLint>(params[1]),
static_cast<GLint>(params[2]), static_cast<GLint>(params[3])};
@@ -1207,7 +1287,7 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_TEXTURE_BORDER_COLOR: {
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
if (!textureObject) return;
SetTextureBorderColorFromUnsignedInts(textureObject, params);
break;
@@ -1218,7 +1298,7 @@ namespace MobileGL::MG_Impl::GLImpl {
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
// ======================= Processing ================================
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
if (!textureObject) return;
Vec4<TextureSwizzleParam> swizzleParams;
@@ -1269,6 +1349,8 @@ namespace MobileGL::MG_Impl::GLImpl {
auto& textureObject =
isProxy ? TextureImpl::pProxyTextureManager->CreateOrReplaceProxyTextureObject(textureUploadTarget)
: bindingSlot.GetBoundObject();
// Name 0 resolves to the target's default texture object - a real texture this call
// (re)specifies like any other; the slot is never empty anymore.
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
if (textureObject->GetStorageType() != TextureStorageType::Mipmap) {
MG_State::pGLContext->RecordError(
@@ -1310,6 +1392,8 @@ namespace MobileGL::MG_Impl::GLImpl {
auto& textureObject =
isProxy ? TextureImpl::pProxyTextureManager->CreateOrReplaceProxyTextureObject(textureUploadTarget)
: bindingSlot.GetBoundObject();
// Name 0 resolves to the target's default texture object - a real texture this call
// (re)specifies like any other; the slot is never empty anymore.
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
if (textureObject->GetStorageType() != TextureStorageType::Mipmap) {
MG_State::pGLContext->RecordError(
@@ -1378,6 +1462,8 @@ namespace MobileGL::MG_Impl::GLImpl {
// target and data is not evenly divisible into the number of bytes needed to store in memory a datum
// indicated by type.
// ======================= Processing ================================
textureInternalFormat =
MG_Util::ConvertInternalFormatToSized(textureInternalFormat, textureInputFormat, texturePixelDataType);
auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget);
Bool isProxy = TextureImpl::IsProxyTextureTarget(textureUploadTarget);
@@ -1386,6 +1472,8 @@ namespace MobileGL::MG_Impl::GLImpl {
: bindingSlot.GetBoundObject();
// ===================== Error Checking ==============================
// Name 0 resolves to the target's default texture object - a real texture this call
// (re)specifies like any other; the slot is never empty anymore.
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
if (!ValidateTextureMutable(textureObject, __func__)) return;
@@ -1419,7 +1507,11 @@ namespace MobileGL::MG_Impl::GLImpl {
auto textureMipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
// Allocate in TextureObject
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, height, depth}, internalBytes});
if (isProxy) {
MGLOG_D("%s: isProxy = true, not allocating", __func__);
} else {
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, height, depth}, internalBytes});
}
if (!originalPixels) {
MGLOG_D("%s: No input pixel and no PBO bound, no pixel transfer", __func__);
@@ -1446,6 +1538,7 @@ namespace MobileGL::MG_Impl::GLImpl {
textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, true);
free(processedPixels);
MaybeAutoGenerateMipmap(target, textureObject, isProxy, level);
}
void TexImage2D_State(GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border,
@@ -1500,6 +1593,8 @@ namespace MobileGL::MG_Impl::GLImpl {
: bindingSlot.GetBoundObject();
// ===================== Error Checking ==============================
// Name 0 resolves to the target's default texture object - a real texture this call
// (re)specifies like any other; the slot is never empty anymore.
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
if (!ValidateTextureMutable(textureObject, __func__)) return;
@@ -1604,6 +1699,8 @@ namespace MobileGL::MG_Impl::GLImpl {
auto& textureObject =
isProxy ? TextureImpl::pProxyTextureManager->CreateOrReplaceProxyTextureObject(textureUploadTarget)
: bindingSlot.GetBoundObject();
// Name 0 resolves to the target's default texture object - a real texture this call
// (re)specifies like any other; the slot is never empty anymore.
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
if (!ValidateTextureMutable(textureObject, __func__)) return;
@@ -1659,8 +1756,12 @@ namespace MobileGL::MG_Impl::GLImpl {
if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return;
if (!TextureImpl::ValidateTextureInternalFormat(textureInternalFormat)) return;
// TODO: make sure `internalformat` is in one of supported format for TexBuffer
// GL 3.3 core 3.8.5: buffer zero detaches any buffer from the buffer texture - only a
// nonzero name that is not an existing buffer object is an error. This is reachable on
// the default buffer texture (bound whenever texture 0 is bound to GL_TEXTURE_BUFFER),
// which the GL CTS state reset detaches with glTexBuffer(..., 0) after every case.
auto& bufferObject = MG_State::pGLContext->GetBufferObject(buffer);
if (!bufferObject) {
if (buffer != 0 && !bufferObject) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
@@ -1674,6 +1775,8 @@ namespace MobileGL::MG_Impl::GLImpl {
auto& textureObject = bindingSlot.GetBoundObject();
// ===================== Error Checking ==============================
// Name 0 is the default buffer texture - a real object the (de)attach operates on, not a
// silent no-op; the slot is never empty now that every unit/target holds its default.
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
if (textureObject->GetStorageType() != TextureStorageType::Buffer) {
MG_State::pGLContext->RecordError(
@@ -1694,7 +1797,9 @@ namespace MobileGL::MG_Impl::GLImpl {
GLboolean IsTexture_State(GLuint texture) {
// ======================= Processing ================================
if (!TextureImpl::ValidateTextureName(texture, true)) return GL_FALSE;
// GL 3.3 core 6.1.4: IsTexture generates no error - an unknown, deleted or merely reserved
// name is just GL_FALSE. Probing with the recording validator (as every other Is* entry
// point already avoids doing) would leave a spurious INVALID_VALUE behind.
return MG_State::pGLContext->ValidateTextureObject(texture) ? GL_TRUE : GL_FALSE;
}
@@ -1883,6 +1988,11 @@ namespace MobileGL::MG_Impl::GLImpl {
textureObject->GetSamplerObject()->GetSamplerCompareFunc());
}
break;
case GL_TEXTURE_MAX_ANISOTROPY_EXT:
if (params) {
*params = static_cast<GLint>(textureObject->GetSamplerObject()->GetMaxAnisotropy());
}
break;
case GL_IMAGE_FORMAT_COMPATIBILITY_TYPE:
if (params) {
*params = GL_IMAGE_FORMAT_COMPATIBILITY_BY_SIZE;
@@ -2029,6 +2139,11 @@ namespace MobileGL::MG_Impl::GLImpl {
textureObject->GetSamplerObject()->GetSamplerCompareFunc());
}
break;
case GL_TEXTURE_MAX_ANISOTROPY_EXT:
if (params) {
*params = textureObject->GetSamplerObject()->GetMaxAnisotropy();
}
break;
default:
MG_State::pGLContext->RecordError(ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetTexParameterfv_State",
@@ -2300,7 +2415,7 @@ namespace MobileGL::MG_Impl::GLImpl {
for (SizeT i = 0; i < static_cast<SizeT>(n); ++i) {
Uint textureName = textures[i];
if (textureName == 0) continue;
if (!TextureImpl::ValidateTextureName(textureName, true)) continue;
if (!MG_State::pGLContext->ValidateTextureName(textureName)) continue;
MG_State::pGLContext->MarkTextureObjectForDeletion(textureName);
}
}
@@ -2507,11 +2622,13 @@ namespace MobileGL::MG_Impl::GLImpl {
// ===================== Error Checking ==============================
if (!TextureImpl::ValidateTextureTarget(textureTarget)) return;
// Name 0 unbinds the current target from the active texture unit.
// GL 3.3 core 3.8: name 0 is the target's default texture object - a real texture that
// glTexImage*/glTexParameter*/glGetTex* must operate on - not "nothing bound". Binding it
// restores the unit/target slot to its initial state.
if (texture == 0) {
auto& currentUnit = MG_State::pGLContext->GetTextureUnitObject(activeUnit);
auto& bindingSlot = currentUnit.GetBindingSlot(textureTarget);
bindingSlot.Bind(nullptr);
bindingSlot.Bind(MG_State::pGLContext->GetDefaultTextureObject(textureTarget));
MG_State::pGLContext->NoteTextureUnitTouched(activeUnit);
return;
}
@@ -2523,6 +2640,9 @@ namespace MobileGL::MG_Impl::GLImpl {
return;
}
// GL 3.3 core 3.8.1: a name that GenTextures never returned - or that has since been deleted -
// is not a legal bind target in the core profile (no application-generated names), and the error
// is INVALID_OPERATION, not INVALID_VALUE.
if (!MG_State::pGLContext->ValidateTextureName(texture)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
@@ -2530,8 +2650,6 @@ namespace MobileGL::MG_Impl::GLImpl {
return;
}
if (!TextureImpl::ValidateTextureName(texture, true)) return;
// ======================= Processing ================================
Bool doesTextureExist = MG_State::pGLContext->ValidateTextureObject(texture);
if (!doesTextureExist) {
@@ -2551,14 +2669,25 @@ namespace MobileGL::MG_Impl::GLImpl {
void ActiveTexture_State(GLenum texture) {
// ===================== Error Checking ==============================
if (texture < GL_TEXTURE0 || texture > GL_TEXTURE31) {
// GL 3.3 core 3.8: the valid range is [GL_TEXTURE0, GL_TEXTURE0 +
// GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS) - NOT a fixed 0..31 range. GL CTS's per-case
// state reset iterates every advertised combined unit, so rejecting units the
// implementation itself reports would leave a sticky GL_INVALID_ENUM behind and abort
// whole test batches. The backend already clamps its advertised value to the state
// layer's MAX_TEXTURE_IMAGE_UNITS capacity.
Int maxCombinedUnits = static_cast<Int>(MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS);
if (MG_Backend::pActiveBackendObject) {
maxCombinedUnits = std::min(
maxCombinedUnits, MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxCombinedTextureImageUnits);
}
if (texture < GL_TEXTURE0 || static_cast<Int>(texture - GL_TEXTURE0) >= maxCombinedUnits) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", "ActiveTexture_State",
std::format("Texture must be one of GL_TEXTUREi, where i is in the range 0 to 31, but got "
std::format("Texture must be one of GL_TEXTUREi, where i is in the range 0 to {}, but got "
"invalid enum: 0x{:X}, which may stand for unit {}.",
texture, texture - GL_TEXTURE0)));
maxCombinedUnits - 1, texture, texture - GL_TEXTURE0)));
return;
}
@@ -2918,10 +3047,13 @@ namespace MobileGL::MG_Impl::GLImpl {
auto* textureMipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
textureObject->SetInternalFormat(textureInternalFormat);
// Array targets keep their layer count constant across levels; only true 3D
// textures halve depth per level (GL 3.3 §3.9 glTexStorage3D).
const Bool depthMips = DepthParticipatesInMipmapping(textureObject->GetTarget());
for (GLsizei level = 0; level < levels; ++level) {
const GLsizei levelWidth = std::max<GLsizei>(1, width >> level);
const GLsizei levelHeight = std::max<GLsizei>(1, height >> level);
const GLsizei levelDepth = std::max<GLsizei>(1, depth >> level);
const GLsizei levelDepth = depthMips ? std::max<GLsizei>(1, depth >> level) : depth;
const SizeT byteSize = ComputeTextureStorageByteSize(textureInternalFormat, levelWidth, levelHeight,
levelDepth);
textureMipmapObject->AllocateStorage(textureUploadTarget, level,
@@ -2957,6 +3089,7 @@ namespace MobileGL::MG_Impl::GLImpl {
auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget);
auto& textureObject = bindingSlot.GetBoundObject();
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
if (!TextureImpl::ValidateTextureNotDefault(textureObject, __func__)) return;
TextureStorage1D(textureObject->GetExternalIndex(), levels, internalformat, width);
}
@@ -2971,6 +3104,7 @@ namespace MobileGL::MG_Impl::GLImpl {
auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget);
auto& textureObject = bindingSlot.GetBoundObject();
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
if (!TextureImpl::ValidateTextureNotDefault(textureObject, __func__)) return;
TextureStorage2D(textureObject->GetExternalIndex(), levels, internalformat, width, height);
}
@@ -2986,6 +3120,7 @@ namespace MobileGL::MG_Impl::GLImpl {
auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget);
auto& textureObject = bindingSlot.GetBoundObject();
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
if (!TextureImpl::ValidateTextureNotDefault(textureObject, __func__)) return;
TextureStorage3D(textureObject->GetExternalIndex(), levels, internalformat, width, height, depth);
}
@@ -3141,8 +3276,10 @@ namespace MobileGL::MG_Impl::GLImpl {
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(static_cast<Int>(unit));
MG_State::pGLContext->NoteTextureUnitTouched(static_cast<Int>(unit));
if (texture == 0) {
// GL 4.5 8.1: texture zero unbinds every target of the unit, i.e. rebinds each
// target's default texture object (the unit's initial state).
for (auto& slot : textureUnit.GetAllBindingSlots()) {
slot.Bind(nullptr);
slot.Bind(MG_State::pGLContext->GetDefaultTextureObject(slot.GetTarget()));
}
return;
}
+45 -11
View File
@@ -175,13 +175,15 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
return true;
}
static Bool IsIntegerColorInputFormat(TextureInputFormat format) {
Bool IsIntegerColorInputFormat(TextureInputFormat format) {
return format == TextureInputFormat::RInteger || format == TextureInputFormat::RGInteger ||
format == TextureInputFormat::RGBInteger || format == TextureInputFormat::BGRInteger ||
format == TextureInputFormat::RGBAInteger || format == TextureInputFormat::BGRAInteger;
format == TextureInputFormat::RGBAInteger || format == TextureInputFormat::BGRAInteger ||
format == TextureInputFormat::GreenInteger || format == TextureInputFormat::BlueInteger ||
format == TextureInputFormat::AlphaInteger;
}
static Bool IsIntegerColorInternalFormat(TextureInternalFormat internalFormat) {
Bool IsIntegerColorInternalFormat(TextureInternalFormat internalFormat) {
switch (internalFormat) {
case TextureInternalFormat::R8I:
case TextureInternalFormat::R8UI:
@@ -235,17 +237,15 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
format == TextureInputFormat::StencilIndex;
}
// Mirrors the desktop-GL validity matrix used by GL CTS packed_pixels (glcPackedPixelsTests
// isFormatValid, INPUT_TEXIMAGE): packed-type/format pairing, depth-vs-color mismatch, and
// integer-ness matching all raise GL_INVALID_OPERATION instead of reaching the upload path.
Bool ValidateTextureInternalFormatCompatibleWithInput(TextureInputFormat format,
TextureInternalFormat internalFormat,
TexturePixelDataType type) {
// Client-memory format<->type pairing rules shared by pixel uploads (TexImage*) and readbacks
// (ReadPixels, GetTexImage). Mirrors the desktop-GL validity matrix used by GL CTS packed_pixels
// (glcPackedPixelsTests isFormatValid): packed types constrain the formats they may pair with, and
// integer formats reject floating-point types; violations raise GL_INVALID_OPERATION.
Bool ValidateClientFormatTypePairing(TextureInputFormat format, TexturePixelDataType type) {
const auto recordInvalidOperation = [](const char* message) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateTextureInternalFormatCompatibleWithInput",
message));
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateClientFormatTypePairing", message));
return false;
};
@@ -288,6 +288,27 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
return recordInvalidOperation("Integer format cannot be used with a floating-point type");
}
return true;
}
// Mirrors the desktop-GL validity matrix used by GL CTS packed_pixels (glcPackedPixelsTests
// isFormatValid, INPUT_TEXIMAGE): packed-type/format pairing, depth-vs-color mismatch, and
// integer-ness matching all raise GL_INVALID_OPERATION instead of reaching the upload path.
Bool ValidateTextureInternalFormatCompatibleWithInput(TextureInputFormat format,
TextureInternalFormat internalFormat,
TexturePixelDataType type) {
const auto recordInvalidOperation = [](const char* message) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateTextureInternalFormatCompatibleWithInput",
message));
return false;
};
if (!ValidateClientFormatTypePairing(format, type)) {
return false;
}
// TexImage in core 3.3 has no stencil-only upload path (that arrived with GL 4.4).
if (format == TextureInputFormat::StencilIndex) {
return recordInvalidOperation("STENCIL_INDEX is not a valid texture upload format");
@@ -339,6 +360,19 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
return true;
}
Bool ValidateTextureNotDefault(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
const char* caller) {
if (textureObject && textureObject->GetExternalIndex() == 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"This operation is not allowed on the default texture (zero is "
"bound to the target)."));
return false;
}
return true;
}
Bool ValidateTextureTargetUniformity(SharedPtr<MG_State::GLState::ITextureObject> textureObject,
TextureTarget target) {
if (!textureObject) return true; // should be created later
@@ -23,11 +23,19 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
Bool ValidateTextureSizeRange(Int width, Int height, Int depth);
Bool ValidateTextureInternalFormat(TextureInternalFormat format);
Bool ValidateTextureBorderNumber(Int border);
Bool IsIntegerColorInputFormat(TextureInputFormat format);
Bool IsIntegerColorInternalFormat(TextureInternalFormat internalFormat);
Bool ValidateClientFormatTypePairing(TextureInputFormat format, TexturePixelDataType type);
Bool ValidateTextureInternalFormatCompatibleWithInput(TextureInputFormat format,
TextureInternalFormat internalFormat,
TexturePixelDataType type);
Bool ValidateTextureLevelWithUploadTarget(TextureUploadTarget target, Int level);
Bool ValidateTextureObject(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
// error is generated if zero is bound to target", ARB_texture_storage).
Bool ValidateTextureNotDefault(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
const char* caller);
Bool ValidateTextureTargetUniformity(SharedPtr<MG_State::GLState::ITextureObject> textureObject,
TextureTarget target);
Bool ValidateTextureSubImageOffsets(SharedPtr<MG_State::GLState::ITextureObject> textureObject, Int xoffset,
@@ -78,15 +78,12 @@ namespace MobileGL::MG_Impl::GLImpl {
}
static bool ValidateCurrentVertexAttribIndex(GLuint index, const char* funcName) {
if (!VertexArrayImpl::ValidateVertexAttributeIndex(index)) return false;
if (index == 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
"Generic vertex attribute 0 current value cannot be modified."));
return false;
}
return true;
// GL 3.3 core 2.7: VertexAttrib* sets the current value of ANY generic attribute,
// including index 0 - only an out-of-range index is an error (INVALID_VALUE).
// "Attribute 0 is immutable" was legacy immediate-mode lore; rejecting it broke GL
// CTS's per-case state reset, which writes vertexAttrib4f(0, 0,0,0,1) after every case.
static_cast<void>(funcName);
return VertexArrayImpl::ValidateVertexAttributeIndex(index);
}
static bool TryGetVertexAttribute(GLuint index, const MG_State::GLState::VertexAttribute** outAttr) {
@@ -288,7 +285,9 @@ namespace MobileGL::MG_Impl::GLImpl {
GLuint vao = arrays[i];
if (vao == 0) continue;
if (!VertexArrayImpl::ValidateVertexArrayName(vao)) continue;
// GL 3.3 core 2.10: unknown names are silently ignored on delete; the shared bind-path
// validator would record INVALID_OPERATION instead.
if (!MG_State::pGLContext->ValidateVertexArrayName(vao)) continue;
if (MG_State::pGLContext->GetBoundVertexArray() &&
MG_State::pGLContext->GetBoundVertexArray() == MG_State::pGLContext->GetVertexArrayObject(vao)) {
+14
View File
@@ -764,6 +764,20 @@ namespace MobileGL {
ctx->MajorVersion > 3 || (ctx->MajorVersion == 3 && ctx->MinorVersion >= 1);
}
Bool EGLContext::IsCurrentContextOpenGLCompatibilityProfile() const {
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
auto currentIt = m_threadCurrents.find(CurrentThreadKey());
if (currentIt == m_threadCurrents.end()) {
return false;
}
const auto* ctx = TryGetContext(currentIt->second.Context);
// Affirmative check: true only when the host explicitly requested the
// compatibility bit. Attrib-less contexts report false and thus read as core
// for GL_CONTEXT_PROFILE_MASK (EGL defaults 3.x contexts to the core profile).
return ctx && ctx->ClientAPI == EGL_OPENGL_API &&
(ctx->OpenGLProfileMask & EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT);
}
EGLint EGLContext::GetCurrentContextFlags() const {
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
auto currentIt = m_threadCurrents.find(CurrentThreadKey());
+1
View File
@@ -59,6 +59,7 @@ namespace MobileGL {
Bool ValidateContext(EGLContextHandle context) const;
Bool ValidateContextOnDisplay(EGLDisplayHandle display, EGLContextHandle context) const;
Bool IsCurrentContextOpenGLCoreProfile() const;
Bool IsCurrentContextOpenGLCompatibilityProfile() const;
EGLint GetCurrentContextFlags() const;
// Surface
+15 -1
View File
@@ -9,6 +9,7 @@
#include "Core.h"
#include "MG_State/GLState/RenderbufferState/RenderbufferObject.h"
#include "MG_State/EGLState/Core.h"
#include <Config.h>
namespace MobileGL::MG_State {
void Init() {
@@ -17,6 +18,11 @@ namespace MobileGL::MG_State {
pEGLContext = MakeUnique<EGLState::EGLContext>();
}
Bool IsRelaxedSemanticsActive() {
return MG_Config::Features.RelaxedSemantics ||
!(pEGLContext && pEGLContext->IsCurrentContextOpenGLCoreProfile());
}
namespace GLState {
// Error
void GLContext::RecordError(ErrorCode code, UniquePtr<ErrorInfo> info) {
@@ -226,12 +232,16 @@ namespace MobileGL::MG_State {
return m_textureState.GetTextureObject(index);
}
const SharedPtr<ITextureObject>& GLContext::GetDefaultTextureObject(TextureTarget target) const {
return m_textureState.GetDefaultTextureObject(target);
}
const SharedPtr<ITextureObject>& GLContext::CreateTextureObject(Uint index, TextureTarget target) {
return m_textureState.CreateTextureObject(index, target);
}
void GLContext::MarkTextureObjectForDeletion(Uint index) {
m_textureState.MarkTextureObjectForDeletion(index);
m_textureState.MarkTextureObjectForDeletion(index, IsRelaxedSemanticsActive());
}
TextureUnit& GLContext::GetTextureUnitObject(Int unit) {
@@ -279,6 +289,10 @@ namespace MobileGL::MG_State {
return m_programState.MarkShaderObjectForDeletion(index);
}
void GLContext::ReleaseShaderNameIfOrphaned(const Uint index) {
return m_programState.ReleaseShaderNameIfOrphaned(index);
}
Bool GLContext::ValidateProgramName(const Uint index) const {
return m_programState.ValidateProgramObject(index);
}
+13
View File
@@ -95,6 +95,8 @@ namespace MobileGL {
// Texture
void GenTextureNames(Uint number, Vector<Uint>& textures);
const SharedPtr<ITextureObject>& GetTextureObject(Uint index);
// Per-target default texture object (name 0); see TextureState::GetDefaultTextureObject.
const SharedPtr<ITextureObject>& GetDefaultTextureObject(TextureTarget target) const;
const SharedPtr<ITextureObject>& CreateTextureObject(Uint index, TextureTarget target);
void MarkTextureObjectForDeletion(Uint index);
TextureUnit& GetTextureUnitObject(Int unit);
@@ -106,6 +108,7 @@ namespace MobileGL {
// texture is bound at a unit; lets a backend skip re-resolving an unchanged
// per-draw sampled-texture set.
Uint64 GetTextureBindGeneration() const { return m_textureState.GetTextureBindGeneration(); }
void BumpTextureBindGeneration() { m_textureState.BumpTextureBindGeneration(); }
Bool ValidateTextureName(Uint index) const;
Bool ValidateTextureObject(Uint index) const;
Int GetActiveTextureUnit() const;
@@ -116,6 +119,9 @@ namespace MobileGL {
Uint CreateShader(ShaderStage stage);
void MarkProgramForDeletion(Uint index);
void MarkShaderForDeletion(Uint index);
// Frees a deletion-flagged shader's name once it lost its last GL-visible
// attachment (call after glDetachShader).
void ReleaseShaderNameIfOrphaned(Uint index);
Bool ValidateProgramName(Uint index) const;
Bool ValidateShaderName(Uint index) const;
const SharedPtr<ProgramObject>& GetProgramObject(Uint index);
@@ -247,5 +253,12 @@ namespace MobileGL {
} // namespace GLState
extern UniquePtr<GLState::GLContext> pGLContext;
// True when relaxed GL semantics apply. Strict core rules are enforced only when the
// current EGL context explicitly requested a core profile (core bit in
// EGL_CONTEXT_OPENGL_PROFILE_MASK, or a >=3.1 version request without the compatibility
// bit) and MOBILEGL_RELAXED_SEMANTICS is off; no current context, legacy version
// requests, and the compatibility bit all relax.
Bool IsRelaxedSemanticsActive();
} // namespace MG_State
} // namespace MobileGL
+10 -1
View File
@@ -7,6 +7,7 @@
// End of Source File Header
#include "Error.h"
#include <algorithm>
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
#include <MG_Util/Converters/MGToGL/ErrorCodeConverter.h>
@@ -19,7 +20,15 @@ namespace MobileGL::MG_State::GLState {
MGLOG_E("Recording OpenGL error (%s):\n%s",
MG_Util::ConvertGLEnumToString(MG_Util::ConvertErrorCodeToGLEnum(code)).c_str(),
info->toString().c_str());
m_errors.push_back(MakeUnique<Error>(code, Move(info)));
// GL error semantics are sticky flags, not a queue (GL 3.3 core §2.5): with multiple
// error flags, each is set only while currently unset — repeated errors of the same
// code are discarded until glGetError reads the flag. Unbounded accumulation leaked
// stale errors into later, unrelated glGetError checks (GL CTS deinit noise).
const Bool alreadyPending = std::any_of(m_errors.begin(), m_errors.end(),
[code](const auto& e) { return e->code == code; });
if (!alreadyPending) {
m_errors.push_back(MakeUnique<Error>(code, Move(info)));
}
}
}
@@ -8,6 +8,7 @@
#include "ProgramObject.h"
#include <atomic>
#include <cstring>
#include <MG_Backend/BackendObjects.h>
#include <MG_State/GLState/VertexArrayState/VertexArrayObject.h>
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
@@ -87,6 +88,19 @@ namespace {
}
}
// How many consecutive uniform locations a uniform occupies. Array uniforms (opaque
// or not) span one location per element so glUniform*v(count > 1) and
// glGetUniformLocation("arr[k]") can address elements individually; everything else
// spans a single location. TObjectReflection.size only carries the element count for
// non-block arrays, so prefer the TType, which is authoritative for both.
static MobileGL::Int GetUniformLocationSpan(const glslang::TObjectReflection& uniform) {
const glslang::TType* type = uniform.getType();
if (type != nullptr && type->isSizedArray()) {
return std::max(1, type->getOuterArraySize());
}
return std::max(1, uniform.size);
}
static bool ComputeShaderDeclaresLocalSize(const MobileGL::String& source) {
bool inLineComment = false;
bool inBlockComment = false;
@@ -346,7 +360,18 @@ namespace MobileGL::MG_State::GLState {
}
MGLOG_D("ProgramObject %u: DoReflection - building reflection", m_externalIndex);
if (!m_program->buildReflection()) {
// GL-style reflection naming (GL CTS uniform_block relies on all four):
// - BasicArraySuffix: an array uniform is reported as "arr[0]" per the GL spec.
// - StrictArraySuffix: named-block struct arrays expand per element ("s[0].a",
// "s[1].a", ...) following ARB_program_interface_query rules. Default-block
// (loose) uniforms already expand per element without this option.
// - AllBlockVariables: every member of an active named block is active even when
// no shader statement reads it (ES 3.0/GL 3.3 named-block semantics).
// - 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.
if (!m_program->buildReflection(EShReflectionStrictArraySuffix | EShReflectionBasicArraySuffix |
EShReflectionAllBlockVariables | EShReflectionSharedStd140UBO)) {
m_linkStatus = false;
m_infoLog = "Build reflection failed.";
MGLOG_E("ProgramObject %u: DoReflection - buildReflection() returned false", m_externalIndex);
@@ -361,8 +386,7 @@ namespace MobileGL::MG_State::GLState {
for (int i = 0; i < m_activeUniformCount; i++) {
auto& uniform = m_program->getUniform(i);
auto location = uniform.layoutLocation();
const Int locationSpan =
(uniform.getType() && uniform.getType()->isOpaque()) ? std::max(1, uniform.size) : 1;
const Int locationSpan = GetUniformLocationSpan(uniform);
requiredUniformLocations += locationSpan;
if (location != glslang::TQualifier::layoutLocationEnd) {
m_maxUniformLocation = std::max(m_maxUniformLocation, location + locationSpan - 1);
@@ -401,8 +425,7 @@ namespace MobileGL::MG_State::GLState {
m_externalIndex, uniform.name.c_str());
continue; // will allocate unallocated uniforms later
}
const Int locationSpan =
(uniform.getType() && uniform.getType()->isOpaque()) ? std::max(1, uniform.size) : 1;
const Int locationSpan = GetUniformLocationSpan(uniform);
for (Int element = 0; element < locationSpan; ++element) {
m_uniformIndexInTProgram[location + element] = i;
}
@@ -419,8 +442,8 @@ namespace MobileGL::MG_State::GLState {
});
for (auto index : unallocatedUniformIndex) {
auto& uniform = m_program->getUniform(index);
const Int locationSpan =
(uniform.getType() && uniform.getType()->isOpaque()) ? std::max(1, uniform.size) : 1;
const Int locationSpan = GetUniformLocationSpan(uniform);
Bool placed = false;
for (; locNeedle <= m_maxUniformLocation; locNeedle++) {
bool hasRoom = locNeedle + locationSpan - 1 <= m_maxUniformLocation;
for (Int element = 0; hasRoom && element < locationSpan; ++element) {
@@ -437,8 +460,25 @@ namespace MobileGL::MG_State::GLState {
"(index %d)",
m_externalIndex, uniform.name.c_str(), locNeedle, locNeedle + locationSpan - 1, index);
locNeedle += locationSpan;
placed = true;
break;
}
if (!placed) {
// Explicit-location uniforms can fragment the space so no contiguous
// span is left; grow the table instead of leaving the uniform without
// a location (which would make it unsettable via glUniform*).
const SizeT base = m_uniformIndexInTProgram.size();
m_uniformIndexInTProgram.resize(base + locationSpan, glslang::TQualifier::layoutLocationEnd);
m_uniformSamplerOrImageUnitIndex.resize(base + locationSpan, -1);
m_maxUniformLocation = static_cast<Uint>(base + locationSpan - 1);
for (Int element = 0; element < locationSpan; ++element) {
m_uniformIndexInTProgram[base + element] = index;
}
m_uniformLocations[uniform.name] = static_cast<Uint>(base);
MGLOG_D("ProgramObject %u: Reflection - grew location table to place uniform '%s' at %zu..%zu",
m_externalIndex, uniform.name.c_str(), base, base + locationSpan - 1);
locNeedle = base + locationSpan;
}
}
for (int i = 0; i < m_activeUniformCount; i++) {
@@ -454,10 +494,17 @@ namespace MobileGL::MG_State::GLState {
continue;
}
const auto explicitBinding = m_explicitOpaqueUniformBindings.find(uniform.name);
// Reflection names an array "texs[0]" while the layout(binding = N) map from the IO
// resolver is keyed by the declared name ("texs"); look up both spellings.
auto explicitBinding = m_explicitOpaqueUniformBindings.find(uniform.name);
if (explicitBinding == m_explicitOpaqueUniformBindings.end() && uniform.name.length() > 3 &&
uniform.name.compare(uniform.name.length() - 3, 3, "[0]") == 0) {
explicitBinding =
m_explicitOpaqueUniformBindings.find(uniform.name.substr(0, uniform.name.length() - 3));
}
const int initialUnit =
explicitBinding != m_explicitOpaqueUniformBindings.end() ? static_cast<int>(explicitBinding->second) : 0;
const Int locationSpan = std::max(1, uniform.size);
const Int locationSpan = GetUniformLocationSpan(uniform);
for (Int element = 0; element < locationSpan &&
location + element < m_uniformSamplerOrImageUnitIndex.size(); ++element) {
m_uniformSamplerOrImageUnitIndex[location + element] =
@@ -622,8 +669,11 @@ namespace MobileGL::MG_State::GLState {
m_uniformSizesInBytes.clear();
m_uniformOffsets.clear();
m_globalUboScratch.clear();
m_uniformOffsets.resize(m_maxUniformLocation + 1);
m_uniformSizesInBytes.resize(m_maxUniformLocation + 1);
// kInvalidUniformOffset marks locations that end up without global-UBO backing
// (e.g. the optimizer eliminated every use of the uniform); the fallback pass
// below gives those locations tail storage so glUniform* always has a target.
m_uniformOffsets.resize(m_maxUniformLocation + 1, kInvalidUniformOffset);
m_uniformSizesInBytes.resize(m_maxUniformLocation + 1, 0);
for (SizeT i = 0; i < m_generatedSpirv.size(); i++) {
auto& spv = m_generatedSpirv[i];
@@ -653,31 +703,95 @@ namespace MobileGL::MG_State::GLState {
m_globalUboScratch.resize(size);
}
for (const auto& [name, offset] : meta.plainUniformOffsetsInUBO) {
if (m_uniformLocations.find(name) != m_uniformLocations.end()) {
m_uniformOffsets[m_uniformLocations[name]] = offset;
MGLOG_D("ProgramObject %u: GenerateBinary - uniform '%s' offset=%u assigned to location %u",
m_externalIndex, name.c_str(), offset, m_uniformLocations[name]);
} else {
MGLOG_D("ProgramObject %u: GenerateBinary - uniform '%s' offset=%u but not found in "
"m_uniformLocations",
m_externalIndex, name.c_str(), offset);
}
}
for (const auto& [name, size] : meta.plainUniformMemberSizesInBytes) {
if (m_uniformLocations.find(name) != m_uniformLocations.end()) {
m_uniformSizesInBytes[m_uniformLocations[name]] = size;
MGLOG_D("ProgramObject %u: GenerateBinary - uniform '%s' size=%u assigned to location %u",
m_externalIndex, name.c_str(), size, m_uniformLocations[name]);
} else {
MGLOG_D("ProgramObject %u: GenerateBinary - uniform '%s' size=%u but not found in "
"m_uniformLocations",
m_externalIndex, name.c_str(), size);
}
// SPIRV-Reflect leaf names never carry a "[0]" suffix; frontend
// reflection keys arrays as "arr[0]" (GL naming), so retry with the
// suffix before declaring the uniform unbacked.
auto locationIt = m_uniformLocations.find(name);
if (locationIt == m_uniformLocations.end()) {
locationIt = m_uniformLocations.find(name + "[0]");
}
if (locationIt == m_uniformLocations.end()) {
MGLOG_D("ProgramObject %u: GenerateBinary - uniform '%s' offset=%u but not found in "
"m_uniformLocations",
m_externalIndex, name.c_str(), offset);
continue;
}
const Uint baseLocation = locationIt->second;
if (!IsValidUniformLocation(static_cast<Int>(baseLocation))) {
continue;
}
const Int uniformIndex = m_uniformIndexInTProgram[baseLocation];
const GLint arraySize = GetActiveUniformArraySize(uniformIndex);
SizeT memberSize = 0;
const auto sizeIt = meta.plainUniformMemberSizesInBytes.find(name);
if (sizeIt != meta.plainUniformMemberSizesInBytes.end()) {
memberSize = sizeIt->second;
}
Uint arrayStride = 0;
const auto strideIt = meta.plainUniformArrayStridesInUBO.find(name);
if (strideIt != meta.plainUniformArrayStridesInUBO.end()) {
arrayStride = strideIt->second;
}
// Array uniforms span one location per element (see DoReflection);
// give each element its real byte offset inside the UBO.
const GLint elementCount = (arraySize > 1 && arrayStride == 0) ? 1 : std::max(arraySize, 1);
for (GLint element = 0; element < elementCount; ++element) {
const Uint location = baseLocation + static_cast<Uint>(element);
if (location > m_maxUniformLocation || m_uniformIndexInTProgram[location] != uniformIndex) {
break;
}
m_uniformOffsets[location] = offset + static_cast<Uint>(element) * arrayStride;
const SizeT consumed = static_cast<SizeT>(element) * arrayStride;
m_uniformSizesInBytes[location] = memberSize > consumed ? memberSize - consumed : 0;
}
MGLOG_D("ProgramObject %u: GenerateBinary - uniform '%s' offset=%u stride=%u size=%zu assigned "
"to locations %u..%u",
m_externalIndex, name.c_str(), offset, arrayStride, memberSize, baseLocation,
baseLocation + static_cast<Uint>(elementCount) - 1);
}
MGLOG_D("ProgramObject %u: GenerateBinary - finished parsing module %zu metadata",
m_externalIndex, i);
}
}
// Fallback pass: a linked program's active non-opaque uniforms must accept
// glUniform*/glGetUniform* even when the optimized SPIR-V no longer contains
// them (AggressiveDCE can remove a dead loop together with the only loads of a
// uniform -- or the entire global UBO, leaving the scratch unallocated). Hand
// such locations CPU-side storage at the (16-byte aligned) tail of the shadow
// buffer; backends bind at least the SPIR-V-declared UBO range, and the GPU
// never reads these bytes, so this only keeps the GL-visible state coherent.
for (Uint location = 0; location <= m_maxUniformLocation; ++location) {
if (m_uniformOffsets[location] != kInvalidUniformOffset) continue;
if (!IsValidUniformLocation(static_cast<Int>(location))) continue;
const auto& uniform = m_program->getUniform(m_uniformIndexInTProgram[location]);
const glslang::TType* type = uniform.getType();
if (type != nullptr && type->isOpaque()) continue;
if (uniform.index >= 0 && uniform.index < m_program->getNumUniformBlocks() &&
std::strstr(m_program->getUniformBlock(uniform.index).name.c_str(),
MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME) == nullptr) {
// Member of a named uniform block: not settable through glUniform*, so it
// needs no global-UBO shadow storage.
continue;
}
// std140-style slot: the matrix upload paths write column vectors at
// 16-byte strides, so a matrix slot must cover cols * 16 bytes.
SizeT slotSize = MG_Util::GetGLTypeSize(uniform.glDefineType);
if (type != nullptr && type->isMatrix()) {
slotSize = static_cast<SizeT>(type->getMatrixCols()) * 16u;
}
slotSize = (slotSize + 15u) & ~static_cast<SizeT>(15u);
const SizeT slotOffset = (m_globalUboScratch.size() + 15u) & ~static_cast<SizeT>(15u);
m_globalUboScratch.resize(slotOffset + slotSize, 0);
m_uniformOffsets[location] = static_cast<Uint>(slotOffset);
m_uniformSizesInBytes[location] = slotSize;
MGLOG_D("ProgramObject %u: GenerateBinary - uniform '%s' location %u has no UBO backing in the "
"generated SPIR-V (optimized out?); allocated %zu fallback bytes at scratch offset %zu",
m_externalIndex, uniform.name.c_str(), location, slotSize, slotOffset);
}
}
void ProgramObject::WaitUntilGenerationCompleted() const {
@@ -18,6 +18,13 @@ namespace MobileGL::MG_State::GLState {
public:
ProgramObject(Uint externalIndex) : m_externalIndex(externalIndex), m_lifetimeId(AllocateLifetimeId()) {}
bool ShaderIsAttached(const SharedPtr<ShaderObject>& shader);
// GL-visible attachment: in the attach list and not pending detach (glDetachShader
// defers the actual removal to the next link).
Bool ShaderIsAttachedGLVisible(const SharedPtr<ShaderObject>& shader) const {
const auto matches = [&shader](const SharedPtr<ShaderObject>& s) { return s.get() == shader.get(); };
if (std::none_of(m_shaders.begin(), m_shaders.end(), matches)) return false;
return std::none_of(m_detachedShaders.begin(), m_detachedShaders.end(), matches);
}
bool AttachShader(const SharedPtr<ShaderObject>& shader);
SizeT DetachShader(const SharedPtr<ShaderObject>& shader);
SizeT RemoveShader(const SharedPtr<ShaderObject>& shader);
@@ -43,8 +50,51 @@ namespace MobileGL::MG_State::GLState {
Uint GetMaxUniformLocation() const { return m_maxUniformLocation; }
Int GetUniformLocation(const String& name) const {
const auto it = m_uniformLocations.find(name);
if (it == m_uniformLocations.end()) return -1;
return (Int)it->second;
if (it != m_uniformLocations.end()) return (Int)it->second;
// Reflection stores GL-style names: an array uniform is keyed "arr[0]" (its base
// location). A bare "arr" query resolves to that entry; an "arr[k]" query resolves
// to base + k because DoReflection reserves one location per array element.
if (name.empty()) return -1;
if (name.back() != ']') {
const auto suffixedIt = m_uniformLocations.find(name + "[0]");
if (suffixedIt != m_uniformLocations.end()) return (Int)suffixedIt->second;
return -1;
}
if (name.length() < 4) return -1;
const SizeT bracket = name.rfind('[');
// Require at least one digit between the brackets.
if (bracket == String::npos || bracket + 1 >= name.length() - 1) return -1;
Uint element = 0;
for (SizeT i = bracket + 1; i < name.length() - 1; ++i) {
if (name[i] < '0' || name[i] > '9') return -1;
element = element * 10 + static_cast<Uint>(name[i] - '0');
if (element > 0x0FFFFFFFu) return -1;
}
auto baseIt = m_uniformLocations.find(name.substr(0, bracket) + "[0]");
if (baseIt == m_uniformLocations.end()) {
// Legacy key without the "[0]" suffix (defensive; reflection normally
// stores the suffixed form for arrays).
baseIt = m_uniformLocations.find(name.substr(0, bracket));
if (baseIt == m_uniformLocations.end()) return -1;
}
const Int base = (Int)baseIt->second;
if (!IsValidUniformLocation(base)) return -1;
const Int index = m_uniformIndexInTProgram[base];
// "[k]" only addresses arrays ("scalar[0]" is not a uniform name), and only
// in-range elements.
const glslang::TType* type = m_program->getUniform(index).getType();
if (type == nullptr || !type->isArray()) return -1;
if (static_cast<GLint>(element) >= GetActiveUniformArraySize(index)) return -1;
const Int location = base + (Int)element;
if (!UniformLocationsAliasSameUniform(base, location)) return -1;
return location;
}
// True when both locations are element slots of the same uniform variable.
Bool UniformLocationsAliasSameUniform(Int a, Int b) const {
if (!IsValidUniformLocation(a) || !IsValidUniformLocation(b)) return false;
return m_uniformIndexInTProgram[a] == m_uniformIndexInTProgram[b];
}
Int GetActiveUniformIndex(const String& name) const {
@@ -54,6 +104,19 @@ namespace MobileGL::MG_State::GLState {
return uniformIndex;
}
// Reflection stores an array uniform under "arr[0]"; accept the bare "arr"
// spelling too. The reverse ("arr[0]" against a bare "arr" entry) is kept for
// robustness against non-suffixed reflection entries.
if (!name.empty() && name.back() != ']') {
const String suffixedName = name + "[0]";
const Int suffixedIndex = m_program->getUniformIndex(suffixedName.c_str());
if (suffixedIndex >= 0 && suffixedIndex < m_activeUniformCount &&
m_program->getUniform(suffixedIndex).name == suffixedName) {
return suffixedIndex;
}
return -1;
}
if (name.length() <= 3 || name.compare(name.length() - 3, 3, "[0]") != 0) return -1;
const String baseName = name.substr(0, name.length() - 3);
const Int baseIndex = m_program->getUniformIndex(baseName.c_str());
@@ -104,11 +167,24 @@ namespace MobileGL::MG_State::GLState {
}
// GL_UNIFORM_ARRAY_STRIDE: byte stride of an array member in a named block; 0 for a non-array
// block member; -1 for a default-block uniform. glslang yields arrayStride==0 for the
// default-block case, so gate on block membership to return the spec-mandated -1.
// block member; -1 for a default-block uniform (glslang yields arrayStride==0 there, so gate
// on block membership for the spec-mandated -1). The stride itself is derived from the type
// instead of glslang's reflected arrayStride: for an array nested inside a struct member,
// glslang computes that field against the enclosing STRUCT's (unset) packing and reports a
// tight std430-like stride (ivec2 a[7] -> 8), even though its own member offsets and the
// generated SPIR-V lay the array out with std140 16-byte-rounded strides. MobileGL's UBO
// layout is always std140, where every array element stride rounds up to a vec4.
GLint GetActiveUniformArrayStride(Uint index) const {
const auto& uniform = m_program->getUniform(static_cast<Int>(index));
return (uniform.index < 0) ? -1 : uniform.arrayStride;
if (uniform.index < 0) return -1;
const glslang::TType* type = uniform.getType();
if (type == nullptr || !type->isArray()) return 0;
if (type->isMatrix()) {
const bool rowMajor = GetActiveUniformIsRowMajor(index) != 0;
const int vectors = rowMajor ? type->getMatrixRows() : type->getMatrixCols();
return GetActiveUniformMatrixStride(index) * vectors;
}
return 16; // scalars and vectors: std140 rounds the element stride up to a vec4
}
// GL_UNIFORM_IS_ROW_MAJOR: 1 only for a row-major matrix in a named block, else 0. The
@@ -170,6 +246,9 @@ namespace MobileGL::MG_State::GLState {
auto& uniform = m_program->getUniform(static_cast<Int>(index));
return uniform.name;
}
// Sentinel for a uniform location without global-UBO backing storage (should not
// survive linking: GenerateBinary falls back to tail-allocated scratch storage).
static constexpr Uint kInvalidUniformOffset = ~0u;
Uint GetUniformOffset(Uint location) const { return m_uniformOffsets[location]; }
Uint GetUniformSizesInBytes(Uint location) const { return MG_Util::GetGLTypeSize(GetUniformType(location)); }
@@ -292,6 +371,11 @@ namespace MobileGL::MG_State::GLState {
Uint GetUniformBlockIndex(const char* name) const {
auto it = m_uniformBlockIndexByName.find(name);
if (it != m_uniformBlockIndexByName.end()) return it->second;
// Instances of an arrayed block are reflected as "Block[0]".."Block[N-1]";
// a bare "Block" query resolves to the first instance per GL semantics.
const String suffixedName = String(name) + "[0]";
it = m_uniformBlockIndexByName.find(suffixedName);
if (it != m_uniformBlockIndexByName.end()) return it->second;
return 0xFFFFFFFFu; // GL_INVALID_INDEX
}
Bool IsActiveUniformBlock(Uint index) const {
@@ -300,7 +384,11 @@ namespace MobileGL::MG_State::GLState {
}
Uint GetUBOSizeAt(Uint index) const {
if (!IsActiveUniformBlock(index)) return 0;
return m_program->getUniformBlock((Int)index).size;
// glslang reports the unpadded end offset of the last member, but a std140 block
// (like a std140 struct) occupies a vec4-rounded size, and that is what the
// backend compiles: ES drivers reject draws whose bound UBO range is smaller
// than the block (a block ending in ivec3 reported 12 while the driver needs 16).
return (m_program->getUniformBlock((Int)index).size + 15u) & ~15u;
}
const String& GetUniformBlockName(Uint index) const {
@@ -308,8 +396,30 @@ namespace MobileGL::MG_State::GLState {
return ubo.name;
}
// Uniform entries that belong to an arrayed uniform block are reflected once, against
// the first instance ("Block[0]"); per GL semantics every other instance shares that
// member set. Maps any instance's block index to the index owning the member entries.
Uint GetUniformBlockMemberOwnerIndex(Uint index) const {
const String& name = GetUniformBlockName(index);
if (name.empty() || name.back() != ']') return index;
const SizeT bracket = name.rfind('[');
if (bracket == String::npos) return index;
const auto it = m_uniformBlockIndexByName.find(name.substr(0, bracket) + "[0]");
if (it != m_uniformBlockIndexByName.end()) return it->second;
return index;
}
// GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS: derived from the same active-uniform scan that
// fills GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES, so the two queries always agree
// (glslang's numMembers counts declared members, which diverges from the reflected
// entry list for struct arrays and arrayed block instances).
Int GetUniformBlockActiveUniformCount(Uint index) const {
return m_program->getUniformBlock((Int)index).numMembers;
const Int ownerIndex = static_cast<Int>(GetUniformBlockMemberOwnerIndex(index));
Int count = 0;
for (Uint uniformIndex = 0; uniformIndex < m_activeUniformCount; ++uniformIndex) {
if (GetActiveUniformBlockIndex(uniformIndex) == ownerIndex) ++count;
}
return count;
}
Bool IsUniformBlockReferencedByStage(Uint index, EShLanguage stage) const {
@@ -29,9 +29,18 @@ namespace MobileGL::MG_State::GLState {
if (!CheckIndexAvail(program, m_programObjects)) return; // FIXME: add error reporting here
auto& programObject = m_programObjects[program];
if (programObject != nullptr) {
// Snapshot the attachments: deleting the program is a detach point for shaders
// that were flagged with glDeleteShader while still attached.
const Vector<SharedPtr<ShaderObject>> attachedShaders = programObject->GetAttachedShaders();
programObject->MarkAsDeleted();
programObject.reset();
m_programIndexGenerator.Delete(program);
for (const auto& shader : attachedShaders) {
const Uint shaderName = shader->GetExternalIndex();
if (CheckIndexAvail(shaderName, m_shaderObjects) && m_shaderObjects[shaderName] == shader) {
ReleaseShaderNameIfOrphaned(shaderName);
}
}
}
}
@@ -66,12 +75,35 @@ namespace MobileGL::MG_State::GLState {
if (!CheckIndexAvail(shader, m_shaderObjects)) return;
auto& shaderObject = m_shaderObjects[shader];
if (shaderObject != nullptr) {
m_shaderObjects[shader]->MarkAsDeleted();
m_shaderObjects[shader].reset();
m_shaderIndexGenerator.Delete(shader);
// glDeleteShader on an attached shader only FLAGS it; the name stays valid (and
// glShaderSource/glCompileShader keep working on it) until the shader is detached
// from every program. The GL CTS compiles shaders through exactly this
// create-attach-delete-source-compile sequence (uniform_block.common.name_matching).
shaderObject->MarkAsDeleted();
ReleaseShaderNameIfOrphaned(shader);
}
}
Bool ProgramState::ShaderHasGLVisibleAttachment(const SharedPtr<ShaderObject>& shaderObject) const {
for (const auto& programObject : m_programObjects) {
if (programObject != nullptr && programObject->ShaderIsAttachedGLVisible(shaderObject)) {
return true;
}
}
// A program deleted while current vacates its table slot but stays alive as the
// current program; its attachments still count.
return m_currentProgram != nullptr && m_currentProgram->ShaderIsAttachedGLVisible(shaderObject);
}
void ProgramState::ReleaseShaderNameIfOrphaned(Uint shader) {
if (!CheckIndexAvail(shader, m_shaderObjects)) return;
auto& shaderObject = m_shaderObjects[shader];
if (shaderObject == nullptr || !shaderObject->GetDeleteStatus()) return;
if (ShaderHasGLVisibleAttachment(shaderObject)) return;
shaderObject.reset();
m_shaderIndexGenerator.Delete(shader);
}
Bool ProgramState::ValidateShaderObject(Uint shader) const {
return CheckIndexAvail(shader, m_shaderObjects) && m_shaderObjects[shader] != nullptr;
}
@@ -26,11 +26,16 @@ namespace MobileGL::MG_State::GLState {
Uint CreateShader(ShaderStage stage);
const SharedPtr<ShaderObject>& GetShaderObject(Uint shader);
void MarkShaderObjectForDeletion(Uint shader);
// Frees a deletion-flagged shader's name once no program holds a GL-visible
// attachment to it (the deferred half of glDeleteShader-while-attached).
void ReleaseShaderNameIfOrphaned(Uint shader);
Bool ValidateShaderObject(Uint shader) const;
const SharedPtr<ProgramObject>& GetCurrentProgram() const { return m_currentProgram; }
private:
Bool ShaderHasGLVisibleAttachment(const SharedPtr<ShaderObject>& shaderObject) const;
template <typename T>
static Bool CheckIndexAvail(const SizeT idx, const Vector<T>& vec) {
return idx < vec.size();
@@ -78,6 +78,13 @@ namespace MobileGL {
++m_version;
}
void SamplerObject::SetMaxAnisotropy(Float maxAnisotropy) {
if (maxAnisotropy == m_samplerParameters.maxAnisotropy) return;
m_samplerParameters.maxAnisotropy = maxAnisotropy;
++m_version;
}
void SamplerObject::SetSamplerCompareFunc(SamplerCompareFunc func) {
if (func == m_samplerParameters.compareFunc) return;
@@ -128,6 +135,10 @@ namespace MobileGL {
return m_samplerParameters.lodBias;
}
Float SamplerObject::GetMaxAnisotropy() const {
return m_samplerParameters.maxAnisotropy;
}
SamplerCompareMode SamplerObject::GetCompareMode() const {
return m_samplerParameters.compareMode;
}
@@ -65,6 +65,7 @@ namespace MobileGL {
Float minLod = -1000.0f;
Float maxLod = 1000.0f;
Float lodBias = 0.0f;
Float maxAnisotropy = 1.0f;
SamplerCompareFunc compareFunc = SamplerCompareFunc::Always;
SamplerCompareMode compareMode = SamplerCompareMode::None;
};
@@ -83,6 +84,7 @@ namespace MobileGL {
void SetMipmapMode(SamplerMipmapMode mode);
void SetLodRange(Float minLod, Float maxLod);
void SetLodBias(Float bias);
void SetMaxAnisotropy(Float maxAnisotropy);
void SetSamplerCompareFunc(SamplerCompareFunc func);
void SetCompareMode(SamplerCompareMode mode);
@@ -95,6 +97,7 @@ namespace MobileGL {
Float GetMinLod() const;
Float GetMaxLod() const;
Float GetLodBias() const;
Float GetMaxAnisotropy() const;
SamplerCompareMode GetCompareMode() const;
SamplerCompareFunc GetSamplerCompareFunc() const;
Uint GetExternalIndex() const;
@@ -76,6 +76,14 @@ namespace MobileGL {
BGRInteger,
RGBAInteger,
BGRAInteger,
// Desktop-GL single-channel client formats (table 3.3): the data holds one component that
// feeds the G, B or A channel; the remaining channels default to 0 (color) / 1 (alpha).
Green,
Blue,
Alpha,
GreenInteger,
BlueInteger,
AlphaInteger,
StencilIndex,
DepthComponent,
DepthStencil,
@@ -7,6 +7,7 @@
// End of Source File Header
#include "TextureObject.h"
#include "MG_State/GLState/Core.h"
#include "MG_Util/Types.h"
#include <MG_Util/Metrics/TextureMetrics.h>
@@ -52,6 +53,20 @@ namespace MobileGL {
void TextureObjectBase::SetInternalFormat(TextureInternalFormat format) {
if (format == m_internalFormat) return;
// A default texture (name 0) changes IsUndefinedDefaultTexture on the
// Unknown<->defined transition, which changes per-draw sampled-set membership
// without any bind happening; bump the bind generation so cached sampled sets
// re-resolve instead of replaying the stale membership. The identity check
// excludes the other externalIndex-0 objects (proxy textures, default-FBO
// attachments) whose definedness never feeds sampled-set membership, so e.g.
// proxy probes cannot churn the cache.
if (m_externalIndex == 0 && pGLContext &&
(m_internalFormat == TextureInternalFormat::Unknown) !=
(format == TextureInternalFormat::Unknown) &&
pGLContext->GetDefaultTextureObject(GetTarget()).get() == this) {
pGLContext->BumpTextureBindGeneration();
}
m_internalFormat = format;
++m_textureParamsVersion;
}
@@ -154,6 +154,17 @@ namespace MobileGL::MG_State::GLState {
: nullptr;
}
// The per-target default texture objects (name 0) sit permanently in every texture unit's
// binding slots, so "nothing useful bound" is no longer a null slot. While a default texture
// has never been given an image (its internal format is still Unknown) it can contribute
// nothing to sampling; backends treat such a binding exactly like the old empty slot and
// skip per-draw sync/bind work for it. Once an application defines an image on a default
// texture it loses this shortcut and is synced like any other texture.
inline Bool IsUndefinedDefaultTexture(const ITextureObject* texture) {
return texture != nullptr && texture->GetExternalIndex() == 0 &&
texture->GetFormat() == TextureInternalFormat::Unknown;
}
class TextureObjectWithOneMipmap : public TextureObjectMipmap {
public:
TextureObjectWithOneMipmap(TextureTarget target, Uint externalIndex)
@@ -18,9 +18,51 @@
#include "TextureObjectStubs.h"
namespace MobileGL::MG_State::GLState {
static SharedPtr<ITextureObject> MakeTextureObjectForTarget(Uint index, TextureTarget target) {
switch (target) {
case TextureTarget::Texture1D:
return MakeShared<TextureObject1D>(index);
case TextureTarget::TextureCubeMap:
return MakeShared<TextureObject2DCube>(index);
case TextureTarget::Texture2D:
return MakeShared<TextureObject2D>(index);
case TextureTarget::Texture3D:
return MakeShared<TextureObject3D>(index);
case TextureTarget::TextureBuffer:
return MakeShared<TextureObjectBuffer>(index);
// These texture types are still stubbed:
case TextureTarget::TextureRectangle:
return MakeShared<TextureObjectRectangle>(index);
case TextureTarget::Texture2DMultisample:
return MakeShared<TextureObject2DMultisample>(index);
case TextureTarget::Texture1DArray:
return MakeShared<TextureObject1DArray>(index);
case TextureTarget::Texture2DArray:
return MakeShared<TextureObject2DArray>(index);
case TextureTarget::TextureCubeMapArray:
return MakeShared<TextureObjectCubeMapArray>(index);
case TextureTarget::Texture2DMultisampleArray:
return MakeShared<TextureObject2DMultisampleArray>(index);
default:
MOBILEGL_ASSERT(false, "Unimplemented texture type when creating texture object!: %d", (int)target);
return nullptr;
}
}
TextureState::TextureState() : m_indexGenerator(1024, 1) {
// GL 3.3 core 3.8: each target owns one default texture object (name 0) per context,
// shared across all texture units, and it is the initial binding of every unit/target
// slot. It is created outside m_textureObjects so name-based paths (glIsTexture,
// GenTextures/DeleteTextures, by-name DSA lookups) never see it.
for (int i = 0; i < (int)TextureTarget::TextureTargetCount; ++i) {
m_defaultTextureObjects[i] = MakeTextureObjectForTarget(0, static_cast<TextureTarget>(i));
}
for (int i = 0; i < MAX_TEXTURE_IMAGE_UNITS; ++i) {
m_textureUnits[i] = TextureUnit();
for (auto& bindingSlot : m_textureUnits[i].GetAllBindingSlots()) {
bindingSlot.Bind(m_defaultTextureObjects[(int)bindingSlot.GetTarget()]);
}
}
}
@@ -33,6 +75,12 @@ namespace MobileGL::MG_State::GLState {
return nullTextureObject;
}
const SharedPtr<ITextureObject>& TextureState::GetDefaultTextureObject(TextureTarget target) const {
MOBILEGL_ASSERT(target > TextureTarget::Unknown && target < TextureTarget::TextureTargetCount,
"GetDefaultTextureObject: invalid texture target %d", (int)target);
return m_defaultTextureObjects[(int)target];
}
void TextureState::GenerateNames(Uint number, Vector<Uint>& textures) {
textures.resize(number);
m_indexGenerator.Generate(number, textures.data());
@@ -40,52 +88,15 @@ namespace MobileGL::MG_State::GLState {
const SharedPtr<ITextureObject>& TextureState::CreateTextureObject(Uint index, TextureTarget target) {
auto& textureObject = m_textureObjects[index];
switch (target) {
case TextureTarget::Texture1D:
textureObject = MakeShared<TextureObject1D>(index);
break;
case TextureTarget::TextureCubeMap:
textureObject = MakeShared<TextureObject2DCube>(index);
break;
case TextureTarget::Texture2D:
textureObject = MakeShared<TextureObject2D>(index);
break;
case TextureTarget::Texture3D:
textureObject = MakeShared<TextureObject3D>(index);
break;
case TextureTarget::TextureBuffer:
textureObject = MakeShared<TextureObjectBuffer>(index);
break;
// These texture types are still stubbed:
case TextureTarget::TextureRectangle:
textureObject = MakeShared<TextureObjectRectangle>(index);
break;
case TextureTarget::Texture2DMultisample:
textureObject = MakeShared<TextureObject2DMultisample>(index);
break;
case TextureTarget::Texture1DArray:
textureObject = MakeShared<TextureObject1DArray>(index);
break;
case TextureTarget::Texture2DArray:
textureObject = MakeShared<TextureObject2DArray>(index);
break;
case TextureTarget::TextureCubeMapArray:
textureObject = MakeShared<TextureObjectCubeMapArray>(index);
break;
case TextureTarget::Texture2DMultisampleArray:
textureObject = MakeShared<TextureObject2DMultisampleArray>(index);
break;
default:
MOBILEGL_ASSERT(false, "Unimplemented texture type when creating texture object!: %d", (int)target);
textureObject = MakeTextureObjectForTarget(index, target);
if (!textureObject) {
static SharedPtr<ITextureObject> nullTextureObject = nullptr;
return nullTextureObject;
}
return textureObject;
}
void TextureState::MarkTextureObjectForDeletion(Uint index) {
void TextureState::MarkTextureObjectForDeletion(Uint index, Bool keepUnboundReservation) {
if (m_indexGenerator.IsValid(index)) {
auto it = m_textureObjects.find(index);
if (it != m_textureObjects.end()) {
@@ -94,7 +105,9 @@ namespace MobileGL::MG_State::GLState {
auto& bindingSlots = m_textureUnits[unit].GetAllBindingSlots();
for (auto& bindingSlot : bindingSlots) {
if (bindingSlot.GetBoundObject() == it->second) {
bindingSlot.Bind(nullptr);
// GL 3.3 core 3.8.1: deleting a bound texture rebinds zero, i.e. the
// target's default texture object, on every unit it was bound to.
bindingSlot.Bind(m_defaultTextureObjects[(int)bindingSlot.GetTarget()]);
}
}
}
@@ -109,8 +122,16 @@ namespace MobileGL::MG_State::GLState {
// re-resolved instead of dangling.
BumpTextureBindGeneration();
m_textureObjects.erase(index);
m_indexGenerator.Delete(index);
} else if (!keepUnboundReservation) {
// GL 3.3 core 3.8.1 makes a deleted name unused again even when GenTextures only
// reserved it and no bind ever instantiated an object (so a later bind of it must
// fail), and the reservation has to return to the free list.
m_indexGenerator.Delete(index);
}
m_indexGenerator.Delete(index);
// Relaxed semantics: legacy apps may delete a generated name before its first
// bind, then bind and populate that same name. Keep such a reservation alive there;
// a real texture object reaching deletion still releases its index above.
}
}
@@ -49,12 +49,18 @@ namespace MobileGL::MG_State::GLState {
void GenerateNames(Uint number, Vector<Uint>& textures);
const SharedPtr<ITextureObject>& CreateTextureObject(Uint index, TextureTarget target);
const SharedPtr<ITextureObject>& GetTextureObject(Uint index);
// The context's default texture object (name 0) for `target`. GL 3.3 core 3.8: texture
// zero names a real, per-target texture object shared by every texture unit; binding 0
// binds it, and image/parameter calls on it must work like on any texture. It is not a
// GenTextures name: it lives outside m_textureObjects (so glIsTexture(0) stays GL_FALSE
// and by-name lookups keep failing for 0) and can never be deleted.
const SharedPtr<ITextureObject>& GetDefaultTextureObject(TextureTarget target) const;
TextureUnit& GetUnitObject(Int unit);
ImageTextureBinding& GetImageTextureBinding(Int unit);
const ImageTextureBinding& GetImageTextureBinding(Int unit) const;
Int GetActiveTextureUnit() const;
void SetActiveTextureUnit(Int unit);
void MarkTextureObjectForDeletion(Uint index);
void MarkTextureObjectForDeletion(Uint index, Bool keepUnboundReservation);
Bool ValidateName(Uint index) const;
Bool ValidateTextureObject(Uint index) const;
@@ -83,5 +89,8 @@ namespace MobileGL::MG_State::GLState {
Array<ImageTextureBinding, MAX_TEXTURE_IMAGE_UNITS> m_imageTextureBindings;
IndexGenerator<Uint> m_indexGenerator;
UnorderedMap<GLuint, SharedPtr<ITextureObject>> m_textureObjects;
// One default texture object (external name 0) per target, created with the context and
// immortal for its lifetime; the initial binding of every unit/target slot.
Array<SharedPtr<ITextureObject>, (int)TextureTarget::TextureTargetCount> m_defaultTextureObjects;
};
} // namespace MobileGL::MG_State::GLState
@@ -40,10 +40,16 @@ if (APPLE)
target_link_libraries(DirectVulkanSanityTest PRIVATE objc)
target_link_libraries(DirectVulkanSanityTest PRIVATE ${QUARTZCORE_FRAMEWORK})
endif()
if (MSVC)
# The GLES headers declare gl* as dllimport on Windows, so objects like GetProcAddress.cpp
# reference __imp_gl*. Those resolve against the in-library GL entry-point definitions only
# if their objects are part of the link; pull the whole static library like the DLL does.
target_link_options(DirectVulkanSanityTest PRIVATE /WHOLEARCHIVE:MobileGL_s)
endif()
target_compile_definitions(DirectVulkanSanityTest PRIVATE -DNOMINMAX)
include(GoogleTest)
gtest_discover_tests(DirectVulkanSanityTest DISCOVERY_TIMEOUT 30)
gtest_discover_tests(DirectVulkanSanityTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS integration)
add_executable(
DirectVulkanTestExec
@@ -9,8 +9,13 @@
#include <gtest/gtest.h>
#include <cstring>
#include <map>
#include <string>
#include <vector>
#include <algorithm>
#include <MG_Backend/DirectGLES/BackendObject_DirectGLES.h>
#include <MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h>
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
// ProbeIndirectInstanceIdIncludesBaseInstance is driven against a fake GLES driver:
@@ -28,6 +33,12 @@ namespace {
GLenum errorRaisedByDraw = GL_NO_ERROR;
GLenum pendingError = GL_NO_ERROR;
std::vector<std::string> extensions;
// GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT the fake reports, and whether it was ever asked:
// querying it on a driver without the extension would raise GL_INVALID_ENUM.
GLfloat maxTextureMaxAnisotropy = 16.0f;
bool maxTextureMaxAnisotropyQueried = false;
GLuint nextBufferId = 1;
GLuint nextShaderId = 1;
@@ -86,7 +97,7 @@ namespace {
*data = 1;
break;
case GL_NUM_EXTENSIONS:
*data = 0;
*data = static_cast<GLint>(g_fake.extensions.size());
break;
default:
// Leave the caller's defaults for every other capability query.
@@ -114,11 +125,16 @@ namespace {
return reinterpret_cast<const GLubyte*>("");
}
};
// GL_NUM_EXTENSIONS reports 0 above, so this is never reached; it exists so the
// table stays complete if the extension loop ever runs.
funcs.glGetStringi = [](GLenum, GLuint) -> const GLubyte* { return nullptr; };
funcs.glGetStringi = [](GLenum name, GLuint index) -> const GLubyte* {
if (name != GL_EXTENSIONS || index >= g_fake.extensions.size()) return nullptr;
return reinterpret_cast<const GLubyte*>(g_fake.extensions[index].c_str());
};
funcs.glGetFloatv = [](GLenum pname, GLfloat* data) {
switch (pname) {
case GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT:
g_fake.maxTextureMaxAnisotropyQueried = true;
data[0] = g_fake.maxTextureMaxAnisotropy;
break;
// Two-component range queries.
case GL_ALIASED_LINE_WIDTH_RANGE:
case GL_SMOOTH_LINE_WIDTH_RANGE:
@@ -400,3 +416,65 @@ TEST(IndirectInstanceIdProbe, FillInCapabilitiesWiresProbeResult) {
EXPECT_FALSE(conformingCaps.IndirectDrawInstanceIdIncludesBaseInstance);
ExpectProbeReleasedAllObjects();
}
// The extension string is what apps gate on (LWJGL builds GLCapabilities from it), so advertising
// it on a driver that cannot filter anisotropically would leave them silently on trilinear.
TEST(TextureAnisotropyCapabilities, ExtensionIsAdvertisedOnlyWhenTheHostDriverSupportsIt) {
const auto contains = [](const MobileGL::Vector<MobileGL::GLExtension>& extensions,
MobileGL::GLExtension wanted) {
return std::find(extensions.begin(), extensions.end(), wanted) != extensions.end();
};
const auto without = MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false);
EXPECT_FALSE(contains(without, MobileGL::E_GL_EXT_texture_filter_anisotropic));
EXPECT_FALSE(contains(without, MobileGL::E_GL_ARB_texture_filter_anisotropic));
const auto with = MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, true);
EXPECT_TRUE(contains(with, MobileGL::E_GL_EXT_texture_filter_anisotropic));
EXPECT_TRUE(contains(with, MobileGL::E_GL_ARB_texture_filter_anisotropic));
// Same rule on the Vulkan backend, where the gate is the samplerAnisotropy device feature.
const auto vkWithout = MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false);
EXPECT_FALSE(contains(vkWithout, MobileGL::E_GL_EXT_texture_filter_anisotropic));
const auto vkWith = MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, true);
EXPECT_TRUE(contains(vkWith, MobileGL::E_GL_EXT_texture_filter_anisotropic));
EXPECT_TRUE(contains(vkWith, MobileGL::E_GL_ARB_texture_filter_anisotropic));
}
TEST(TextureAnisotropyCapabilities, MaxAnisotropyIsQueriedOnlyWhenTheExtensionIsPresent) {
ResetFakeDriver();
g_fake.maxVertexSsboBlocks = 0;
const auto funcs = MakeFakeGLESFunctions();
MobileGL::MG_External::GLESCapabilities absentCaps;
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(absentCaps, funcs));
// Never probed (it would be GL_INVALID_ENUM), and reported as "no anisotropy".
EXPECT_FALSE(g_fake.maxTextureMaxAnisotropyQueried);
EXPECT_FLOAT_EQ(absentCaps.MaxTextureMaxAnisotropy, 1.0f);
ResetFakeDriver();
g_fake.maxVertexSsboBlocks = 0;
g_fake.maxTextureMaxAnisotropy = 16.0f;
g_fake.extensions.emplace_back("GL_EXT_texture_filter_anisotropic");
MobileGL::MG_External::GLESCapabilities presentCaps;
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(presentCaps, funcs));
EXPECT_TRUE(g_fake.maxTextureMaxAnisotropyQueried);
EXPECT_FLOAT_EQ(presentCaps.MaxTextureMaxAnisotropy, 16.0f);
}
TEST(TextureAnisotropyCapabilities, ExtensionPresenceIsDetectedExactly) {
ResetFakeDriver();
g_fake.maxVertexSsboBlocks = 0;
const auto funcs = MakeFakeGLESFunctions();
MobileGL::MG_External::GLESCapabilities absentCaps;
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(absentCaps, funcs));
EXPECT_FALSE(absentCaps.SupportsTextureFilterAnisotropy);
ResetFakeDriver();
g_fake.maxVertexSsboBlocks = 0;
g_fake.extensions.emplace_back("GL_EXT_texture_filter_anisotropic");
MobileGL::MG_External::GLESCapabilities presentCaps;
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(presentCaps, funcs));
EXPECT_TRUE(presentCaps.SupportsTextureFilterAnisotropy);
}
@@ -17,4 +17,4 @@ target_link_libraries(
)
include(GoogleTest)
gtest_discover_tests(BackendLoaderTest DISCOVERY_TIMEOUT 30)
gtest_discover_tests(BackendLoaderTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
+73 -2
View File
@@ -8,6 +8,8 @@
#include <gtest/gtest.h>
#include <limits>
#include "Includes.h"
#include "Init.h"
#include <Config.h>
@@ -20,9 +22,31 @@ using namespace MobileGL;
class BufferTest : public ::testing::Test {
protected:
void SetUp() override { MobileGL::Initialize(); }
// GL error flags are sticky per error code and the context outlives an individual test in this
// binary, so drain whatever an earlier test left pending - otherwise an error-code assertion
// here reads someone else's error. Bounded: one flag per code, so this cannot hang the suite.
static void DrainPendingGlErrors() {
for (Int drained = 0; drained < 16 && MG_Impl::GLImpl::GetError() != GL_NO_ERROR; ++drained) {
}
}
void TearDown() override {}
// The call under test must raise exactly the expected error and nothing more: a second pending
// error means one entry point queued several, which GetError() would hand out at an unrelated
// call site later on.
static void ExpectSingleGlError(GLenum expected) {
EXPECT_EQ(MG_Impl::GLImpl::GetError(), expected);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "the call recorded more than one error";
}
void SetUp() override {
MobileGL::Initialize();
DrainPendingGlErrors();
}
void TearDown() override {
// Attribute a leaked error to the test that caused it instead of to whoever runs next.
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "test left an unconsumed GL error behind";
}
};
TEST_F(BufferTest, Binding) {
@@ -105,6 +129,53 @@ TEST_F(BufferTest, GenerateManyNames_NoPrematureCreation) {
}
}
// GL 3.3 core 2.9 name lifecycle. The same three rules are asserted per object family (see the
// texture/vertex-array/framebuffer/renderbuffer suites): a deleted or never-generated name is
// INVALID_OPERATION to bind, deleting one is silent, and a generated-but-never-bound reservation
// is still released so the name gets recycled.
TEST_F(BufferTest, DeleteOfUnknownOrAlreadyDeletedBufferNameIsSilent) {
GLuint buffer = 0;
MG_Impl::GLImpl::GenBuffers(1, &buffer);
ASSERT_NE(buffer, 0u);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::DeleteBuffers(1, &buffer);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// Double delete, name 0 and a never-generated name must all be ignored without an error.
MG_Impl::GLImpl::DeleteBuffers(1, &buffer);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
const GLuint unknownNames[] = {0u, std::numeric_limits<GLuint>::max()};
MG_Impl::GLImpl::DeleteBuffers(2, unknownNames);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(BufferTest, DeleteGeneratedButUnboundBufferNameReleasesReservationAndBindFails) {
GLuint buffer = 0;
MG_Impl::GLImpl::GenBuffers(1, &buffer);
ASSERT_NE(buffer, 0u);
ASSERT_TRUE(MG_State::pGLContext->ValidateBufferName(buffer));
MG_Impl::GLImpl::DeleteBuffers(1, &buffer);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
EXPECT_FALSE(MG_State::pGLContext->ValidateBufferName(buffer));
MG_Impl::GLImpl::BindBuffer(GL_ARRAY_BUFFER, buffer);
ExpectSingleGlError(GL_INVALID_OPERATION);
GLuint recycled = 0;
MG_Impl::GLImpl::GenBuffers(1, &recycled);
EXPECT_EQ(recycled, buffer);
}
TEST_F(BufferTest, BindNeverGeneratedBufferNameIsInvalidOperation) {
// Not a small literal: other tests in this binary share the context and generate names in
// bulk, so a low number may well be a legitimately reserved name here.
MG_Impl::GLImpl::BindBuffer(GL_ARRAY_BUFFER, std::numeric_limits<GLuint>::max());
ExpectSingleGlError(GL_INVALID_OPERATION);
}
TEST_F(BufferTest, AcquireMemory) {
auto& slot = MobileGL::MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Uniform);
Vector<Uint> bufferNames;
+1 -1
View File
@@ -24,4 +24,4 @@ if (MSVC)
endif()
include(GoogleTest)
gtest_discover_tests(BufferTest DISCOVERY_TIMEOUT 30)
gtest_discover_tests(BufferTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
+1 -1
View File
@@ -62,7 +62,7 @@ set(LINK_LIBRARIES
)
include(GoogleTest)
gtest_discover_tests(SanityTest DISCOVERY_TIMEOUT 30)
gtest_discover_tests(SanityTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
add_subdirectory(BackendLoader)
add_subdirectory(Buffer)
+1 -1
View File
@@ -17,4 +17,4 @@ target_link_libraries(
)
include(GoogleTest)
gtest_discover_tests(EGLStateTest DISCOVERY_TIMEOUT 30)
gtest_discover_tests(EGLStateTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
@@ -102,3 +102,33 @@ TEST(EGLStateMakeCurrent, SameThreadRepeatedAttachReleaseDoesNotLeaveStaleOwner)
EXPECT_TRUE(fixture->State.MakeCurrent(EGL_NO_DISPLAY, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT));
EXPECT_EQ(fixture->State.ConsumeError(), EGL_SUCCESS);
}
// The compatibility-profile accessor is affirmative-only (it backs GL_CONTEXT_PROFILE_MASK
// reporting): attrib-less contexts (profile mask 0) and released threads both answer "not
// compat" and therefore read as core-profile contexts.
TEST(EGLStateProfile, CompatibilityProfileRequiresExplicitCompatBit) {
auto fixture = CreateFixture();
EXPECT_FALSE(fixture->State.IsCurrentContextOpenGLCompatibilityProfile());
EXPECT_TRUE(fixture->State.MakeCurrent(fixture->Display, fixture->Surface, fixture->Surface, fixture->Context));
EXPECT_FALSE(fixture->State.IsCurrentContextOpenGLCompatibilityProfile());
const EGLint compatAttribs[] = {EGL_CONTEXT_MAJOR_VERSION,
3,
EGL_CONTEXT_MINOR_VERSION,
3,
EGL_CONTEXT_OPENGL_PROFILE_MASK,
EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT,
EGL_NONE};
const auto compatContext =
fixture->State.CreateContext(fixture->Display, fixture->Config, EGL_NO_CONTEXT, compatAttribs);
ASSERT_NE(compatContext, EGL_NO_CONTEXT);
EXPECT_TRUE(fixture->State.MakeCurrent(fixture->Display, fixture->Surface, fixture->Surface, compatContext));
EXPECT_TRUE(fixture->State.IsCurrentContextOpenGLCompatibilityProfile());
EXPECT_FALSE(fixture->State.IsCurrentContextOpenGLCoreProfile());
EXPECT_TRUE(fixture->State.MakeCurrent(EGL_NO_DISPLAY, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT));
EXPECT_FALSE(fixture->State.IsCurrentContextOpenGLCompatibilityProfile());
EXPECT_EQ(fixture->State.ConsumeError(), EGL_SUCCESS);
}
+1 -1
View File
@@ -17,4 +17,4 @@ target_link_libraries(
)
include(GoogleTest)
gtest_discover_tests(FramebufferTest DISCOVERY_TIMEOUT 30)
gtest_discover_tests(FramebufferTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
@@ -8,9 +8,12 @@
#include <gtest/gtest.h>
#include <limits>
#include "Includes.h"
#include "Init.h"
#include <MG_Backend/BackendObjects.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>
@@ -32,6 +35,8 @@ namespace {
Int g_clearNamedFramebufferfvCallCount = 0;
Int g_clearNamedFramebufferfiCallCount = 0;
Int g_readPixelsCallCount = 0;
GLenum g_lastReadPixelsFormat = GL_NONE;
GLenum g_lastReadPixelsType = GL_NONE;
void RecordBlitNamedFramebuffer(const SharedPtr<MG_State::GLState::FramebufferObject>& readFramebuffer,
const SharedPtr<MG_State::GLState::FramebufferObject>& drawFramebuffer,
@@ -66,15 +71,39 @@ namespace {
++g_clearNamedFramebufferfiCallCount;
}
void RecordReadPixels(GLint, GLint, GLsizei, GLsizei, GLenum, GLenum, void*) {
void RecordReadPixels(GLint, GLint, GLsizei, GLsizei, GLenum format, GLenum type, void*) {
++g_readPixelsCallCount;
g_lastReadPixelsFormat = format;
g_lastReadPixelsType = type;
}
} // namespace
class FramebufferTest : public ::testing::Test {
protected:
// GL error flags are sticky per error code and the context outlives an individual test in this
// binary, so drain whatever an earlier test left pending - otherwise an error-code assertion
// here reads someone else's error. Bounded: one flag per code, so this cannot hang the suite.
static void DrainPendingGlErrors() {
for (Int drained = 0; drained < 16 && MG_Impl::GLImpl::GetError() != GL_NO_ERROR; ++drained) {
}
}
// The call under test must raise exactly the expected error and nothing more: a second pending
// error means one entry point queued several, which GetError() would hand out at an unrelated
// call site later on.
static void ExpectSingleGlError(GLenum expected) {
EXPECT_EQ(MG_Impl::GLImpl::GetError(), expected);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "the call recorded more than one error";
}
void TearDown() override {
// Attribute a leaked error to the test that caused it instead of to whoever runs next.
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "test left an unconsumed GL error behind";
}
void SetUp() override {
MobileGL::Initialize();
DrainPendingGlErrors();
const auto defaultFramebuffer = MG_State::pGLContext->GetFramebufferObject(0);
ASSERT_NE(defaultFramebuffer, nullptr);
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).Bind(defaultFramebuffer);
@@ -97,6 +126,8 @@ protected:
g_clearNamedFramebufferfvCallCount = 0;
g_clearNamedFramebufferfiCallCount = 0;
g_readPixelsCallCount = 0;
g_lastReadPixelsFormat = GL_NONE;
g_lastReadPixelsType = GL_NONE;
MG_Backend::gBackendFunctionsTable.GL.BlitNamedFramebuffer = nullptr;
MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferfv = nullptr;
MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferfi = nullptr;
@@ -115,6 +146,83 @@ TEST_F(FramebufferTest, CreateFramebuffersCreatesObjectsImmediately) {
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// GL 3.3 core 4.4.1/4.4.2 name lifecycle - mirrors the rules asserted for the other object
// families: deleting an unknown name is silent, a released reservation is recycled, and binding
// a dead name is INVALID_OPERATION.
TEST_F(FramebufferTest, DeleteOfUnknownOrAlreadyDeletedFramebufferNameIsSilent) {
GLuint framebuffer = 0;
MG_Impl::GLImpl::GenFramebuffers(1, &framebuffer);
ASSERT_NE(framebuffer, 0u);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::DeleteFramebuffers(1, &framebuffer);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::DeleteFramebuffers(1, &framebuffer);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// Not a small literal: other tests in this binary share the context and generate names in
// bulk, so a low number may well be a legitimately reserved name here.
const GLuint unknownNames[] = {0u, std::numeric_limits<GLuint>::max()};
MG_Impl::GLImpl::DeleteFramebuffers(2, unknownNames);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(FramebufferTest, DeleteGeneratedButUnboundFramebufferNameReleasesReservationAndBindFails) {
GLuint framebuffer = 0;
MG_Impl::GLImpl::GenFramebuffers(1, &framebuffer);
ASSERT_NE(framebuffer, 0u);
ASSERT_TRUE(MG_State::pGLContext->ValidateFramebufferName(framebuffer));
MG_Impl::GLImpl::DeleteFramebuffers(1, &framebuffer);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
EXPECT_FALSE(MG_State::pGLContext->ValidateFramebufferName(framebuffer));
MG_Impl::GLImpl::BindFramebuffer(GL_FRAMEBUFFER, framebuffer);
ExpectSingleGlError(GL_INVALID_OPERATION);
GLuint recycled = 0;
MG_Impl::GLImpl::GenFramebuffers(1, &recycled);
EXPECT_EQ(recycled, framebuffer);
}
TEST_F(FramebufferTest, DeleteOfUnknownOrAlreadyDeletedRenderbufferNameIsSilent) {
GLuint renderbuffer = 0;
MG_Impl::GLImpl::GenRenderbuffers(1, &renderbuffer);
ASSERT_NE(renderbuffer, 0u);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::DeleteRenderbuffers(1, &renderbuffer);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::DeleteRenderbuffers(1, &renderbuffer);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// Not a small literal: other tests in this binary share the context and generate names in
// bulk, so a low number may well be a legitimately reserved name here.
const GLuint unknownNames[] = {0u, std::numeric_limits<GLuint>::max()};
MG_Impl::GLImpl::DeleteRenderbuffers(2, unknownNames);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(FramebufferTest, DeleteGeneratedButUnboundRenderbufferNameReleasesReservationAndBindFails) {
GLuint renderbuffer = 0;
MG_Impl::GLImpl::GenRenderbuffers(1, &renderbuffer);
ASSERT_NE(renderbuffer, 0u);
ASSERT_TRUE(MG_State::pGLContext->ValidateRenderbufferName(renderbuffer));
MG_Impl::GLImpl::DeleteRenderbuffers(1, &renderbuffer);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
EXPECT_FALSE(MG_State::pGLContext->ValidateRenderbufferName(renderbuffer));
MG_Impl::GLImpl::BindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
ExpectSingleGlError(GL_INVALID_OPERATION);
GLuint recycled = 0;
MG_Impl::GLImpl::GenRenderbuffers(1, &recycled);
EXPECT_EQ(recycled, renderbuffer);
}
TEST_F(FramebufferTest, DefaultFramebufferIdentityTracksFramebufferNameZero) {
const auto defaultFramebuffer = MG_State::pGLContext->GetFramebufferObject(0);
ASSERT_NE(defaultFramebuffer, nullptr);
@@ -221,6 +329,77 @@ TEST_F(FramebufferTest, ReadPixelsAllowsPersistentMappedPixelPackBuffer) {
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(FramebufferTest, ReadPixelsRejectsMismatchedPackedTypeFormatPairs) {
GLuint framebuffer = 0;
GLuint texture = 0;
MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer);
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture);
MG_Impl::GLImpl::TextureStorage2D(texture, 1, GL_RGBA8, 4, 4);
MG_Impl::GLImpl::NamedFramebufferTexture(framebuffer, GL_COLOR_ATTACHMENT0, texture, 0);
MG_Impl::GLImpl::BindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer);
MG_Backend::gBackendFunctionsTable.GL.ReadPixels = RecordReadPixels;
Uint8 pixelStorage[4 * 4 * 4] = {};
// Packed RGB type with a non-RGB format must never reach the backend (GL CTS packed_pixels
// reads GL_RED with GL_UNSIGNED_SHORT_5_6_5 and expects an error).
MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, GL_RED, GL_UNSIGNED_SHORT_5_6_5, pixelStorage);
EXPECT_EQ(g_readPixelsCallCount, 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
// Packed RGBA type with a non-RGBA/BGRA format is rejected as well.
MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, GL_RGB, GL_UNSIGNED_INT_8_8_8_8, pixelStorage);
EXPECT_EQ(g_readPixelsCallCount, 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
// Packed depth-stencil type requires the DEPTH_STENCIL format.
MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, GL_RGBA, GL_UNSIGNED_INT_24_8, pixelStorage);
EXPECT_EQ(g_readPixelsCallCount, 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
// A plain RGBA/UNSIGNED_BYTE readback keeps working.
MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, GL_RGBA, GL_UNSIGNED_BYTE, pixelStorage);
EXPECT_EQ(g_readPixelsCallCount, 1);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(FramebufferTest, ReadPixelsForwardsSingleChannelDesktopClientFormats) {
GLuint framebuffer = 0;
GLuint texture = 0;
MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer);
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture);
MG_Impl::GLImpl::TextureStorage2D(texture, 1, GL_RGBA8, 4, 4);
MG_Impl::GLImpl::NamedFramebufferTexture(framebuffer, GL_COLOR_ATTACHMENT0, texture, 0);
MG_Impl::GLImpl::BindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer);
MG_Backend::gBackendFunctionsTable.GL.ReadPixels = RecordReadPixels;
Uint8 pixelStorage[4 * 4 * 4] = {};
// Desktop GL treats GL_GREEN/GL_BLUE/GL_ALPHA as valid ReadPixels client formats (GL CTS
// packed_pixels rgba8_format_green failed with GL_INVALID_ENUM before). The state layer must
// validate them and forward the raw enum to the backend, which extracts the source channel
// from a wide RGBA read.
const GLenum singleChannelFormats[] = {GL_GREEN, GL_BLUE, GL_ALPHA};
Int expectedCallCount = 0;
for (const GLenum format : singleChannelFormats) {
MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, format, GL_UNSIGNED_BYTE, pixelStorage);
EXPECT_EQ(g_readPixelsCallCount, ++expectedCallCount);
EXPECT_EQ(g_lastReadPixelsFormat, format);
EXPECT_EQ(g_lastReadPixelsType, static_cast<GLenum>(GL_UNSIGNED_BYTE));
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// Packed-type pairing still applies: packed RGB/RGBA types never pair with single-channel formats.
MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, GL_GREEN, GL_UNSIGNED_SHORT_5_6_5, pixelStorage);
EXPECT_EQ(g_readPixelsCallCount, expectedCallCount);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
// Integer client formats reject floating-point types.
MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, GL_GREEN_INTEGER, GL_FLOAT, pixelStorage);
EXPECT_EQ(g_readPixelsCallCount, expectedCallCount);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
}
TEST_F(FramebufferTest, NamedRenderbufferStorageAndFramebufferAttachDoNotChangeBindings) {
GLuint framebuffer = 0;
GLuint renderbuffer = 0;
@@ -419,3 +598,263 @@ TEST_F(FramebufferTest, BlitNamedFramebufferAllowsDefaultFramebufferZero) {
EXPECT_EQ(MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject(), defaultRead);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// ---- Packed-type readback encoding ------------------------------------------------------------------
// Oracle-independent guard for the DirectGLES client-format readback conversion: feeds known wide RGBA
// rows through ReadbackImpl::ConvertWideReadbackRow and asserts the exact packed words. Field positions
// were hand-computed from GL 3.3 table 3.6 and match the GL CTS packed_pixels comparison functions
// (glcPackedPixelsTests.cpp pack_UNSIGNED_*): non-REV types pack the first format component from the
// most significant bit, *_REV types from the least significant bit.
namespace {
namespace ReadbackImpl = MG_Backend::DirectGLES::ReadbackImpl;
// Converts a row of wide pixels (4 components of wideType each) into `format`/`type` words.
template <typename WordT, typename SrcT>
Vector<WordT> ConvertWideRowToPackedWords(const Vector<SrcT>& wide, GLenum wideType, GLenum format,
GLenum type) {
ReadbackImpl::ReadbackChannelMapping mapping{};
EXPECT_TRUE(ReadbackImpl::GetReadbackChannelMapping(format, mapping));
EXPECT_EQ(ReadbackImpl::GetReadbackDstPixelSize(mapping, type), sizeof(WordT));
const SizeT width = wide.size() / 4;
Vector<WordT> out(width, static_cast<WordT>(0));
ReadbackImpl::ConvertWideReadbackRow(reinterpret_cast<const Uint8*>(wide.data()),
reinterpret_cast<Uint8*>(out.data()), width, wideType, mapping,
type);
return out;
}
// Normalized encodes read the wide row as RGBA8 (values are v / 255).
template <typename WordT>
Vector<WordT> ConvertRGBA8Row(const Vector<Uint8>& rgba, GLenum format, GLenum type) {
return ConvertWideRowToPackedWords<WordT>(rgba, GL_UNSIGNED_BYTE, format, type);
}
// Wide RGBA8 pattern shared by the normalized-encode tests. Expected fields below are
// round(v / 255 * (2^bits - 1)), computed by hand per pixel.
// R G B A
const Vector<Uint8> kRGBA8Row{255, 0, 128, 64, // P0
10, 250, 33, 200, // P1
85, 170, 255, 0}; // P2
} // namespace
TEST(PackedReadbackEncodeTest, EncodesUnsignedShort565) {
// P0: R=31 G=0 B=round(128*31/255)=16 -> 31<<11 | 0<<5 | 16 = 0xF810
// P1: R=round(10*31/255)=1 G=round(250*63/255)=62 B=round(33*31/255)=4 -> 1<<11|62<<5|4 = 0x0FC4
// P2: R=round(85*31/255)=10 G=round(170*63/255)=42 B=31 -> 10<<11|42<<5|31 = 0x555F
const auto words = ConvertRGBA8Row<Uint16>(kRGBA8Row, GL_RGB, GL_UNSIGNED_SHORT_5_6_5);
EXPECT_EQ(words[0], 0xF810u);
EXPECT_EQ(words[1], 0x0FC4u);
EXPECT_EQ(words[2], 0x555Fu);
}
TEST(PackedReadbackEncodeTest, EncodesUnsignedShort565Rev) {
// REV packs R from the LSB: P2 -> 10 | 42<<5 | 31<<11 = 0xFD4A
const auto words = ConvertRGBA8Row<Uint16>(kRGBA8Row, GL_RGB, GL_UNSIGNED_SHORT_5_6_5_REV);
EXPECT_EQ(words[2], 0xFD4Au);
}
TEST(PackedReadbackEncodeTest, EncodesUnsignedShort4444) {
// P0: R=15 G=0 B=round(128*15/255)=8 A=round(64*15/255)=4 -> 0xF084
// P2: R=round(85*15/255)=5 G=round(170*15/255)=10 B=15 A=0 -> 0x5AF0
const auto words = ConvertRGBA8Row<Uint16>(kRGBA8Row, GL_RGBA, GL_UNSIGNED_SHORT_4_4_4_4);
EXPECT_EQ(words[0], 0xF084u);
EXPECT_EQ(words[2], 0x5AF0u);
}
TEST(PackedReadbackEncodeTest, EncodesUnsignedShort4444Rev) {
// P0 fields R=15 G=0 B=8 A=4 packed from the LSB -> 15 | 0<<4 | 8<<8 | 4<<12 = 0x480F
const auto words = ConvertRGBA8Row<Uint16>(kRGBA8Row, GL_RGBA, GL_UNSIGNED_SHORT_4_4_4_4_REV);
EXPECT_EQ(words[0], 0x480Fu);
}
TEST(PackedReadbackEncodeTest, EncodesUnsignedShort5551) {
// P0: R=31 G=0 B=16 A=round(64/255)=0 -> 31<<11 | 16<<1 = 0xF820
// P1: R=1 G=round(250*31/255)=30 B=4 A=round(200/255)=1 -> 1<<11|30<<6|4<<1|1 = 0x0F89
const auto words = ConvertRGBA8Row<Uint16>(kRGBA8Row, GL_RGBA, GL_UNSIGNED_SHORT_5_5_5_1);
EXPECT_EQ(words[0], 0xF820u);
EXPECT_EQ(words[1], 0x0F89u);
}
TEST(PackedReadbackEncodeTest, EncodesUnsignedShort1555Rev) {
// P1 fields R=1 G=30 B=4 A=1 packed from the LSB -> 1 | 30<<5 | 4<<10 | 1<<15 = 0x93C1
const auto words = ConvertRGBA8Row<Uint16>(kRGBA8Row, GL_RGBA, GL_UNSIGNED_SHORT_1_5_5_5_REV);
EXPECT_EQ(words[1], 0x93C1u);
}
TEST(PackedReadbackEncodeTest, EncodesUnsignedInt2101010Rev) {
// P0: R=1023 G=0 B=round(128*1023/255)=514 A=round(64*3/255)=1 -> 1023|514<<20|1<<30 = 0x602003FF
// P2: R=round(85*1023/255)=341 G=round(170*1023/255)=682 B=1023 A=0 -> 0x3FFAA955
const auto words = ConvertRGBA8Row<Uint32>(kRGBA8Row, GL_RGBA, GL_UNSIGNED_INT_2_10_10_10_REV);
EXPECT_EQ(words[0], 0x602003FFu);
EXPECT_EQ(words[2], 0x3FFAA955u);
}
TEST(PackedReadbackEncodeTest, EncodesUnsignedInt1010102) {
// P2 fields R=341 G=682 B=1023 A=0 packed from the MSB -> 341<<22 | 682<<12 | 1023<<2 = 0x556AAFFC
const auto words = ConvertRGBA8Row<Uint32>(kRGBA8Row, GL_RGBA, GL_UNSIGNED_INT_10_10_10_2);
EXPECT_EQ(words[2], 0x556AAFFCu);
}
TEST(PackedReadbackEncodeTest, EncodesUnsignedByte332) {
// P0: R=7 G=0 B=round(128*3/255)=2 -> 7<<5 | 2 = 0xE2
// P1: R=round(10*7/255)=0 G=round(250*7/255)=7 B=round(33*3/255)=0 -> 7<<2 = 0x1C
const auto words = ConvertRGBA8Row<Uint8>(kRGBA8Row, GL_RGB, GL_UNSIGNED_BYTE_3_3_2);
EXPECT_EQ(words[0], 0xE2u);
EXPECT_EQ(words[1], 0x1Cu);
}
TEST(PackedReadbackEncodeTest, EncodesUnsignedByte233Rev) {
// P0 fields R=7 G=0 B=2 packed from the LSB -> 7 | 0<<3 | 2<<6 = 0x87
const auto words = ConvertRGBA8Row<Uint8>(kRGBA8Row, GL_RGB, GL_UNSIGNED_BYTE_2_3_3_REV);
EXPECT_EQ(words[0], 0x87u);
}
TEST(PackedReadbackEncodeTest, Encodes8888KeepsLegacyByteOrder) {
// Regression for the previously supported types: P0 = (255, 0, 128, 64).
const auto msbFirst = ConvertRGBA8Row<Uint32>(kRGBA8Row, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8);
EXPECT_EQ(msbFirst[0], 0xFF008040u);
const auto lsbFirst = ConvertRGBA8Row<Uint32>(kRGBA8Row, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8_REV);
EXPECT_EQ(lsbFirst[0], 0x408000FFu);
}
TEST(PackedReadbackEncodeTest, EncodesBGRAWithChannelMapping) {
// BGRA's first format component is Blue: P0 fields B=8 G=0 R=15 A=4 -> 8<<12 | 15<<4 | 4 = 0x80F4
const auto words = ConvertRGBA8Row<Uint16>(kRGBA8Row, GL_BGRA, GL_UNSIGNED_SHORT_4_4_4_4);
EXPECT_EQ(words[0], 0x80F4u);
}
TEST(PackedReadbackEncodeTest, EncodesIntegerRGBA2101010RevWithFieldClamp) {
// Integer sources clamp to each field's unsigned range (10/10/10/2 bits).
const Vector<Uint32> wide{1023u, 1024u, 5u, 4u};
const auto words =
ConvertWideRowToPackedWords<Uint32>(wide, GL_UNSIGNED_INT, GL_RGBA_INTEGER, GL_UNSIGNED_INT_2_10_10_10_REV);
EXPECT_EQ(words[0], 0xC05FFFFFu); // 1023 | 1023<<10 | 5<<20 | 3<<30
}
TEST(PackedReadbackEncodeTest, EncodesIntegerNegativeValuesClampToZero) {
const Vector<Int32> wide{-5, 2, 100000, 1};
const auto words =
ConvertWideRowToPackedWords<Uint32>(wide, GL_INT, GL_RGBA_INTEGER, GL_UNSIGNED_INT_2_10_10_10_REV);
EXPECT_EQ(words[0], 0x7FF00800u); // 0 | 2<<10 | 1023<<20 | 1<<30
}
TEST(PackedReadbackEncodeTest, EncodesIntegerRGB565) {
const Vector<Uint32> wide{31u, 64u, 2u, 0u};
const auto words =
ConvertWideRowToPackedWords<Uint16>(wide, GL_UNSIGNED_INT, GL_RGB_INTEGER, GL_UNSIGNED_SHORT_5_6_5);
EXPECT_EQ(words[0], 0xFFE2u); // 31<<11 | 63<<5 | 2 (G clamps 64 -> 63)
}
TEST(PackedReadbackEncodeTest, EncodesPackedFloat10F11F11FRev) {
// F11(1.0)=0x3C0 F11(0.5)=0x380 F10(0.25)=0x1A0 -> 0x3C0 | 0x380<<11 | 0x1A0<<22 = 0x681C03C0.
// Second pixel: values above 65024 clamp to the max finite F11 (0x7BF), negatives go to zero.
const Vector<Float> wide{1.0f, 0.5f, 0.25f, 1.0f, 100000.0f, -1.0f, 0.25f, 1.0f};
const auto words = ConvertWideRowToPackedWords<Uint32>(wide, GL_FLOAT, GL_RGB, GL_UNSIGNED_INT_10F_11F_11F_REV);
EXPECT_EQ(words[0], 0x681C03C0u);
EXPECT_EQ(words[1], 0x680007BFu);
}
TEST(PackedReadbackEncodeTest, EncodesSharedExponent5999Rev) {
// (1.0, 0.5, 0.25): shared exponent 16, fields 256/128/64 -> 256 | 128<<9 | 64<<18 | 16<<27
const Vector<Float> wide{1.0f, 0.5f, 0.25f, 1.0f};
const auto words = ConvertWideRowToPackedWords<Uint32>(wide, GL_FLOAT, GL_RGB, GL_UNSIGNED_INT_5_9_9_9_REV);
EXPECT_EQ(words[0], 0x81010100u);
}
TEST(PackedReadbackEncodeTest, RejectsMismatchedPackedFieldCounts) {
ReadbackImpl::ReadbackChannelMapping rgba{};
ASSERT_TRUE(ReadbackImpl::GetReadbackChannelMapping(GL_RGBA, rgba));
ReadbackImpl::ReadbackChannelMapping rgbInteger{};
ASSERT_TRUE(ReadbackImpl::GetReadbackChannelMapping(GL_RGB_INTEGER, rgbInteger));
// 3-field packed types never pair with 4-component formats and vice versa.
EXPECT_EQ(ReadbackImpl::GetReadbackDstPixelSize(rgba, GL_UNSIGNED_SHORT_5_6_5), 0u);
EXPECT_EQ(ReadbackImpl::GetReadbackDstPixelSize(rgbInteger, GL_UNSIGNED_SHORT_4_4_4_4), 0u);
// Packed-float RGB types never pair with integer formats.
EXPECT_EQ(ReadbackImpl::GetReadbackDstPixelSize(rgbInteger, GL_UNSIGNED_INT_5_9_9_9_REV), 0u);
EXPECT_EQ(ReadbackImpl::GetReadbackDstPixelSize(rgbInteger, GL_UNSIGNED_INT_10F_11F_11F_REV), 0u);
}
// ---- GL CTS packed_pixels readback root-cause regressions --------------------------------------
TEST_F(FramebufferTest, ReadPixelsRejectsIntegerFormatMismatchWithReadBuffer) {
GLuint framebuffer = 0;
GLuint texture = 0;
MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer);
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture);
MG_Impl::GLImpl::TextureStorage2D(texture, 1, GL_RGBA8UI, 4, 4);
MG_Impl::GLImpl::NamedFramebufferTexture(framebuffer, GL_COLOR_ATTACHMENT0, texture, 0);
MG_Impl::GLImpl::BindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer);
MG_Backend::gBackendFunctionsTable.GL.ReadPixels = RecordReadPixels;
Uint8 pixelStorage[4 * 4 * 4] = {};
// GL 3.3 section 4.3.1: normalized format on an integer read buffer -> GL_INVALID_OPERATION
// (GL CTS packed_pixels expects the error for every mismatched combination).
MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, GL_RGBA, GL_UNSIGNED_BYTE, pixelStorage);
EXPECT_EQ(g_readPixelsCallCount, 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
// The matching integer readback stays valid.
MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, GL_RGBA_INTEGER, GL_UNSIGNED_INT, pixelStorage);
EXPECT_EQ(g_readPixelsCallCount, 1);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// And the inverse mismatch: integer format on a normalized attachment.
GLuint normalizedTexture = 0;
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &normalizedTexture);
MG_Impl::GLImpl::TextureStorage2D(normalizedTexture, 1, GL_RGBA8, 4, 4);
MG_Impl::GLImpl::NamedFramebufferTexture(framebuffer, GL_COLOR_ATTACHMENT0, normalizedTexture, 0);
MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, GL_RGBA_INTEGER, GL_UNSIGNED_INT, pixelStorage);
EXPECT_EQ(g_readPixelsCallCount, 1);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
}
TEST_F(FramebufferTest, BindRenderbufferZeroUnbindsWithoutError) {
// The GL CTS state reset calls glBindRenderbuffer(GL_RENDERBUFFER, 0) and expects no error;
// name 0 used to be reported as an invalid renderbuffer name.
MG_Impl::GLImpl::BindRenderbuffer(GL_RENDERBUFFER, 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(FramebufferTest, FramebufferTexture3DAttachesSliceWithLayerTracking) {
// glFramebufferTexture3D with zoffset used to be rejected outright, leaving a sticky
// GL_INVALID_OPERATION behind (GL CTS packed_pixels varied_rectangle runs on GL_TEXTURE_3D).
GLuint framebuffer = 0;
GLuint texture = 0;
MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer);
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_3D, 1, &texture);
MG_Impl::GLImpl::TextureStorage3D(texture, 1, GL_RGBA8, 4, 4, 2);
MG_Impl::GLImpl::BindFramebuffer(GL_DRAW_FRAMEBUFFER, framebuffer);
MG_Impl::GLImpl::FramebufferTexture3D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_3D, texture, 0, 1);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
const auto framebufferObject = MG_State::pGLContext->GetFramebufferObject(framebuffer);
ASSERT_NE(framebufferObject, nullptr);
const auto& attachment = framebufferObject->GetAttachment(FramebufferAttachmentType::Color0);
ASSERT_TRUE(attachment.IsTexture());
EXPECT_EQ(attachment.GetTextureLayer(), 1);
EXPECT_FALSE(attachment.IsLayered());
}
TEST_F(FramebufferTest, NonRenderableColorFormatsReportUnsupportedFramebuffer) {
// Without a probing backend the conservative list applies: RGB9_E5 is texture-only, so
// attaching it must not report GL_FRAMEBUFFER_COMPLETE (GL CTS packed_pixels rgb9_e5 expects
// read errors instead of silent unwritten readbacks).
GLuint framebuffer = 0;
GLuint texture = 0;
MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer);
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture);
MG_Impl::GLImpl::TextureStorage2D(texture, 1, GL_RGB9_E5, 4, 4);
MG_Impl::GLImpl::NamedFramebufferTexture(framebuffer, GL_COLOR_ATTACHMENT0, texture, 0);
MG_Impl::GLImpl::BindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer);
EXPECT_EQ(MG_Impl::GLImpl::CheckFramebufferStatus(GL_READ_FRAMEBUFFER),
static_cast<GLenum>(GL_FRAMEBUFFER_UNSUPPORTED));
Uint8 pixelStorage[4 * 4 * 4] = {};
MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, GL_RGBA, GL_UNSIGNED_BYTE, pixelStorage);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_FRAMEBUFFER_OPERATION);
}
+2 -2
View File
@@ -39,5 +39,5 @@ target_link_libraries(
)
include(GoogleTest)
gtest_discover_tests(ProgramUtilTest DISCOVERY_TIMEOUT 30)
gtest_discover_tests(ProgramTest DISCOVERY_TIMEOUT 30)
gtest_discover_tests(ProgramUtilTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
gtest_discover_tests(ProgramTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
+437
View File
@@ -1845,3 +1845,440 @@ TEST_F(ProgramTest, GetActiveUniformsivErrors) {
EXPECT_EQ(GetError(), GL_NO_ERROR);
EXPECT_EQ(params[0], -999);
}
namespace {
GLuint LinkVsFsProgram(const char* vsSource, const char* fsSource) {
char infoLog[4096] = "";
GLuint vs = CreateShader(GL_VERTEX_SHADER);
ShaderSource(vs, 1, &vsSource, nullptr);
CompileShader(vs);
GLint vsStatus = GL_FALSE;
GetShaderiv(vs, GL_COMPILE_STATUS, &vsStatus);
GetShaderInfoLog(vs, sizeof(infoLog), nullptr, infoLog);
EXPECT_EQ(vsStatus, GL_TRUE) << infoLog;
GLuint fs = CreateShader(GL_FRAGMENT_SHADER);
ShaderSource(fs, 1, &fsSource, nullptr);
CompileShader(fs);
GLint fsStatus = GL_FALSE;
GetShaderiv(fs, GL_COMPILE_STATUS, &fsStatus);
GetShaderInfoLog(fs, sizeof(infoLog), nullptr, infoLog);
EXPECT_EQ(fsStatus, GL_TRUE) << infoLog;
GLuint program = CreateProgram();
AttachShader(program, vs);
AttachShader(program, fs);
LinkProgram(program);
GLint linkStatus = GL_FALSE;
GetProgramiv(program, GL_LINK_STATUS, &linkStatus);
GetProgramInfoLog(program, sizeof(infoLog), nullptr, infoLog);
EXPECT_EQ(linkStatus, GL_TRUE) << infoLog;
return program;
}
const char* kPassthroughCoordsVs = R"(#version 330
in vec4 a_position;
in vec4 a_coords;
out vec4 coords_in;
void main() {
gl_Position = a_position;
coords_in = a_coords;
})";
} // namespace
// Repro for KHR-GL33.shaders.loops.do_while_dynamic_iterations.empty_body_* (and the
// only_continue / unconditional_break variants): the loop is dead code, so the SPIR-V
// optimizer eliminates it together with the only loads of `one` / `ui_one` -- and with
// them the entire global UBO. The uniforms stay active in link reflection, so
// glUniform1i on them must still have backing storage instead of memcpy-ing to null.
TEST_F(ProgramTest, DoWhileDeadLoopUniformsKeepBackingStorage) {
const char* loopBodies[] = {"", "continue;", "break;"};
for (const char* body : loopBodies) {
const String fsSource = String(R"(#version 330
uniform int ui_one;
uniform mediump int one;
in vec4 coords_in;
out vec4 o_color;
void main() {
vec4 res = coords_in;
mediump int i = 0;
do {)") + body + R"(} while (i++ < one*ui_one);
o_color = res;
})";
GLuint program = LinkVsFsProgram(kPassthroughCoordsVs, fsSource.c_str());
const GLint locOne = GetUniformLocation(program, "one");
const GLint locUiOne = GetUniformLocation(program, "ui_one");
ASSERT_GE(locOne, 0) << "body: '" << body << "'";
ASSERT_GE(locUiOne, 0) << "body: '" << body << "'";
UseProgram(program);
Uniform1i(locOne, 1); // crashed with a null MapUBO() before the fallback storage
Uniform1i(locUiOne, 2);
EXPECT_EQ(GetError(), GL_NO_ERROR) << "body: '" << body << "'";
GLint readback = -1;
GetUniformiv(program, locOne, &readback);
EXPECT_EQ(readback, 1) << "body: '" << body << "'";
readback = -1;
GetUniformiv(program, locUiOne, &readback);
EXPECT_EQ(readback, 2) << "body: '" << body << "'";
EXPECT_EQ(GetError(), GL_NO_ERROR) << "body: '" << body << "'";
}
}
// Repro for KHR-GL33.shaders.struct.uniform.*nested_struct_array_*: leaf uniforms of
// nested struct arrays need (a) one location per array element and (b) real byte
// offsets inside the global UBO. Before the fix every leaf had a single location and
// offset 0, so glUniform2fv(loc, 2, ...) tripped the size assert on the neighboring
// float uniform (and corrupted it in release builds).
TEST_F(ProgramTest, NestedStructArrayUniformElementWrites) {
// Struct shape from CTS glcShaderStructTests nested_struct_array (uniform case).
const char* fsSource = R"(#version 330
struct T {
mediump float a;
mediump vec2 b[2];
};
struct S {
mediump float a;
T b[3];
int c;
};
uniform S s[2];
in vec4 coords_in;
out vec4 o_color;
void main() {
mediump float r = (s[0].b[1].b[0].x + s[1].b[2].b[1].y) * s[0].b[0].a;
mediump float g = s[1].b[0].b[0].y * s[0].b[2].a * s[1].b[2].a;
mediump float b = (s[0].b[2].b[1].y + s[0].b[1].b[0].y + s[1].a) * s[0].b[1].a;
mediump float a = float(s[0].c) + s[1].b[2].a - s[1].b[1].a;
o_color = vec4(r, g, b, a);
})";
GLuint program = LinkVsFsProgram(kPassthroughCoordsVs, fsSource);
UseProgram(program);
const GLint locVecArray = GetUniformLocation(program, "s[0].b[1].b");
ASSERT_GE(locVecArray, 0);
// Element locations are consecutive and reachable via the "[k]" suffix.
EXPECT_EQ(GetUniformLocation(program, "s[0].b[1].b[0]"), locVecArray);
EXPECT_EQ(GetUniformLocation(program, "s[0].b[1].b[1]"), locVecArray + 1);
EXPECT_EQ(GetUniformLocation(program, "s[0].b[1].b[2]"), -1);
// Distinct scalar leaves must land at distinct UBO offsets (they all aliased
// offset 0 before the fix).
const char* scalarLeaves[] = {"s[0].b[0].a", "s[0].b[1].a", "s[0].b[2].a", "s[1].a", "s[1].b[1].a",
"s[1].b[2].a"};
const GLfloat scalarValues[] = {0.5f, 0.25f, 0.125f, 7.0f, 3.0f, 4.0f};
for (SizeT i = 0; i < std::size(scalarLeaves); ++i) {
const GLint loc = GetUniformLocation(program, scalarLeaves[i]);
ASSERT_GE(loc, 0) << scalarLeaves[i];
Uniform1f(loc, scalarValues[i]);
}
// CTS-style whole-array write: glUniform2fv with count = 2 on a vec2[2] leaf.
// Before the fix this asserted/corrupted the next uniform ("s[0].b[2].a").
const GLfloat vecData[4] = {1.0f, 2.0f, 3.0f, 4.0f};
Uniform2fv(locVecArray, 2, vecData);
EXPECT_EQ(GetError(), GL_NO_ERROR);
GLfloat vecReadback[2] = {};
GetUniformfv(program, locVecArray, vecReadback);
EXPECT_EQ(vecReadback[0], 1.0f);
EXPECT_EQ(vecReadback[1], 2.0f);
GetUniformfv(program, locVecArray + 1, vecReadback);
EXPECT_EQ(vecReadback[0], 3.0f);
EXPECT_EQ(vecReadback[1], 4.0f);
// All scalar leaves survived the array write intact.
for (SizeT i = 0; i < std::size(scalarLeaves); ++i) {
GLfloat readback = -1.0f;
GetUniformfv(program, GetUniformLocation(program, scalarLeaves[i]), &readback);
EXPECT_EQ(readback, scalarValues[i]) << scalarLeaves[i];
}
// std140: vec2 array elements inside the struct are 16 bytes apart, and the
// per-element offsets differ.
auto programObject = MG_State::pGLContext->GetProgramObject(program);
ASSERT_NE(programObject, nullptr);
const Uint offsetElement0 = programObject->GetUniformOffset(static_cast<Uint>(locVecArray));
const Uint offsetElement1 = programObject->GetUniformOffset(static_cast<Uint>(locVecArray + 1));
EXPECT_EQ(offsetElement1, offsetElement0 + 16u);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// Plain top-level uniform arrays share the same per-element location machinery.
TEST_F(ProgramTest, PlainArrayUniformElementLocationsAndWrites) {
const char* fsSource = R"(#version 330
uniform float arr[4];
uniform float guard;
in vec4 coords_in;
out vec4 o_color;
void main() {
o_color = vec4(arr[0] + arr[1], arr[2] + arr[3], guard, 1.0);
})";
GLuint program = LinkVsFsProgram(kPassthroughCoordsVs, fsSource);
UseProgram(program);
const GLint locArr = GetUniformLocation(program, "arr");
ASSERT_GE(locArr, 0);
EXPECT_EQ(GetUniformLocation(program, "arr[0]"), locArr);
EXPECT_EQ(GetUniformLocation(program, "arr[2]"), locArr + 2);
EXPECT_EQ(GetUniformLocation(program, "arr[4]"), -1);
const GLint locGuard = GetUniformLocation(program, "guard");
ASSERT_GE(locGuard, 0);
EXPECT_EQ(GetUniformLocation(program, "guard[0]"), -1); // not an array
Uniform1f(locGuard, 9.0f);
const GLfloat values[4] = {1.0f, 2.0f, 3.0f, 4.0f};
Uniform1fv(locArr, 4, values);
for (int i = 0; i < 4; ++i) {
GLfloat readback = -1.0f;
GetUniformfv(program, locArr + i, &readback);
EXPECT_EQ(readback, values[i]) << "arr[" << i << "]";
}
// Overlong writes stop at the end of the array (GL 3.3 §2.11.4) instead of
// spilling into the next uniform.
const GLfloat tail[3] = {30.0f, 40.0f, 50.0f};
Uniform1fv(GetUniformLocation(program, "arr[2]"), 3, tail);
EXPECT_EQ(GetError(), GL_NO_ERROR);
GLfloat readback = -1.0f;
GetUniformfv(program, locArr + 2, &readback);
EXPECT_EQ(readback, 30.0f);
GetUniformfv(program, locArr + 3, &readback);
EXPECT_EQ(readback, 40.0f);
GetUniformfv(program, locGuard, &readback);
EXPECT_EQ(readback, 9.0f); // untouched by the overlong write
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// ---------------------------------------------------------------------------
// GL CTS KHR-GL33.shaders.uniform_block regression pack. MobileGL's SPIR-V
// pipeline lays every uniform block out as std140; the frontend implements the
// GL-visible consequences of that choice: packed/shared qualifiers compile (as
// std140), reflection uses GL naming ("arr[0]", per-element struct arrays),
// unused block members stay active, block sizes are vec4-padded, and array
// strides are std140 even for arrays nested inside struct members.
// ---------------------------------------------------------------------------
TEST_F(ProgramTest, UniformBlockPackedAndSharedLayoutsCompileAsStd140) {
const char* fsSource = R"(#version 330
layout(packed) uniform PackedBlock {
vec4 pv;
};
layout(shared, row_major) uniform SharedBlock {
float sf;
mat4 sm;
};
out vec4 o_color;
void main() {
o_color = pv + vec4(sf) + vec4(sm[0][0]);
})";
GLuint program = LinkVsFsProgram(kPassthroughCoordsVs, fsSource);
// The blocks land on the implementation's chosen layout: std140 offsets.
const GLuint pv = UniformIndexByName(program, "pv");
const GLuint sf = UniformIndexByName(program, "sf");
const GLuint sm = UniformIndexByName(program, "sm");
ASSERT_NE(pv, GL_INVALID_INDEX);
ASSERT_NE(sf, GL_INVALID_INDEX);
ASSERT_NE(sm, GL_INVALID_INDEX);
EXPECT_EQ(QueryUniformiv(program, pv, GL_UNIFORM_OFFSET), 0);
EXPECT_EQ(QueryUniformiv(program, sf, GL_UNIFORM_OFFSET), 0);
EXPECT_EQ(QueryUniformiv(program, sm, GL_UNIFORM_OFFSET), 16);
// The remaining qualifiers in the rewritten layout() list survive.
EXPECT_EQ(QueryUniformiv(program, sm, GL_UNIFORM_IS_ROW_MAJOR), 1);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
TEST_F(ProgramTest, UniformBlockReflectsUnusedMembersWithGLNamesAndPaddedSize) {
const char* fsSource = R"(#version 330
layout(std140) uniform Blk {
float used;
vec4 unusedArr[3];
ivec3 tail;
};
out vec4 o_color;
void main() {
o_color = vec4(used);
})";
GLuint program = LinkVsFsProgram(kPassthroughCoordsVs, fsSource);
const GLuint blockIndex = GetUniformBlockIndex(program, "Blk");
ASSERT_NE(blockIndex, GL_INVALID_INDEX);
// All three members are active (unusedArr and tail are never read), the array is
// reported under its GL name "unusedArr[0]", and both spellings resolve.
const GLuint used = UniformIndexByName(program, "used");
const GLuint unusedSuffixed = UniformIndexByName(program, "unusedArr[0]");
const GLuint unusedBare = UniformIndexByName(program, "unusedArr");
const GLuint tail = UniformIndexByName(program, "tail");
ASSERT_NE(used, GL_INVALID_INDEX);
ASSERT_NE(unusedSuffixed, GL_INVALID_INDEX);
ASSERT_NE(tail, GL_INVALID_INDEX);
EXPECT_EQ(unusedSuffixed, unusedBare);
char nameBuf[64] = "";
GLsizei nameLen = 0;
GLint arraySize = 0;
GLenum type = 0;
GetActiveUniform(program, unusedSuffixed, sizeof(nameBuf), &nameLen, &arraySize, &type, nameBuf);
EXPECT_STREQ(nameBuf, "unusedArr[0]");
EXPECT_EQ(arraySize, 3);
EXPECT_EQ(type, static_cast<GLenum>(GL_FLOAT_VEC4));
// std140 layout of the unused members.
EXPECT_EQ(QueryUniformiv(program, unusedSuffixed, GL_UNIFORM_OFFSET), 16);
EXPECT_EQ(QueryUniformiv(program, unusedSuffixed, GL_UNIFORM_ARRAY_STRIDE), 16);
EXPECT_EQ(QueryUniformiv(program, tail, GL_UNIFORM_OFFSET), 64);
// GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS agrees with the INDICES list and counts all members.
GLint activeInBlock = 0;
GetActiveUniformBlockiv(program, blockIndex, GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS, &activeInBlock);
ASSERT_EQ(activeInBlock, 3);
GLint indices[3] = {-1, -1, -1};
GetActiveUniformBlockiv(program, blockIndex, GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES, indices);
for (GLint index : indices) {
EXPECT_TRUE(index == static_cast<GLint>(used) || index == static_cast<GLint>(unusedSuffixed) ||
index == static_cast<GLint>(tail));
}
// The block ends with an ivec3 at offset 64 (unpadded end 76); the backend compiles
// the std140 block at its vec4-padded size, and the reported size must cover it or
// buffers sized from this query are too small to draw with.
GLint dataSize = 0;
GetActiveUniformBlockiv(program, blockIndex, GL_UNIFORM_BLOCK_DATA_SIZE, &dataSize);
EXPECT_EQ(dataSize, 80);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
TEST_F(ProgramTest, UniformBlockStructArrayExpandsPerElementWithStd140Strides) {
const char* fsSource = R"(#version 330
struct S {
ivec2 v[2];
float f;
};
layout(std140) uniform Blk2 {
S s[2];
} inst;
out vec4 o_color;
void main() {
o_color = vec4(inst.s[0].f);
})";
GLuint program = LinkVsFsProgram(kPassthroughCoordsVs, fsSource);
// ARB_program_interface_query naming: one entry per struct array element, prefixed
// with the BLOCK name (not the instance name), basic arrays suffixed with "[0]".
const GLuint v0 = UniformIndexByName(program, "Blk2.s[0].v[0]");
const GLuint f0 = UniformIndexByName(program, "Blk2.s[0].f");
const GLuint v1 = UniformIndexByName(program, "Blk2.s[1].v[0]");
const GLuint f1 = UniformIndexByName(program, "Blk2.s[1].f");
ASSERT_NE(v0, GL_INVALID_INDEX);
ASSERT_NE(f0, GL_INVALID_INDEX);
ASSERT_NE(v1, GL_INVALID_INDEX);
ASSERT_NE(f1, GL_INVALID_INDEX);
// std140: ivec2 v[2] rounds each element up to a vec4 (stride 16, NOT the tight 8
// glslang reflects for arrays nested inside a struct member); struct size rounds to
// 48, giving s[1] members a 48-byte bias.
EXPECT_EQ(QueryUniformiv(program, v0, GL_UNIFORM_OFFSET), 0);
EXPECT_EQ(QueryUniformiv(program, v0, GL_UNIFORM_ARRAY_STRIDE), 16);
EXPECT_EQ(QueryUniformiv(program, v0, GL_UNIFORM_SIZE), 2);
EXPECT_EQ(QueryUniformiv(program, f0, GL_UNIFORM_OFFSET), 32);
EXPECT_EQ(QueryUniformiv(program, v1, GL_UNIFORM_OFFSET), 48);
EXPECT_EQ(QueryUniformiv(program, f1, GL_UNIFORM_OFFSET), 80);
GLint dataSize = 0;
const GLuint blockIndex = GetUniformBlockIndex(program, "Blk2");
ASSERT_NE(blockIndex, GL_INVALID_INDEX);
GetActiveUniformBlockiv(program, blockIndex, GL_UNIFORM_BLOCK_DATA_SIZE, &dataSize);
EXPECT_EQ(dataSize, 96);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
TEST_F(ProgramTest, UniformBlockInstanceArrayReportsPerInstanceBlocks) {
const char* fsSource = R"(#version 330
layout(std140) uniform ArrBlk {
vec4 av;
} insts[2];
out vec4 o_color;
void main() {
o_color = insts[0].av + insts[1].av;
})";
GLuint program = LinkVsFsProgram(kPassthroughCoordsVs, fsSource);
const GLuint inst0 = GetUniformBlockIndex(program, "ArrBlk[0]");
const GLuint inst1 = GetUniformBlockIndex(program, "ArrBlk[1]");
ASSERT_NE(inst0, GL_INVALID_INDEX);
ASSERT_NE(inst1, GL_INVALID_INDEX);
EXPECT_NE(inst0, inst1);
// A bare block name resolves to the first instance.
EXPECT_EQ(GetUniformBlockIndex(program, "ArrBlk"), inst0);
// Every instance of the array shares the single reflected member set.
GLint count0 = 0;
GLint count1 = 0;
GetActiveUniformBlockiv(program, inst0, GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS, &count0);
GetActiveUniformBlockiv(program, inst1, GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS, &count1);
EXPECT_EQ(count0, 1);
EXPECT_EQ(count1, 1);
GLint index0 = -1;
GLint index1 = -1;
GetActiveUniformBlockiv(program, inst0, GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES, &index0);
GetActiveUniformBlockiv(program, inst1, GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES, &index1);
EXPECT_EQ(index0, index1);
EXPECT_EQ(static_cast<GLuint>(index0), UniformIndexByName(program, "ArrBlk.av"));
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
TEST_F(ProgramTest, DeleteShaderWhileAttachedKeepsNameUsableUntilDetach) {
// GL CTS compiles through exactly this sequence (create, attach, DELETE, source,
// compile): glDeleteShader on an attached shader only flags it, and the name must
// keep working until the last detach.
const char* vsSource = R"(#version 330
void main() { gl_Position = vec4(0.0); }
)";
const char* fsSource = R"(#version 330
out vec4 o_color;
void main() { o_color = vec4(1.0); }
)";
GLuint program = CreateProgram();
GLuint vs = CreateShader(GL_VERTEX_SHADER);
AttachShader(program, vs);
DeleteShader(vs);
EXPECT_EQ(IsShader(vs), GL_TRUE); // still alive: attached
ShaderSource(vs, 1, &vsSource, nullptr);
CompileShader(vs);
GLint status = GL_FALSE;
GetShaderiv(vs, GL_COMPILE_STATUS, &status);
EXPECT_EQ(status, GL_TRUE);
status = GL_FALSE;
GetShaderiv(vs, GL_DELETE_STATUS, &status);
EXPECT_EQ(status, GL_TRUE);
GLuint fs = CreateShader(GL_FRAGMENT_SHADER);
AttachShader(program, fs);
DeleteShader(fs);
ShaderSource(fs, 1, &fsSource, nullptr);
CompileShader(fs);
LinkProgram(program);
GLint linkStatus = GL_FALSE;
char infoLog[1024] = "";
GetProgramiv(program, GL_LINK_STATUS, &linkStatus);
GetProgramInfoLog(program, sizeof(infoLog), nullptr, infoLog);
EXPECT_EQ(linkStatus, GL_TRUE) << infoLog;
// The last GL-visible detach releases the flagged shader's name.
DetachShader(program, vs);
EXPECT_EQ(IsShader(vs), GL_FALSE);
// Deleting the program releases the other flagged shader.
DeleteProgram(program);
EXPECT_EQ(IsShader(fs), GL_FALSE);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
+371 -5
View File
@@ -107,7 +107,7 @@ void main() {
PreprocessShaderSource(ShaderStage::Vertex, source);
EXPECT_EQ(source.find("#version 460 core\n"), 0);
EXPECT_EQ(source.find("#version 330 core\n"), 0);
EXPECT_NE(source.find("in vec3 position;"), String::npos);
EXPECT_NE(source.find("out vec2 uv;"), String::npos);
EXPECT_EQ(source.find("attribute"), String::npos);
@@ -136,7 +136,7 @@ void main() {
PreprocessShaderSource(ShaderStage::Fragment, source);
EXPECT_EQ(source.find("#version 460 core\n"), 0);
EXPECT_EQ(source.find("#version 330 core\n"), 0);
EXPECT_NE(source.find("out vec4 mg_FragColor;\n"), String::npos);
EXPECT_NE(source.find("in vec2 uv;"), String::npos);
EXPECT_NE(source.find("texture(texture0, uv)"), String::npos);
@@ -153,6 +153,243 @@ void main() {
}
}
TEST_F(ProgramUtilTest, PreprocessMinecraft112BlurShaderKeepsLegacySampleIdentifier) {
using namespace MG_Util::ShaderTranspiler;
// assets/minecraft/shaders/program/blur.fsh from the unmodified Minecraft 1.12 client jar.
String source = R"(#version 120
uniform sampler2D DiffuseSampler;
varying vec2 texCoord;
varying vec2 oneTexel;
uniform vec2 InSize;
uniform vec2 BlurDir;
uniform float Radius;
void main() {
vec4 blurred = vec4(0.0);
float totalStrength = 0.0;
float totalAlpha = 0.0;
float totalSamples = 0.0;
for(float r = -Radius; r <= Radius; r += 1.0) {
vec4 sample = texture2D(DiffuseSampler, texCoord + oneTexel * r * BlurDir);
// Accumulate average alpha
totalAlpha = totalAlpha + sample.a;
totalSamples = totalSamples + 1.0;
// Accumulate smoothed blur
float strength = 1.0 - abs(r / Radius);
totalStrength = totalStrength + strength;
blurred = blurred + sample;
}
gl_FragColor = vec4(blurred.rgb / (Radius * 2.0 + 1.0), totalAlpha);
}
)";
PreprocessShaderSource(ShaderStage::Fragment, source);
EXPECT_EQ(source.find("#version 330 core\n"), 0);
EXPECT_NE(source.find("vec4 sample = texture(DiffuseSampler"), String::npos);
EXPECT_NE(source.find("totalAlpha = totalAlpha + sample.a;"), String::npos);
EXPECT_NE(source.find("float totalSamples = 0.0;"), String::npos);
EXPECT_NE(source.find("totalSamples = totalSamples + 1.0;"), String::npos);
EXPECT_NE(source.find("blurred = blurred + sample;"), String::npos);
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source};
auto res = ShaderCompiler::CompileShader(attrib);
if (!res) {
FAIL() << "errc: " << res.error().errc << "\nlog: " << res.error().log << "\nsource:\n" << source;
}
}
TEST_F(ProgramUtilTest, PreprocessLegacySampleInterfaceIdentifiersKeepNames) {
using namespace MG_Util::ShaderTranspiler;
String vertexSource = R"(#version 150
attribute vec3 sample;
void main() {
gl_Position = vec4(sample, 1.0);
}
)";
PreprocessShaderSource(ShaderStage::Vertex, vertexSource);
EXPECT_EQ(vertexSource.find("#version 330 core\n"), 0);
EXPECT_NE(vertexSource.find("in vec3 sample;"), String::npos);
ShaderAttrib vertexAttrib{.shaderType = GL_VERTEX_SHADER, .sourceStr = vertexSource};
auto vertexResult = ShaderCompiler::CompileShader(vertexAttrib);
if (!vertexResult) {
FAIL() << "errc: " << vertexResult.error().errc << "\nlog: " << vertexResult.error().log
<< "\nsource:\n" << vertexSource;
}
String fragmentSource = R"(#version 150
uniform sampler2D sample;
varying vec2 texCoord;
void main() {
gl_FragColor = texture2D(sample, texCoord);
}
)";
PreprocessShaderSource(ShaderStage::Fragment, fragmentSource);
EXPECT_EQ(fragmentSource.find("#version 330 core\n"), 0);
EXPECT_NE(fragmentSource.find("uniform sampler2D sample;"), String::npos);
EXPECT_NE(fragmentSource.find("texture(sample, texCoord)"), String::npos);
ShaderAttrib fragmentAttrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = fragmentSource};
auto fragmentResult = ShaderCompiler::CompileShader(fragmentAttrib);
if (!fragmentResult) {
FAIL() << "errc: " << fragmentResult.error().errc << "\nlog: " << fragmentResult.error().log
<< "\nsource:\n" << fragmentSource;
}
}
TEST_F(ProgramUtilTest, PreprocessEsslVersionsRemainVulkanCompatible) {
using namespace MG_Util::ShaderTranspiler;
const auto verifyVersion = [](const char* inputVersion, const char* expectedVersion) {
SCOPED_TRACE(inputVersion);
String source = inputVersion;
source += R"(
precision mediump float;
out vec4 fragColor;
void main() {
fragColor = vec4(1.0);
}
)";
PreprocessShaderSource(ShaderStage::Fragment, source);
EXPECT_EQ(source.find(expectedVersion), 0);
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source};
auto res = ShaderCompiler::CompileShader(attrib);
if (!res) {
FAIL() << "errc: " << res.error().errc << "\nlog: " << res.error().log << "\nsource:\n" << source;
}
};
// Preserve the pre-existing desktop-core route: the current resource table cannot parse ESSL built-ins.
verifyVersion("#version 300 es", "#version 460 core\n");
verifyVersion("#version 310 es", "#version 460 core\n");
}
TEST_F(ProgramUtilTest, PreprocessModernDesktopVersionsRecognizesUtf8Bom) {
using namespace MG_Util::ShaderTranspiler;
const auto verifyVersion = [](const char* inputVersion) {
SCOPED_TRACE(inputVersion);
String source = "\xef\xbb\xbf";
source += inputVersion;
source += R"(
out vec4 fragColor;
void main() {
fragColor = vec4(1.0);
}
)";
PreprocessShaderSource(ShaderStage::Fragment, source);
EXPECT_EQ(source.find("#version 460 core\n"), 0);
EXPECT_EQ(source.find("\xef\xbb\xbf"), String::npos);
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source};
auto res = ShaderCompiler::CompileShader(attrib);
if (!res) {
FAIL() << "errc: " << res.error().errc << "\nlog: " << res.error().log << "\nsource:\n" << source;
}
};
verifyVersion("#version 400 core");
verifyVersion("#version 460 core");
}
TEST_F(ProgramUtilTest, PreprocessUsesRealSpacedVersionDirectiveForInjectedOutput) {
using namespace MG_Util::ShaderTranspiler;
String source = R"(// #version 460 core
/* "#version 400 core" */
#line 7 "#version 460 core"
# version 120
varying vec2 uv;
void main() {
gl_FragColor = vec4(uv, 0.0, 1.0);
}
)";
PreprocessShaderSource(ShaderStage::Fragment, source);
const SizeT versionPos = source.find("#version 330 core\n");
const SizeT outputPos = source.find("out vec4 mg_FragColor;\n");
EXPECT_NE(versionPos, String::npos);
EXPECT_EQ(outputPos, versionPos + std::strlen("#version 330 core\n"));
EXPECT_NE(source.find("// #version 460 core"), String::npos);
EXPECT_EQ(source.find("#line"), String::npos);
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source};
auto res = ShaderCompiler::CompileShader(attrib);
if (!res) {
FAIL() << "errc: " << res.error().errc << "\nlog: " << res.error().log << "\nsource:\n" << source;
}
}
TEST_F(ProgramUtilTest, PreprocessModernSampleQualifierStaysAtVersion460) {
using namespace MG_Util::ShaderTranspiler;
String source = R"(#version 400 core
sample in vec4 interpolatedColor;
out vec4 fragColor;
void main() {
fragColor = interpolatedColor;
}
)";
PreprocessShaderSource(ShaderStage::Fragment, source);
EXPECT_EQ(source.find("#version 460 core\n"), 0);
EXPECT_NE(source.find("sample in vec4 interpolatedColor;"), String::npos);
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source};
auto res = ShaderCompiler::CompileShader(attrib);
if (!res) {
FAIL() << "errc: " << res.error().errc << "\nlog: " << res.error().log << "\nsource:\n" << source;
}
}
TEST_F(ProgramUtilTest, PreprocessGpuShader5SampleQualifierUsesVersion460) {
using namespace MG_Util::ShaderTranspiler;
for (const char* extension : {"GL_ARB_gpu_shader5", "GL_NV_gpu_shader5"}) {
SCOPED_TRACE(extension);
String source = "#version 150\n#extension ";
source += extension;
source += R"( : enable
sample in vec4 interpolatedColor;
out vec4 fragColor;
void main() {
fragColor = interpolatedColor;
}
)";
PreprocessShaderSource(ShaderStage::Fragment, source);
EXPECT_EQ(source.find("#version 460 core\n"), 0);
EXPECT_NE(source.find("sample in vec4 interpolatedColor;"), String::npos);
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source};
auto res = ShaderCompiler::CompileShader(attrib);
if (!res) {
FAIL() << "errc: " << res.error().errc << "\nlog: " << res.error().log << "\nsource:\n" << source;
}
}
}
TEST_F(ProgramUtilTest, PreprocessLegacyFragmentShaderModernizesFragData) {
using namespace MG_Util::ShaderTranspiler;
@@ -164,7 +401,7 @@ void main() {
PreprocessShaderSource(ShaderStage::Fragment, source);
EXPECT_EQ(source.find("#version 460 core\n"), 0);
EXPECT_EQ(source.find("#version 330 core\n"), 0);
EXPECT_NE(source.find("layout(location = 0) out vec4 mg_FragData[8];\n"), String::npos);
EXPECT_NE(source.find("mg_FragData[0] = vec4(1.0);"), String::npos);
EXPECT_NE(source.find("mg_FragData[1].a = 0.5;"), String::npos);
@@ -182,7 +419,7 @@ TEST_F(ProgramUtilTest, PreprocessKeepsDefaultPrecisionStatements) {
// Mirrors the GL CTS helper shaders (e.g. glcPixelStorageModesTests): the old qualifier strip
// turned "precision highp float;" into invalid "precision float;". Precision qualifiers are
// legal (and ignored) in the forced 460 core profile, so they now pass through untouched.
// legal (and ignored) in the normalized desktop core profile, so they now pass through untouched.
String source = R"(#version 330
precision highp float;
precision mediump int;
@@ -212,7 +449,7 @@ TEST_F(ProgramUtilTest, PreprocessKeepsPrecisionInLegacyShaderForGlslang) {
using namespace MG_Util::ShaderTranspiler;
// Legacy ES-style shader: precision statements and qualifier macros are left for glslang
// (its preprocessor expands the #define; the 460 core parse ignores the qualifiers).
// (its preprocessor expands the #define; the normalized 330 core parse ignores the qualifiers).
String source = R"(#define HIGHP_OR_DEFAULT highp
precision HIGHP_OR_DEFAULT float;
precision mediump int;
@@ -319,6 +556,81 @@ TEST_F(ProgramUtilTest, CompileSimpleVertexShader) {
}
}
// Legacy desktop sources are normalized to "#version 330 core", which is stricter than the 460 they
// used to be forced to. A shader declaring 330 while using 420-era syntax without the matching
// #extension line is accepted by real drivers, so CompileShader retries it at 460 instead of failing.
TEST_F(ProgramUtilTest, CompileShaderRetriesAt460WhenLegacyVersionRejects420Syntax) {
using namespace MG_Util::ShaderTranspiler;
String source = R"(#version 330
layout(binding = 0) uniform sampler2D InSampler;
in vec2 texCoord;
out vec4 fragColor;
void main() {
fragColor = texture(InSampler, texCoord);
})";
PreprocessShaderSource(ShaderStage::Fragment, source);
// The normal path still emits 330 - the retry must not become the default.
ASSERT_EQ(source.find("#version 330 core"), 0u);
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source};
auto res = ShaderCompiler::CompileShader(attrib);
if (!res) {
FAIL() << "errc: " << res.error().errc << "\nlog: " << res.error().log;
}
// Same source compiled for the OpenGL environment must take the retry too.
ShaderAttrib glAttrib{
.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source, .flags = ShaderCompileBits::CompileForOpenGL};
auto glRes = ShaderCompiler::CompileShader(glAttrib);
if (!glRes) {
FAIL() << "errc: " << glRes.error().errc << "\nlog: " << glRes.error().log;
}
}
TEST_F(ProgramUtilTest, CompileShaderStillFailsWithOriginalDiagnosticsWhenRetryCannotHelp) {
using namespace MG_Util::ShaderTranspiler;
String source = R"(#version 330
in vec2 texCoord;
out vec4 fragColor;
void main() {
fragColor = thisFunctionDoesNotExist(texCoord);
})";
PreprocessShaderSource(ShaderStage::Fragment, source);
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source};
auto res = ShaderCompiler::CompileShader(attrib);
ASSERT_FALSE(res);
EXPECT_EQ(res.error().errc, -2);
EXPECT_NE(res.error().log.find("thisFunctionDoesNotExist"), String::npos) << res.error().log;
}
TEST_F(ProgramUtilTest, RetargetLegacyVersionDirectiveOnlyTouchesNormalizedDesktopCore) {
using namespace MG_Util::ShaderTranspiler;
String normalized = "#version 330 core\nvoid main() {}\n";
EXPECT_TRUE(RetargetLegacyVersionDirectiveTo460(normalized));
EXPECT_EQ(normalized.find("#version 460 core"), 0u);
// Already modern: nothing to retarget.
String modern = "#version 460 core\nvoid main() {}\n";
EXPECT_FALSE(RetargetLegacyVersionDirectiveTo460(modern));
EXPECT_EQ(modern.find("#version 460 core"), 0u);
// ES and compatibility sources keep what they declared.
String es = "#version 300 es\nvoid main() {}\n";
EXPECT_FALSE(RetargetLegacyVersionDirectiveTo460(es));
EXPECT_EQ(es.find("#version 300 es"), 0u);
String compat = "#version 330 compatibility\nvoid main() {}\n";
EXPECT_FALSE(RetargetLegacyVersionDirectiveTo460(compat));
EXPECT_EQ(compat.find("#version 330 compatibility"), 0u);
// A commented-out directive is not the real one.
String commented = "// #version 330 core\nvoid main() {}\n";
EXPECT_FALSE(RetargetLegacyVersionDirectiveTo460(commented));
EXPECT_EQ(commented.find("#version 460"), String::npos);
}
const char* fs = R"(#version 150
uniform sampler2D InSampler;
@@ -1017,3 +1329,57 @@ void main() {
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(binRes->at(0), optimized));
}
TEST_F(ProgramUtilTest, PreprocessCoercesBlockPackingQualifiersToStd140) {
using namespace MG_Util::ShaderTranspiler;
// glslang rejects `packed`/`shared` outright when generating SPIR-V, and MobileGL's
// UBO layout is always std140 anyway; the preprocessor rewrites the qualifiers so the
// validation compile, reflection, and generated SPIR-V all agree on std140 (GL CTS
// KHR-GL33.shaders.uniform_block.*.packed/shared).
String source = R"(#version 330
layout(packed) uniform PackedBlock { vec4 pv; };
layout(shared, row_major) uniform SharedBlock { mat4 sm; };
layout ( shared ) uniform SpacedBlock { float sx; };
layout(std140) uniform KeptBlock { float kx; };
// A non-layout use of the identifier stays untouched (compute storage qualifier).
void main() {
gl_Position = pv + vec4(sm[0][0]) + vec4(sx) + vec4(kx);
})";
PreprocessShaderSource(ShaderStage::Vertex, source);
EXPECT_EQ(source.find("packed"), String::npos);
EXPECT_EQ(source.find("layout(shared"), String::npos);
EXPECT_NE(source.find("layout(std140) uniform PackedBlock"), String::npos);
EXPECT_NE(source.find("layout(std140, row_major) uniform SharedBlock"), String::npos);
EXPECT_NE(source.find("layout ( std140 ) uniform SpacedBlock"), String::npos);
EXPECT_NE(source.find("layout(std140) uniform KeptBlock"), String::npos);
ShaderAttrib attrib{.shaderType = GL_VERTEX_SHADER,
.sourceStr = source,
.flags = ShaderCompileBits::CompileForOpenGL};
auto res = ShaderCompiler::CompileShader(attrib);
if (!res) {
FAIL() << "errc: " << res.error().errc << "\nlog: " << res.error().log << "\nsource:\n" << source;
}
}
TEST_F(ProgramUtilTest, PreprocessLeavesComputeSharedStorageQualifierAlone) {
using namespace MG_Util::ShaderTranspiler;
// `shared` is only a packing qualifier inside layout(...); the compute-shader storage
// qualifier of the same spelling must survive.
String source = R"(#version 430
layout(local_size_x = 8) in;
shared float sharedScratch[8];
layout(shared) uniform Blk { float bx; };
void main() {
sharedScratch[gl_LocalInvocationIndex] = bx;
})";
PreprocessShaderSource(ShaderStage::Compute, source);
EXPECT_NE(source.find("shared float sharedScratch[8];"), String::npos);
EXPECT_NE(source.find("layout(std140) uniform Blk"), String::npos);
}
+1 -1
View File
@@ -17,4 +17,4 @@ target_link_libraries(
)
include(GoogleTest)
gtest_discover_tests(QueryTest DISCOVERY_TIMEOUT 30)
gtest_discover_tests(QueryTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
+120
View File
@@ -19,6 +19,7 @@
#include <MG_Backend/BackendObjects.h>
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
#include <MG_Impl/GLImpl/RenderState/GL_RenderState.h>
#include <MG_Impl/GLImpl/Texture/GL_Texture.h>
#include <MG_Impl/GLImpl/VertexArray/Validators.h>
#include <MG_State/GLState/Core.h>
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
@@ -103,6 +104,79 @@ namespace {
private:
MobileGL::MG_External::GLESCapabilities m_snapshot;
};
struct TextureBindCall {
GLenum target;
GLuint texture;
};
MobileGL::Vector<TextureBindCall>* g_textureBindCalls = nullptr;
GLuint g_nextBackendTextureId = 73;
void RecordTextureBind(GLenum target, GLuint texture) {
if (g_textureBindCalls) {
g_textureBindCalls->push_back({target, texture});
}
}
void GenerateBackendTextures(GLsizei count, GLuint* textures) {
for (GLsizei i = 0; i < count; ++i) {
textures[i] = g_nextBackendTextureId++;
}
}
void DeleteBackendTextures(GLsizei, const GLuint*) {}
GLenum NoBackendError() {
return GL_NO_ERROR;
}
// Isolates the DirectGLES globals touched by the binding-cache regression test. The test
// installs only the native ES entry points needed to construct/bind a backend texture and
// restores the process-wide state even when a gtest assertion unwinds the test body.
struct ScopedDirectGLESTextureBindings {
ScopedDirectGLESTextureBindings():
previousContext(MobileGL::Move(MobileGL::MG_State::pGLContext)),
previousFunctions(MobileGL::MG_Backend::DirectGLES::g_GLESFuncs),
previousActiveUnit(MobileGL::MG_Backend::DirectGLES::TextureImpl::g_activeTextureUnit),
previousCache(MobileGL::MG_Backend::DirectGLES::TextureImpl::g_boundTexturesCache),
previousRegistry(MobileGL::MG_Backend::DirectGLES::TextureImpl::g_backendTextureObjects) {
MobileGL::MG_State::pGLContext = MobileGL::MakeUnique<MobileGL::MG_State::GLState::GLContext>();
MobileGL::MG_Backend::DirectGLES::TextureImpl::g_activeTextureUnit = 0;
MobileGL::MG_Backend::DirectGLES::TextureImpl::g_boundTexturesCache = {};
MobileGL::MG_Backend::DirectGLES::TextureImpl::g_backendTextureObjects = {};
MobileGL::MG_External::GLESFunctionsTable functions{};
functions.glBindTexture = RecordTextureBind;
functions.glDeleteTextures = DeleteBackendTextures;
functions.glGenTextures = GenerateBackendTextures;
functions.glGetError = NoBackendError;
MobileGL::MG_Backend::DirectGLES::SetGLESFuncsTable(functions);
g_textureBindCalls = &bindCalls;
}
~ScopedDirectGLESTextureBindings() {
g_textureBindCalls = nullptr;
MobileGL::MG_Backend::DirectGLES::TextureImpl::g_boundTexturesCache = {};
MobileGL::MG_Backend::DirectGLES::TextureImpl::g_backendTextureObjects = previousRegistry;
MobileGL::MG_Backend::DirectGLES::SetGLESFuncsTable(previousFunctions);
MobileGL::MG_Backend::DirectGLES::TextureImpl::g_activeTextureUnit = previousActiveUnit;
MobileGL::MG_Backend::DirectGLES::TextureImpl::g_boundTexturesCache = previousCache;
MobileGL::MG_State::pGLContext = MobileGL::Move(previousContext);
}
ScopedDirectGLESTextureBindings(const ScopedDirectGLESTextureBindings&) = delete;
ScopedDirectGLESTextureBindings& operator=(const ScopedDirectGLESTextureBindings&) = delete;
MobileGL::Vector<TextureBindCall> bindCalls;
private:
MobileGL::UniquePtr<MobileGL::MG_State::GLState::GLContext> previousContext;
MobileGL::MG_External::GLESFunctionsTable previousFunctions;
MobileGL::Uint previousActiveUnit;
decltype(MobileGL::MG_Backend::DirectGLES::TextureImpl::g_boundTexturesCache) previousCache;
decltype(MobileGL::MG_Backend::DirectGLES::TextureImpl::g_backendTextureObjects) previousRegistry;
};
} // namespace
TEST(Sanity, BasicAssertions) {
@@ -148,6 +222,52 @@ TEST(DirectGLESSanity, AdvertisesVoxyRequiredRenderingExtensionsWithoutRaisingGL
extensions.end());
}
TEST(DirectGLESSanity, BindingZeroClearsPreviousNativeTextureBinding) {
using namespace MobileGL;
namespace DirectGLES = MG_Backend::DirectGLES;
ScopedDirectGLESTextureBindings state;
GLuint frontendTexture = 0;
MG_Impl::GLImpl::GenTextures(1, &frontendTexture);
ASSERT_NE(frontendTexture, 0u);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, frontendTexture);
const auto& frontendTextureObject = MG_State::pGLContext->GetTextureUnitObject(0)
.GetBindingSlot(TextureTarget::Texture2D)
.GetBoundObject();
ASSERT_NE(frontendTextureObject, nullptr);
ASSERT_EQ(frontendTextureObject->GetExternalIndex(), frontendTexture);
auto& backendTexture = DirectGLES::TextureImpl::g_backendTextureObjects.GetOrCreate(frontendTextureObject);
backendTexture = MakeShared<DirectGLES::TextureImpl::BackendTextureObject>();
const GLuint backendTextureId = backendTexture->GetBackendTextureId();
DirectGLES::BindCurrentTextures();
ASSERT_EQ(state.bindCalls.size(), 1u);
EXPECT_EQ(state.bindCalls[0].target, GL_TEXTURE_2D);
EXPECT_EQ(state.bindCalls[0].texture, backendTextureId);
// The default 1D slot maps to the same native ES target as 2D. It must not clear and force a
// redundant rebind while the real 2D frontend object remains current.
DirectGLES::BindCurrentTextures();
EXPECT_EQ(state.bindCalls.size(), 1u);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0);
ASSERT_TRUE(MG_State::GLState::IsUndefinedDefaultTexture(
MG_State::pGLContext->GetTextureUnitObject(0)
.GetBindingSlot(TextureTarget::Texture2D)
.GetBoundObject()
.get()));
DirectGLES::BindCurrentTextures();
ASSERT_EQ(state.bindCalls.size(), 2u);
EXPECT_EQ(state.bindCalls[1].target, GL_TEXTURE_2D);
EXPECT_EQ(state.bindCalls[1].texture, 0u);
EXPECT_EQ(DirectGLES::TextureImpl::g_boundTexturesCache[0][static_cast<SizeT>(TextureTarget::Texture2D)],
nullptr);
}
TEST(DirectGLESSanity, ProvidesNamedFramebufferBlitForDirectStateAccess) {
MobileGL::MG_Backend::DirectGLES::BackendObject_DirectGLES backend;
const auto& funcs = backend.GetBackendFunctions().GL;
+2 -2
View File
@@ -17,7 +17,7 @@ target_link_libraries(
)
include(GoogleTest)
gtest_discover_tests(TextureTest DISCOVERY_TIMEOUT 30)
gtest_discover_tests(TextureTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
add_executable(
VkClearManagerTest
@@ -35,4 +35,4 @@ target_link_libraries(
${LINK_LIBRARIES}
)
gtest_discover_tests(VkClearManagerTest DISCOVERY_TIMEOUT 30)
gtest_discover_tests(VkClearManagerTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -17,4 +17,4 @@ target_link_libraries(
)
include(GoogleTest)
gtest_discover_tests(VertexArrayTest DISCOVERY_TIMEOUT 30)
gtest_discover_tests(VertexArrayTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
@@ -8,6 +8,8 @@
#include <gtest/gtest.h>
#include <limits>
#include "Includes.h"
#include "Init.h"
@@ -35,9 +37,31 @@ protected:
return vbo;
}
void SetUp() override { MobileGL::Initialize(); }
// GL error flags are sticky per error code and the context outlives an individual test in this
// binary, so drain whatever an earlier test left pending - otherwise an error-code assertion
// here reads someone else's error. Bounded: one flag per code, so this cannot hang the suite.
static void DrainPendingGlErrors() {
for (Int drained = 0; drained < 16 && MG_Impl::GLImpl::GetError() != GL_NO_ERROR; ++drained) {
}
}
void TearDown() override {}
// The call under test must raise exactly the expected error and nothing more: a second pending
// error means one entry point queued several, which GetError() would hand out at an unrelated
// call site later on.
static void ExpectSingleGlError(GLenum expected) {
EXPECT_EQ(MG_Impl::GLImpl::GetError(), expected);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "the call recorded more than one error";
}
void SetUp() override {
MobileGL::Initialize();
DrainPendingGlErrors();
}
void TearDown() override {
// Attribute a leaked error to the test that caused it instead of to whoever runs next.
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "test left an unconsumed GL error behind";
}
};
TEST_F(VertexArrayTest, GenerateAndBindVAO) {
@@ -57,6 +81,46 @@ TEST_F(VertexArrayTest, GenerateAndBindVAO) {
// Do not detect if it supports default VAO
}
// GL 3.3 core 2.10 name lifecycle - the same three rules the other object families assert:
// deleting an unknown name is silent, a released reservation is recycled, and binding a dead
// name is INVALID_OPERATION.
TEST_F(VertexArrayTest, DeleteOfUnknownOrAlreadyDeletedVertexArrayNameIsSilent) {
GLuint vao = 0;
MG_Impl::GLImpl::GenVertexArrays(1, &vao);
ASSERT_NE(vao, 0u);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::DeleteVertexArrays(1, &vao);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::DeleteVertexArrays(1, &vao);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// Not a small literal: other tests in this binary share the context and generate names in
// bulk, so a low number may well be a legitimately reserved name here.
const GLuint unknownNames[] = {0u, std::numeric_limits<GLuint>::max()};
MG_Impl::GLImpl::DeleteVertexArrays(2, unknownNames);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(VertexArrayTest, DeleteGeneratedButUnboundVertexArrayNameReleasesReservationAndBindFails) {
GLuint vao = 0;
MG_Impl::GLImpl::GenVertexArrays(1, &vao);
ASSERT_NE(vao, 0u);
ASSERT_TRUE(MG_State::pGLContext->ValidateVertexArrayName(vao));
MG_Impl::GLImpl::DeleteVertexArrays(1, &vao);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
EXPECT_FALSE(MG_State::pGLContext->ValidateVertexArrayName(vao));
MG_Impl::GLImpl::BindVertexArray(vao);
ExpectSingleGlError(GL_INVALID_OPERATION);
GLuint recycled = 0;
MG_Impl::GLImpl::GenVertexArrays(1, &recycled);
EXPECT_EQ(recycled, vao);
}
TEST_F(VertexArrayTest, VertexAttributeSetup) {
Vector<Uint> vaoNames;
MobileGL::MG_State::pGLContext->GenVertexArrayNames(1, vaoNames);
@@ -264,7 +328,9 @@ TEST_F(VertexArrayTest, VertexBindingIndexIsBoundedByTheAdvertisedAttribLimit) {
const GLuint outOfRange = MG_Impl::GLImpl::VertexArrayImpl::GetMaxVertexAttribs();
MG_Impl::GLImpl::VertexAttribBinding(0, outOfRange);
EXPECT_TRUE(MG_State::pGLContext->HasGLError());
// Asserting the exact code (rather than just "some error") also consumes it, so the next test
// does not inherit it - GL error flags are sticky and this context is shared.
ExpectSingleGlError(GL_INVALID_VALUE);
}
// The default attribute -> binding-point mapping is the identity. It used to be a 16-element literal
@@ -1107,9 +1173,17 @@ TEST_F(GeneralVertexArrayTest, CurrentAttrib_PackedValidation) {
GetVertexAttribfv(1, GL_CURRENT_VERTEX_ATTRIB, out);
EXPECT_FLOAT_EQ(out[0], 42.0f); // unchanged by the failed call
// Attribute 0 is rejected by MobileGL policy (GL_INVALID_OPERATION).
// GL 3.3 core 2.7: attribute 0's current value is writable like any other generic
// attribute - no error. (An earlier MobileGL policy rejected index 0 with
// GL_INVALID_OPERATION, which broke GL CTS's per-case state reset: gluStateReset writes
// vertexAttrib4f(0, 0,0,0,1) for every attribute after every case.)
VertexAttribP4ui(0, GL_UNSIGNED_INT_2_10_10_10_REV, GL_FALSE, 1u);
EXPECT_EQ(GetError(), GL_INVALID_OPERATION);
EXPECT_EQ(GetError(), GL_NO_ERROR);
GetVertexAttribfv(0, GL_CURRENT_VERTEX_ATTRIB, out);
EXPECT_EQ(GetError(), GL_NO_ERROR);
EXPECT_FLOAT_EQ(out[0], 1.0f); // x field of the packed word
VertexAttrib4f(0, 0.0f, 0.0f, 0.0f, 1.0f); // restore the initial current value
EXPECT_EQ(GetError(), GL_NO_ERROR);
// Out-of-range index -> GL_INVALID_VALUE.
VertexAttribP4ui(VertexArrayImpl::GetMaxVertexAttribs(), GL_UNSIGNED_INT_2_10_10_10_REV, GL_FALSE, 1u);
@@ -799,6 +799,9 @@ namespace MobileGL::MG_Util::BackendLoader {
if (std::strcmp(extension, "GL_EXT_texture_norm16") == 0) {
caps.SupportsNorm16Texture = true;
}
if (std::strcmp(extension, "GL_EXT_texture_filter_anisotropic") == 0) {
caps.SupportsTextureFilterAnisotropy = true;
}
if (std::strcmp(extension, "GL_EXT_base_instance") == 0) {
caps.SupportsBaseInstance = true;
}
@@ -908,6 +911,13 @@ namespace MobileGL::MG_Util::BackendLoader {
glesFuncs.glGetIntegerv(GL_MAX_VIEWPORTS, &maxViewports);
glesFuncs.glGetIntegerv(GL_MAX_VIEWPORT_DIMS, maxViewportDims);
glesFuncs.glGetIntegerv(GL_VIEWPORT_SUBPIXEL_BITS, &viewportSubpixelBits);
// Only legal to query once the extension has been seen in the loop above, hence not batched
// with the unconditional probes: on a driver without it this raises GL_INVALID_ENUM.
if (caps.SupportsTextureFilterAnisotropy) {
GLfloat maxTextureMaxAnisotropy = 1.0f;
glesFuncs.glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &maxTextureMaxAnisotropy);
caps.MaxTextureMaxAnisotropy = std::max(maxTextureMaxAnisotropy, 1.0f);
}
caps.AliasedLineWidthRangeMin = aliasedLineWidthRange[0];
caps.AliasedLineWidthRangeMax = aliasedLineWidthRange[1];
caps.SmoothLineWidthRangeMin = smoothLineWidthRange[0];
@@ -1031,6 +1031,12 @@ namespace MobileGL {
String GLESShadingLanguageVersionString;
Bool SupportsPersistentMapping = false;
Bool SupportsNorm16Texture = false;
// GL_EXT_texture_filter_anisotropic is present, so sampler/texture
// anisotropy may be forwarded without raising GL_INVALID_ENUM in GLES.
Bool SupportsTextureFilterAnisotropy = false;
// GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT of the host driver; only queried when the
// extension above is present, and left at 1.0 (no anisotropy) otherwise.
Float MaxTextureMaxAnisotropy = 1.0f;
Bool SupportsBaseInstance = false;
// GL_EXT_disjoint_timer_query is present in the extension string.
Bool SupportsDisjointTimerQuery = false;
@@ -124,6 +124,7 @@ namespace MobileGL::MG_Util::BackendLoader {
caps.UniformBufferOffsetAlignment = static_cast<int>(p.limits.minUniformBufferOffsetAlignment);
caps.AliasedLineWidthRangeMin = p.limits.lineWidthRange[0];
caps.AliasedLineWidthRangeMax = p.limits.lineWidthRange[1];
caps.MaxSamplerAnisotropy = p.limits.maxSamplerAnisotropy;
caps.SmoothLineWidthRangeMin = p.limits.lineWidthRange[0];
caps.SmoothLineWidthRangeMax = p.limits.lineWidthRange[1];
caps.SmoothLineWidthGranularity = p.limits.lineWidthGranularity;
@@ -208,6 +209,7 @@ namespace MobileGL::MG_Util::BackendLoader {
caps.UniformBufferOffsetAlignment = static_cast<int>(properties.limits.minUniformBufferOffsetAlignment);
caps.AliasedLineWidthRangeMin = properties.limits.lineWidthRange[0];
caps.AliasedLineWidthRangeMax = properties.limits.lineWidthRange[1];
caps.MaxSamplerAnisotropy = properties.limits.maxSamplerAnisotropy;
caps.SmoothLineWidthRangeMin = properties.limits.lineWidthRange[0];
caps.SmoothLineWidthRangeMax = properties.limits.lineWidthRange[1];
caps.SmoothLineWidthGranularity = properties.limits.lineWidthGranularity;
@@ -18,6 +18,9 @@ namespace MobileGL {
Int UniformBufferOffsetAlignment = 256;
Float AliasedLineWidthRangeMin = 1.0f;
Float AliasedLineWidthRangeMax = 1.0f;
// VkPhysicalDeviceLimits::maxSamplerAnisotropy. Whether it can be used at all depends on
// the samplerAnisotropy feature, which the renderer decides at device creation.
Float MaxSamplerAnisotropy = 1.0f;
Float SmoothLineWidthRangeMin = 1.0f;
Float SmoothLineWidthRangeMax = 1.0f;
Float SmoothLineWidthGranularity = 1.0f;
@@ -58,9 +58,17 @@ namespace MobileGL {
TextureInputFormat ConvertGLEnumToTextureInputFormat(GLenum format) {
switch (format) {
// Legacy carve-out: GL_ALPHA stays mapped to Red so alpha-texture uploads keep landing in
// the R channel of the R8-backed storage (TexImage*_State pairs this with a 0,0,0,R
// swizzle). Readback of GL_ALPHA is corrected at the backend, which maps the raw enum to
// source channel 3 (DirectGLES GetReadbackChannelMapping).
case GL_ALPHA:
case GL_RED:
return TextureInputFormat::Red;
case GL_GREEN:
return TextureInputFormat::Green;
case GL_BLUE:
return TextureInputFormat::Blue;
case GL_RG:
return TextureInputFormat::RG;
case GL_RGB:
@@ -73,6 +81,12 @@ namespace MobileGL {
return TextureInputFormat::BGRA;
case GL_RED_INTEGER:
return TextureInputFormat::RInteger;
case GL_GREEN_INTEGER:
return TextureInputFormat::GreenInteger;
case GL_BLUE_INTEGER:
return TextureInputFormat::BlueInteger;
case GL_ALPHA_INTEGER:
return TextureInputFormat::AlphaInteger;
case GL_RG_INTEGER:
return TextureInputFormat::RGInteger;
case GL_RGB_INTEGER:
@@ -117,6 +131,9 @@ namespace MobileGL {
case GL_RGB4:
return TextureInternalFormat::RGB4;
case GL_RGB5:
// GL_RGB565 (GL 4.1 / ARB_ES2_compatibility, used directly by the GL CTS) is the
// ES-facing rendition of the legacy RGB5 resolution.
case GL_RGB565:
return TextureInternalFormat::RGB5;
case GL_RGB8:
return TextureInternalFormat::RGB8;
@@ -294,6 +311,8 @@ namespace MobileGL {
return TexturePixelDataType::UnsignedInt8888;
case GL_UNSIGNED_INT_8_8_8_8_REV:
return TexturePixelDataType::UnsignedInt8888Rev;
case GL_UNSIGNED_INT_10_10_10_2:
return TexturePixelDataType::UnsignedInt1010102;
case GL_UNSIGNED_INT_10F_11F_11F_REV:
return TexturePixelDataType::UnsignedInt101111Rev;
case GL_UNSIGNED_INT_2_10_10_10_REV:
@@ -535,6 +535,7 @@ namespace MobileGL {
CASE(GL_RED_INTEGER)
CASE(GL_GREEN_INTEGER)
CASE(GL_BLUE_INTEGER)
CASE(GL_ALPHA_INTEGER)
CASE(GL_RGB_INTEGER)
CASE(GL_RGBA_INTEGER)
CASE(GL_BGR_INTEGER)
@@ -67,6 +67,18 @@ namespace MobileGL {
return GL_RGBA_INTEGER;
case TextureInputFormat::BGRAInteger:
return GL_BGRA_INTEGER;
case TextureInputFormat::Green:
return GL_GREEN;
case TextureInputFormat::Blue:
return GL_BLUE;
case TextureInputFormat::Alpha:
return GL_ALPHA;
case TextureInputFormat::GreenInteger:
return GL_GREEN_INTEGER;
case TextureInputFormat::BlueInteger:
return GL_BLUE_INTEGER;
case TextureInputFormat::AlphaInteger:
return GL_ALPHA_INTEGER;
case TextureInputFormat::StencilIndex:
return GL_STENCIL_INDEX;
case TextureInputFormat::DepthComponent:
@@ -101,7 +113,10 @@ namespace MobileGL {
case TextureInternalFormat::RGB4:
return GL_RGB4;
case TextureInternalFormat::RGB5:
return GL_RGB5;
// Emit the ES-compatible GL_RGB565 rendition: desktop GL_RGB5 is not a legal
// sized internalformat on OpenGL ES backends, GL_RGB565 is (and GL 4.1+
// accepts it too via ARB_ES2_compatibility).
return GL_RGB565;
case TextureInternalFormat::RGB8:
return GL_RGB8;
case TextureInternalFormat::RGB8Snorm:
@@ -129,6 +129,8 @@ namespace MobileGL {
case TextureInternalFormat::RGBA: {
switch (type) {
case TexturePixelDataType::UnsignedByte:
case TexturePixelDataType::UnsignedInt8888:
case TexturePixelDataType::UnsignedInt8888Rev:
return TextureInternalFormat::RGBA8;
case TexturePixelDataType::UnsignedShort:
return TextureInternalFormat::RGBA16;
@@ -67,6 +67,18 @@ namespace MobileGL {
return "RGBAInteger";
case TextureInputFormat::BGRAInteger:
return "BGRAInteger";
case TextureInputFormat::Green:
return "Green";
case TextureInputFormat::Blue:
return "Blue";
case TextureInputFormat::Alpha:
return "Alpha";
case TextureInputFormat::GreenInteger:
return "GreenInteger";
case TextureInputFormat::BlueInteger:
return "BlueInteger";
case TextureInputFormat::AlphaInteger:
return "AlphaInteger";
case TextureInputFormat::StencilIndex:
return "StencilIndex";
case TextureInputFormat::DepthComponent:
@@ -65,6 +65,16 @@ namespace MobileGL {
return VK_FORMAT_R8G8B8A8_UINT;
case TextureInputFormat::BGRAInteger:
return VK_FORMAT_B8G8R8A8_UINT;
// Single-channel desktop client formats: the client memory holds one component per pixel,
// matching the R8 layouts (the channel it feeds is a pixel-transfer concern, not a layout one).
case TextureInputFormat::Green:
case TextureInputFormat::Blue:
case TextureInputFormat::Alpha:
return VK_FORMAT_R8_UNORM;
case TextureInputFormat::GreenInteger:
case TextureInputFormat::BlueInteger:
case TextureInputFormat::AlphaInteger:
return VK_FORMAT_R8_UINT;
case TextureInputFormat::StencilIndex:
return VK_FORMAT_S8_UINT;
case TextureInputFormat::DepthComponent:
+69
View File
@@ -0,0 +1,69 @@
// MobileGL - MobileGL/MG_Util/Math/HalfFloat.h
// 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
#pragma once
#include <Includes.h>
namespace MobileGL::MG_Util {
inline Float DecodeHalfBitsToFloat(Uint16 half) {
const Uint32 sign = static_cast<Uint32>(half & 0x8000u) << 16;
const Uint32 exponent = (half >> 10) & 0x1Fu;
const Uint32 mantissa = half & 0x3FFu;
Uint32 bits;
if (exponent == 0) {
if (mantissa == 0) {
bits = sign; // signed zero
} else {
// Subnormal half: renormalize into a float exponent.
Uint32 e = 127 - 15 + 1;
Uint32 m = mantissa;
while ((m & 0x400u) == 0) {
m <<= 1;
--e;
}
bits = sign | (e << 23) | ((m & 0x3FFu) << 13);
}
} else if (exponent == 31) {
bits = sign | 0x7F800000u | (mantissa << 13); // Inf / NaN
} else {
bits = sign | ((exponent + 112) << 23) | (mantissa << 13);
}
return std::bit_cast<Float>(bits);
}
inline Uint16 EncodeFloatToHalfBits(Float value) {
const Uint32 bits = std::bit_cast<Uint32>(value);
const auto sign = static_cast<Uint16>((bits >> 16) & 0x8000u);
const Uint32 exponent = (bits >> 23) & 0xFFu;
const Uint32 mantissa = bits & 0x7FFFFFu;
if (exponent == 0xFF) { // Inf / NaN
return static_cast<Uint16>(sign | 0x7C00u | (mantissa != 0 ? 0x200u : 0u));
}
const Int32 halfExponent = static_cast<Int32>(exponent) - 127 + 15;
if (halfExponent >= 31) {
return static_cast<Uint16>(sign | 0x7C00u); // overflow -> Inf
}
if (halfExponent <= 0) {
if (halfExponent < -10) {
return sign; // underflow -> signed zero
}
const Uint32 m = mantissa | 0x800000u;
const Uint32 shift = static_cast<Uint32>(14 - halfExponent);
Uint32 half = m >> shift;
if ((m >> (shift - 1)) & 1u) {
++half; // round to nearest
}
return static_cast<Uint16>(sign | half);
}
Uint32 half = (static_cast<Uint32>(halfExponent) << 10) | (mantissa >> 13);
if (mantissa & 0x1000u) {
++half; // round to nearest; a carry into the exponent is the correct result
}
return static_cast<Uint16>(sign | half);
}
} // namespace MobileGL::MG_Util
+116
View File
@@ -0,0 +1,116 @@
// MobileGL - MobileGL/MG_Util/Math/SmallFloat.h
// 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
#pragma once
#include <Includes.h>
#include <bit>
#include <cmath>
#include <limits>
namespace MobileGL::MG_Util {
// Encodes an unsigned small float with a 5-bit exponent (bias 15) and mantissaBits mantissa
// bits, per the EXT_packed_float conversion rules: negatives (including -Inf) go to zero,
// +Inf stays +Inf, NaN stays NaN, and finite values above the largest representable value
// clamp to it. The mantissa is truncated (rounding mode is implementation-defined).
inline Uint32 EncodeFloatToUnsignedSmallFloat(Float value, Int mantissaBits) {
const Uint32 bits = std::bit_cast<Uint32>(value);
const Bool negative = (bits & 0x80000000u) != 0;
const Uint32 exponent = (bits >> 23) & 0xFFu;
const Uint32 mantissa = bits & 0x7FFFFFu;
const Uint32 exponentMask = 0x1Fu << mantissaBits;
if (exponent == 0xFFu) {
if (mantissa != 0) {
return exponentMask | 1u; // NaN keeps NaN
}
return negative ? 0u : exponentMask; // -Inf -> 0, +Inf -> +Inf
}
if (negative) {
return 0u;
}
const Int32 smallExponent = static_cast<Int32>(exponent) - 127 + 15;
if (smallExponent >= 31) { // above the largest finite value -> clamp to it
return ((31u - 1u) << mantissaBits) | ((1u << mantissaBits) - 1u);
}
if (smallExponent <= 0) { // subnormal range: renormalize, flushing tiny values to zero
const Uint32 fullMantissa = mantissa | 0x800000u;
const Int32 shift = (23 - mantissaBits) + 1 - smallExponent;
return shift > 23 ? 0u : fullMantissa >> shift;
}
return (static_cast<Uint32>(smallExponent) << mantissaBits) |
(mantissa >> (23u - static_cast<Uint32>(mantissaBits)));
}
inline Uint32 EncodeFloatToUnsignedF11(Float value) { return EncodeFloatToUnsignedSmallFloat(value, 6); }
inline Uint32 EncodeFloatToUnsignedF10(Float value) { return EncodeFloatToUnsignedSmallFloat(value, 5); }
// Decodes an unsigned small float (5-bit exponent, bias 15, mantissaBits mantissa bits).
inline Float DecodeUnsignedSmallFloatToFloat(Uint32 field, Int mantissaBits) {
const Uint32 exponent = (field >> mantissaBits) & 0x1Fu;
const Uint32 mantissa = field & ((1u << mantissaBits) - 1u);
const Float mantissaScale = 1.0f / static_cast<Float>(1u << mantissaBits);
if (exponent == 0) {
return std::exp2(-14.0f) * static_cast<Float>(mantissa) * mantissaScale;
}
if (exponent == 31) {
return mantissa == 0 ? std::numeric_limits<Float>::infinity()
: std::numeric_limits<Float>::quiet_NaN();
}
return std::exp2(static_cast<Float>(exponent) - 15.0f) *
(1.0f + static_cast<Float>(mantissa) * mantissaScale);
}
inline Float DecodeUnsignedF11ToFloat(Uint32 field) { return DecodeUnsignedSmallFloatToFloat(field, 6); }
inline Float DecodeUnsignedF10ToFloat(Uint32 field) { return DecodeUnsignedSmallFloatToFloat(field, 5); }
// RGB9E5 shared-exponent encode, following the EXT_texture_shared_exponent spec algorithm
// (N = 9 mantissa bits, B = 15 exponent bias, Emax = 31).
inline Uint32 EncodeSharedExponentRGB9E5(const Float rgb[3]) {
constexpr Int kMantissaBits = 9;
constexpr Int kExponentBias = 15;
constexpr Float kSharedExpMax = 511.0f / 512.0f * 65536.0f; // (2^N-1)/2^N * 2^(Emax-B)
Float clamped[3];
for (Int i = 0; i < 3; ++i) {
const Float v = rgb[i];
clamped[i] = (std::isnan(v) || v < 0.0f) ? 0.0f : std::min(v, kSharedExpMax);
}
const Float maxComponent = std::max(clamped[0], std::max(clamped[1], clamped[2]));
Int sharedExponent = 0; // all-zero input keeps the all-zero word
if (maxComponent > 0.0f) {
sharedExponent = std::max(-kExponentBias - 1, static_cast<Int>(std::floor(std::log2(maxComponent)))) +
1 + kExponentBias;
const Float maxScaled = std::floor(
maxComponent / std::exp2(static_cast<Float>(sharedExponent - kExponentBias - kMantissaBits)) +
0.5f);
if (maxScaled >= 512.0f) { // rounded up to 2^N: bump the shared exponent instead
++sharedExponent;
}
}
const Float scale = std::exp2(static_cast<Float>(sharedExponent - kExponentBias - kMantissaBits));
Uint32 word = static_cast<Uint32>(sharedExponent) << 27;
for (Int i = 0; i < 3; ++i) {
const auto field = static_cast<Uint32>(std::floor(clamped[i] / scale + 0.5f));
word |= std::min(field, 511u) << (i * kMantissaBits);
}
return word;
}
// RGB9E5 shared-exponent decode.
inline void DecodeSharedExponentRGB9E5(Uint32 word, Float outRgb[3]) {
constexpr Int kMantissaBits = 9;
constexpr Int kExponentBias = 15;
const Int exponent = static_cast<Int>(word >> 27) - kExponentBias - kMantissaBits;
const Float scale = std::exp2(static_cast<Float>(exponent));
for (Int i = 0; i < 3; ++i) {
outRgb[i] = static_cast<Float>((word >> (i * kMantissaBits)) & 0x1FFu) * scale;
}
}
} // namespace MobileGL::MG_Util
+13 -3
View File
@@ -15,10 +15,10 @@ namespace MobileGL {
SizeT GetSizedInternalFormatSizeInBytes(TextureInternalFormat internal) {
switch (internal) {
case TextureInternalFormat::R8:
case TextureInternalFormat::Red: // UNorm8 shadow layout
case TextureInternalFormat::R8Snorm:
case TextureInternalFormat::R8I:
case TextureInternalFormat::R8UI:
case TextureInternalFormat::R3G3B2:
return 1;
case TextureInternalFormat::R16:
@@ -27,14 +27,17 @@ namespace MobileGL {
case TextureInternalFormat::R16UI:
case TextureInternalFormat::R16F:
case TextureInternalFormat::RG8:
case TextureInternalFormat::RG: // UNorm8x2 shadow layout
case TextureInternalFormat::RG8Snorm:
case TextureInternalFormat::RG8I:
case TextureInternalFormat::RG8UI:
case TextureInternalFormat::DepthComponent16:
return 2;
case TextureInternalFormat::R3G3B2: // UNorm8x3 shadow layout
case TextureInternalFormat::RGB4:
case TextureInternalFormat::RGB5:
case TextureInternalFormat::RGB: // UNorm8x3 shadow layout
case TextureInternalFormat::RGB8:
case TextureInternalFormat::RGB8Snorm:
case TextureInternalFormat::SRGB8:
@@ -43,11 +46,10 @@ namespace MobileGL {
case TextureInternalFormat::DepthComponent24:
return 3;
case TextureInternalFormat::RGB10:
case TextureInternalFormat::RGB12:
case TextureInternalFormat::RGBA2:
case TextureInternalFormat::RGBA4:
case TextureInternalFormat::RGB5A1:
case TextureInternalFormat::RGBA: // UNorm8x4 shadow layout
case TextureInternalFormat::RGBA8:
case TextureInternalFormat::RGBA8Snorm:
case TextureInternalFormat::RGBA8I:
@@ -72,6 +74,8 @@ namespace MobileGL {
return 4;
case TextureInternalFormat::RGB16:
case TextureInternalFormat::RGB10: // UNorm16x3 shadow layout
case TextureInternalFormat::RGB12: // UNorm16x3 shadow layout
case TextureInternalFormat::RGB16Snorm:
case TextureInternalFormat::RGB16F:
case TextureInternalFormat::RGB16I:
@@ -113,6 +117,12 @@ namespace MobileGL {
switch (format) {
case TextureInputFormat::Red:
case TextureInputFormat::RInteger:
case TextureInputFormat::Green:
case TextureInputFormat::GreenInteger:
case TextureInputFormat::Blue:
case TextureInputFormat::BlueInteger:
case TextureInputFormat::Alpha:
case TextureInputFormat::AlphaInteger:
return 1;
case TextureInputFormat::RG:
case TextureInputFormat::RGInteger:
+5 -3
View File
@@ -548,8 +548,8 @@ namespace MobileGL::MG_Util::SelfTest {
if (summary.capsValid) {
backendApiVersionString = MG_Backend::DirectGLES::FormatBackendAPIVersionString(
summary.caps.GLESRendererString, summary.caps.GLESVersion.Major, summary.caps.GLESVersion.Minor);
advertisedExtensions = JoinAdvertisedExtensions(
MG_Backend::DirectGLES::BuildAdvertisedExtensions(summary.caps.SupportsDisjointTimerQuery));
advertisedExtensions = JoinAdvertisedExtensions(MG_Backend::DirectGLES::BuildAdvertisedExtensions(
summary.caps.SupportsDisjointTimerQuery, summary.caps.SupportsTextureFilterAnisotropy));
}
AppendMobileGLReportedRows(builder, MG_Backend::DirectGLES::GetRendererIdentity(), backendApiVersionString,
advertisedExtensions);
@@ -841,6 +841,7 @@ namespace MobileGL::MG_Util::SelfTest {
String driverVersionString; // raw hex, vendor-encoded (see RunVulkanDriverPost)
Bool shaderSubgroupUsable = false;
Bool timerQueriesSupported = false;
Bool samplerAnisotropySupported = false;
};
} // namespace
@@ -1107,6 +1108,7 @@ namespace MobileGL::MG_Util::SelfTest {
VkPhysicalDeviceFeatures features{};
vkGetPhysicalDeviceFeaturesFn(physicalDevice, &features);
summary.samplerAnisotropySupported = features.samplerAnisotropy == VK_TRUE;
if (features.multiDrawIndirect == VK_TRUE) {
builder.Pass("multiDrawIndirect", "indirect multi-draw batches run as single native commands");
} else {
@@ -1279,7 +1281,7 @@ namespace MobileGL::MG_Util::SelfTest {
backendApiVersionString = MG_Backend::DirectVulkan::FormatBackendAPIVersionString(
summary.deviceName, summary.apiVersionString, summary.driverVersionString);
advertisedExtensions = JoinAdvertisedExtensions(MG_Backend::DirectVulkan::BuildAdvertisedExtensions(
summary.shaderSubgroupUsable, summary.timerQueriesSupported));
summary.shaderSubgroupUsable, summary.timerQueriesSupported, summary.samplerAnisotropySupported));
}
AppendMobileGLReportedRows(builder, MG_Backend::DirectVulkan::GetRendererIdentity(), backendApiVersionString,
advertisedExtensions);
@@ -14,11 +14,14 @@
#include "SpirvPasses/DecomposeWorkgroupVec3Pass.h"
#include "SpirvPasses/LowerDrawParametersPass.h"
#include "SpirvPasses/RebaseInstanceIndexPass.h"
#include "SpirvPasses/StripUboMemberRelaxedPrecisionPass.h"
#include "spirv-tools/libspirv.h"
#include "spirv-tools/optimizer.hpp"
#include "ShaderSourceProcessor.h"
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
#include <MG_Util/Converters/GLToGlslang/ProgramEnumConverter.h>
#include <cstdlib>
namespace MobileGL {
namespace MG_Util {
@@ -132,27 +135,23 @@ namespace MobileGL {
return Resources;
}
Result<SharedPtr<glslang::TShader>> ShaderCompiler::CompileShader(const ShaderAttrib& attrib) {
auto shaderType = attrib.shaderType;
auto& sourceStr = attrib.sourceStr;
auto lang = MG_Util::ConvertGLEnumToEShLanguage(shaderType);
if (lang == EShLanguage::EShLangCount) {
ResultInfo r;
r.log += "Error: [Preprocess] Unsupported shader type: " + ConvertGLEnumToString(shaderType);
r.errc = -1;
return std::unexpected(r);
}
// One parse attempt. A glslang::TShader cannot be re-parsed, so a retry has to build a
// fresh one with byte-identical setup - hence a single factored body rather than two
// copies that could drift apart.
static Result<SharedPtr<glslang::TShader>> ParseShaderSource(EShLanguage lang, GLenum shaderType,
const String& source,
Flags<ShaderCompileBits> flags) {
SharedPtr<glslang::TShader> res;
auto& tshader = res;
tshader = MakeShared<glslang::TShader>(lang);
const char* src[] = {sourceStr.data()};
// setStrings gets no length array, so it relies on NUL termination: source must be an
// owning buffer that outlives parse(), never a StringView's substring.
const char* src[] = {source.c_str()};
tshader->setStrings(src, 1);
tshader->setNanMinMaxClamp(true);
tshader->setInvertY(true);
tshader->setPreamble("#undef VULKAN\n");
if (attrib.flags & ShaderCompileBits::CompileForOpenGL) {
if (flags & ShaderCompileBits::CompileForOpenGL) {
tshader->setEnvInput(glslang::EShSourceGlsl, lang, glslang::EShClientVulkan, 450);
tshader->setEnvClient(glslang::EShClientOpenGL, glslang::EShTargetOpenGL_450);
tshader->setEnvTarget(glslang::EShTargetSpv, glslang::EShTargetSpv_1_3);
@@ -181,6 +180,39 @@ namespace MobileGL {
return res;
}
Result<SharedPtr<glslang::TShader>> ShaderCompiler::CompileShader(const ShaderAttrib& attrib) {
auto shaderType = attrib.shaderType;
auto lang = MG_Util::ConvertGLEnumToEShLanguage(shaderType);
if (lang == EShLanguage::EShLangCount) {
ResultInfo r;
r.log += "Error: [Preprocess] Unsupported shader type: " + ConvertGLEnumToString(shaderType);
r.errc = -1;
return std::unexpected(r);
}
const String source(attrib.sourceStr);
auto result = ParseShaderSource(lang, shaderType, source, attrib.flags);
if (result) return result;
// Legacy desktop sources are normalized to "#version 330 core", which parses under
// stricter rules than the 460 they used to be forced to: a shader declaring 330 while
// using e.g. layout(binding=...) without the matching #extension line compiles on real
// drivers but is rejected here. Retry once at 460 before reporting failure; a genuinely
// broken shader fails both attempts and keeps its original diagnostics.
String retrySource = source;
if (!MG_Util::ShaderTranspiler::RetargetLegacyVersionDirectiveTo460(retrySource)) {
return result;
}
auto retryResult = ParseShaderSource(lang, shaderType, retrySource, attrib.flags);
if (!retryResult) return result;
MGLOG_D("CompileShader: %s only parsed after retargeting its legacy #version to 460",
ConvertGLEnumToString(shaderType).c_str());
return retryResult;
}
Result<SharedPtr<glslang::TProgram>> ShaderCompiler::LinkProgram(const ProgramAttrib& attrib) {
SharedPtr<glslang::TProgram> program = MakeShared<glslang::TProgram>();
for (auto& s : attrib.shaders) {
@@ -266,6 +298,19 @@ namespace MobileGL {
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
}
bool ShaderCompiler::StripUboMemberRelaxedPrecisionForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary) {
using namespace spvtools;
OptimizerOptions options;
options.set_run_validator(false);
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
optimizer.RegisterPass(
StripUboMemberRelaxedPrecisionPass::CreateStripUboMemberRelaxedPrecisionPass());
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
}
bool ShaderCompiler::RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary) {
using namespace spvtools;
@@ -278,6 +323,75 @@ namespace MobileGL {
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
}
bool ShaderCompiler::DecoratePositionInvariantForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary) {
static constexpr Uint32 kHeaderWords = 5;
static constexpr Uint32 kOpDecorate = 71;
static constexpr Uint32 kOpMemberDecorate = 72;
static constexpr Uint32 kDecorationInvariant = 18;
static constexpr Uint32 kDecorationBuiltIn = 11;
static constexpr Uint32 kBuiltInPosition = 0;
if (inputBinary.size() < kHeaderWords) {
return false;
}
// First pass: find targets that already carry Invariant so we never duplicate.
struct MemberKey {
Uint32 id;
Uint32 member;
bool operator==(const MemberKey& o) const { return id == o.id && member == o.member; }
};
Vector<Uint32> invariantIds;
Vector<MemberKey> invariantMembers;
for (SizeT i = kHeaderWords; i < inputBinary.size();) {
const Uint32 word0 = inputBinary[i];
const Uint32 opcode = word0 & 0xFFFFu;
const Uint32 length = word0 >> 16;
if (length == 0 || i + length > inputBinary.size()) {
return false;
}
if (opcode == kOpDecorate && length >= 3 && inputBinary[i + 2] == kDecorationInvariant) {
invariantIds.push_back(inputBinary[i + 1]);
} else if (opcode == kOpMemberDecorate && length >= 4 &&
inputBinary[i + 3] == kDecorationInvariant) {
invariantMembers.push_back({inputBinary[i + 1], inputBinary[i + 2]});
}
i += length;
}
outputBinary.clear();
outputBinary.reserve(inputBinary.size() + 8);
outputBinary.insert(outputBinary.end(), inputBinary.begin(), inputBinary.begin() + kHeaderWords);
for (SizeT i = kHeaderWords; i < inputBinary.size();) {
const Uint32 word0 = inputBinary[i];
const Uint32 opcode = word0 & 0xFFFFu;
const Uint32 length = word0 >> 16;
outputBinary.insert(outputBinary.end(), inputBinary.begin() + i,
inputBinary.begin() + i + length);
if (opcode == kOpDecorate && length == 4 &&
inputBinary[i + 2] == kDecorationBuiltIn && inputBinary[i + 3] == kBuiltInPosition) {
const Uint32 target = inputBinary[i + 1];
if (std::find(invariantIds.begin(), invariantIds.end(), target) == invariantIds.end()) {
outputBinary.push_back((3u << 16) | kOpDecorate);
outputBinary.push_back(target);
outputBinary.push_back(kDecorationInvariant);
}
} else if (opcode == kOpMemberDecorate && length == 5 &&
inputBinary[i + 3] == kDecorationBuiltIn && inputBinary[i + 4] == kBuiltInPosition) {
const MemberKey key{inputBinary[i + 1], inputBinary[i + 2]};
if (std::find(invariantMembers.begin(), invariantMembers.end(), key) ==
invariantMembers.end()) {
outputBinary.push_back((4u << 16) | kOpMemberDecorate);
outputBinary.push_back(key.id);
outputBinary.push_back(key.member);
outputBinary.push_back(kDecorationInvariant);
}
}
i += length;
}
return true;
}
Result<String> ShaderCompiler::DecompileShader(SpvcSession& session) {
spvc_compiler_options options;
session.CreateOptions(&options);
@@ -27,12 +27,25 @@ namespace MobileGL {
// Only for backends without native draw-parameter support (DirectGLES).
static bool LowerDrawParametersForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary);
// Drops RelaxedPrecision member decorations from uniform-block structs so
// SPIRV-Cross prints the same (highp) member precision in every stage; ES
// drivers reject cross-stage uniform blocks whose member precisions differ.
// Only for the DirectGLES transpile path.
static bool StripUboMemberRelaxedPrecisionForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary);
// Rebases loads of the InstanceIndex builtin to (InstanceIndex - BaseInstance) so
// shaders see GL's zero-based gl_InstanceID. Vertex shaders only; DirectVulkan
// backend only (glslang's relaxed mode aliases gl_InstanceID to gl_InstanceIndex,
// which wrongly includes baseInstance).
static bool RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary);
// Adds the Invariant decoration to every Position builtin output. GL apps
// routinely rely on cross-program position invariance for multi-pass
// equality depth tests (e.g. GEQUAL re-draws of the same geometry), and
// mobile drivers that optimize per-pipeline break that without the
// decoration. DirectVulkan only.
static bool DecoratePositionInvariantForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary);
static Result<String> DecompileShader(SpvcSession& session);
};
} // namespace ShaderTranspiler
@@ -19,6 +19,220 @@ namespace {
return (ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || ch == '_';
}
bool IsIdentifierStart(char ch) {
return (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || ch == '_';
}
MobileGL::String MaskCommentsAndQuotedText(const MobileGL::String& source) {
enum class Region { Code, SingleLineComment, MultiLineComment, QuotedText };
MobileGL::String masked = source;
Region region = Region::Code;
char quote = '\0';
bool escaped = false;
for (SizeT pos = 0; pos < source.size(); pos++) {
const char ch = source[pos];
const char next = pos + 1 < source.size() ? source[pos + 1] : '\0';
if (region == Region::Code) {
if (ch == '/' && next == '/') {
masked[pos] = ' ';
masked[pos + 1] = ' ';
pos++;
region = Region::SingleLineComment;
} else if (ch == '/' && next == '*') {
masked[pos] = ' ';
masked[pos + 1] = ' ';
pos++;
region = Region::MultiLineComment;
} else if (ch == '"' || ch == '\'') {
masked[pos] = ' ';
quote = ch;
escaped = false;
region = Region::QuotedText;
}
continue;
}
if (region == Region::SingleLineComment) {
if (ch == '\n' || ch == '\r') {
region = Region::Code;
} else {
masked[pos] = ' ';
}
continue;
}
if (region == Region::MultiLineComment) {
if (ch == '*' && next == '/') {
masked[pos] = ' ';
masked[pos + 1] = ' ';
pos++;
region = Region::Code;
} else if (ch != '\n' && ch != '\r') {
masked[pos] = ' ';
}
continue;
}
if (ch != '\n' && ch != '\r') {
masked[pos] = ' ';
}
if (escaped) {
escaped = false;
} else if (ch == '\\') {
escaped = true;
} else if (ch == quote) {
region = Region::Code;
}
}
return masked;
}
void SkipDirectiveWhitespace(const MobileGL::String& source, SizeT& pos, SizeT lineEnd) {
while (pos < lineEnd && std::isspace(static_cast<unsigned char>(source[pos]))) {
pos++;
}
}
MobileGL::String ReadDirectiveIdentifier(const MobileGL::String& source, SizeT& pos, SizeT lineEnd) {
if (pos >= lineEnd || !IsIdentifierStart(source[pos])) {
return {};
}
const SizeT start = pos++;
while (pos < lineEnd && IsIdentifierChar(source[pos])) {
pos++;
}
return source.substr(start, pos - start);
}
bool HasUtf8Bom(const MobileGL::String& source) {
return source.size() >= 3 && static_cast<unsigned char>(source[0]) == 0xef &&
static_cast<unsigned char>(source[1]) == 0xbb && static_cast<unsigned char>(source[2]) == 0xbf;
}
struct ShaderLanguageInfo {
unsigned version = 110;
MobileGL::ShaderProfile profile = MobileGL::ShaderProfile::Core;
SizeT versionDirectiveStart = MobileGL::String::npos;
SizeT versionDirectiveEnd = MobileGL::String::npos;
bool hasUtf8Bom = false;
bool enablesGpuShader5 = false;
bool HasVersionDirective() const { return versionDirectiveStart != MobileGL::String::npos; }
};
ShaderLanguageInfo InspectShaderLanguage(const MobileGL::String& source) {
const MobileGL::String code = MaskCommentsAndQuotedText(source);
ShaderLanguageInfo info;
info.hasUtf8Bom = HasUtf8Bom(source);
SizeT lineStart = 0;
while (lineStart < code.size()) {
SizeT lineEnd = code.find('\n', lineStart);
const bool hasLineBreak = lineEnd != MobileGL::String::npos;
if (!hasLineBreak) {
lineEnd = code.size();
}
SizeT probe = lineStart;
if (lineStart == 0 && info.hasUtf8Bom) {
probe = 3;
}
SkipDirectiveWhitespace(code, probe, lineEnd);
if (probe < lineEnd && code[probe] == '#') {
const SizeT directiveStart = probe;
probe++;
SkipDirectiveWhitespace(code, probe, lineEnd);
const MobileGL::String directive = ReadDirectiveIdentifier(code, probe, lineEnd);
if (directive == "version" && !info.HasVersionDirective()) {
SkipDirectiveWhitespace(code, probe, lineEnd);
unsigned version = 0;
bool hasVersionDigits = false;
while (probe < lineEnd && code[probe] >= '0' && code[probe] <= '9') {
hasVersionDigits = true;
version = version * 10 + static_cast<unsigned>(code[probe] - '0');
probe++;
}
if (hasVersionDigits) {
info.version = version;
info.versionDirectiveStart = directiveStart;
info.versionDirectiveEnd = lineEnd + (hasLineBreak ? 1 : 0);
SkipDirectiveWhitespace(code, probe, lineEnd);
const MobileGL::String profile = ReadDirectiveIdentifier(code, probe, lineEnd);
if (profile == "es" || profile == "ES") {
info.profile = MobileGL::ShaderProfile::ES;
} else if (profile == "compatibility") {
info.profile = MobileGL::ShaderProfile::Compatibility;
} else {
info.profile = MobileGL::ShaderProfile::Core;
}
}
} else if (directive == "extension") {
SkipDirectiveWhitespace(code, probe, lineEnd);
const MobileGL::String extension = ReadDirectiveIdentifier(code, probe, lineEnd);
SkipDirectiveWhitespace(code, probe, lineEnd);
if (probe < lineEnd && code[probe] == ':') {
probe++;
SkipDirectiveWhitespace(code, probe, lineEnd);
const MobileGL::String behavior = ReadDirectiveIdentifier(code, probe, lineEnd);
const bool isGpuShader5 = extension == "GL_ARB_gpu_shader5" ||
extension == "GL_NV_gpu_shader5";
const bool enablesExtension = behavior == "enable" || behavior == "require" ||
behavior == "warn";
// Gate the whole source if it ever opts into either extension. This is deliberately
// conservative around conditional directives and keeps legal sample qualifiers intact.
info.enablesGpuShader5 = info.enablesGpuShader5 || (isGpuShader5 && enablesExtension);
}
}
}
lineStart = lineEnd + (hasLineBreak ? 1 : 0);
}
return info;
}
MobileGL::String GetNormalizedVersionDirective(const ShaderLanguageInfo& info) {
if (info.profile == MobileGL::ShaderProfile::ES) {
// Preserve the pre-existing behavior for standard lowercase "es" directives. MobileGL's Vulkan
// glslang resource table cannot parse its ESSL built-ins today, even at ESSL 310, whereas the same
// source is accepted through the normalized desktop core path.
return "#version 460 core\n";
}
// Keep compatibility-profile handling on its pre-existing 460 path. Vulkan glslang does not accept that
// profile today, and this legacy-sample fix must not broaden or otherwise alter that separate limitation.
if (info.profile == MobileGL::ShaderProfile::Compatibility) {
return "#version 460 compatibility\n";
}
const bool useLegacyDesktopVersion =
info.version < 400 && !info.enablesGpuShader5;
return useLegacyDesktopVersion ? "#version 330 core\n" : "#version 460 core\n";
}
void NormalizeVersionDirective(MobileGL::String& source, const ShaderLanguageInfo& info) {
const MobileGL::String replacement = GetNormalizedVersionDirective(info);
if (info.HasVersionDirective()) {
source.replace(info.versionDirectiveStart, info.versionDirectiveEnd - info.versionDirectiveStart,
replacement);
if (info.hasUtf8Bom) {
source.erase(0, 3);
}
return;
}
if (info.hasUtf8Bom) {
source.erase(0, 3);
}
source.insert(0, replacement);
}
bool HasSingleLineFunctionDefinition(const MobileGL::String& source, const MobileGL::String& functionName) {
SizeT lineStart = 0;
while (lineStart < source.size()) {
@@ -153,12 +367,8 @@ namespace {
}
SizeT FindAfterVersionDirective(const MobileGL::String& source) {
const SizeT versionPos = source.find("#version");
if (versionPos == MobileGL::String::npos) {
return 0;
}
const SizeT lineEnd = source.find('\n', versionPos);
return lineEnd == MobileGL::String::npos ? source.size() : lineEnd + 1;
const ShaderLanguageInfo info = InspectShaderLanguage(source);
return info.HasVersionDirective() ? info.versionDirectiveEnd : 0;
}
bool IsExtensionAdvertised(MobileGL::GLExtension extension) {
@@ -261,9 +471,68 @@ namespace {
ReplaceIdentifier(source, "GL_ARB_gpu_shader_int64", "MG_DISABLED_GL_ARB_gpu_shader_int64");
}
// Rewrite the `packed` / `shared` block-packing qualifiers inside layout(...) declarations to
// `std140`. Desktop GL leaves the memory layout of such blocks to the implementation and the
// app must query member offsets; MobileGL's SPIR-V pipeline always lays uniform blocks out as
// std140 (glslang under a SPIR-V target rejects `packed`/`shared` outright and SPIRV-Cross has
// no other packing for UBOs), so std140 IS this implementation's chosen layout. Rewriting at
// the source level keeps the validation compile, the reflection the app queries, and the
// generated SPIR-V all agreeing on that choice. Both replacement tokens are 6 characters, so
// the rewrite is done in place.
void CoerceUniformBlockPackingToStd140(MobileGL::String& source) {
constexpr const char* layoutToken = "layout";
constexpr SizeT layoutLen = 6;
SizeT pos = 0;
while ((pos = source.find(layoutToken, pos)) != MobileGL::String::npos) {
const bool hasLeftBoundary = pos == 0 || !IsIdentifierChar(source[pos - 1]);
SizeT probe = pos + layoutLen;
const bool hasRightBoundary = probe >= source.size() || !IsIdentifierChar(source[probe]);
if (!hasLeftBoundary || !hasRightBoundary) {
pos = probe;
continue;
}
while (probe < source.size() && std::isspace(static_cast<unsigned char>(source[probe]))) {
probe++;
}
if (probe >= source.size() || source[probe] != '(') {
pos = probe;
continue;
}
// Scan the qualifier list; layout qualifier values may contain parenthesized
// constant expressions, so track nesting until the matching ')'.
SizeT cursor = probe + 1;
int depth = 1;
while (cursor < source.size() && depth > 0) {
const char ch = source[cursor];
if (ch == '(') {
depth++;
} else if (ch == ')') {
depth--;
} else if (IsIdentifierChar(ch) && (cursor == 0 || !IsIdentifierChar(source[cursor - 1]))) {
SizeT identifierEnd = cursor;
while (identifierEnd < source.size() && IsIdentifierChar(source[identifierEnd])) {
identifierEnd++;
}
const SizeT identifierLen = identifierEnd - cursor;
if (identifierLen == 6 && (source.compare(cursor, 6, "packed") == 0 ||
source.compare(cursor, 6, "shared") == 0)) {
source.replace(cursor, 6, "std140");
}
cursor = identifierEnd;
continue;
}
cursor++;
}
pos = cursor;
}
}
void ModernizeLegacyGLSL(MobileGL::ShaderStage stage, MobileGL::String& source) {
// Precision qualifiers (highp/mediump/lowp and default-precision statements) are legal and
// ignored in the forced "#version 460 core" profile, so glslang handles them natively.
// ignored in the normalized desktop core profiles, so glslang handles them natively.
ReplaceIdentifier(source, "texture2D", "texture");
ReplaceIdentifier(source, "texture2DProj", "textureProj");
@@ -308,6 +577,11 @@ namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
void PreprocessShaderSource(ShaderStage stage, String& source) {
// Normalize while the inspector's source span still refers to the untouched input. Later passes
// remove comments and directives, so any subsequent insertion re-inspects the current source.
const ShaderLanguageInfo originalLanguage = InspectShaderLanguage(source);
NormalizeVersionDirective(source, originalLanguage);
// remove multi-line comment
size_t commentStartPos = source.find("/*");
while (commentStartPos != String::npos) {
@@ -345,44 +619,8 @@ namespace MobileGL {
noperspectivePos = source.find(str_np);
}
// force #version
ShaderProfile profile = ShaderProfile::Core;
SizeT versionPos = source.find("#version");
SizeT lineEnd = source.find('\n', versionPos);
if (versionPos != String::npos) {
String versionLine = source.substr(versionPos, lineEnd - versionPos);
if (versionLine.find("ES") != String::npos)
profile = ShaderProfile::ES;
else if (versionLine.find("compatibility") != String::npos)
profile = ShaderProfile::Compatibility;
else
profile = ShaderProfile::Core;
} else {
profile = ShaderProfile::Core;
source.insert(0, "#version 460 core\n");
versionPos = 0;
lineEnd = source.find('\n', versionPos);
}
SizeT firstLineEnd = lineEnd;
if (profile != ShaderProfile::ES) {
constexpr const char* versionDirectiveCore = "#version 460 core\n";
constexpr const char* versionDirectiveCompat = "#version 460 compatibility\n";
const char* replacement =
(profile == ShaderProfile::Compatibility) ? versionDirectiveCompat : versionDirectiveCore;
if (firstLineEnd != String::npos) {
source.replace(versionPos, firstLineEnd - versionPos + 1, replacement);
} else {
source = replacement;
}
}
FilterUnsupportedGpuShaderInt64(source);
CoerceUniformBlockPackingToStd140(source);
// Some shader packs define helpers with built-in GLSL names such as round(), tanh(), or fma().
// These may pass OpenGL-style validation but fail when recompiled for Vulkan/SPIR-V generation.
@@ -395,6 +633,21 @@ namespace MobileGL {
InjectDepthRangeBuiltinShim(stage, source);
}
Bool RetargetLegacyVersionDirectiveTo460(String& source) {
// Re-inspect rather than searching for the literal directive: it is not necessarily at
// offset 0 (a BOM or comments may precede it) and a commented-out "#version" elsewhere
// must not be mistaken for the real one.
const ShaderLanguageInfo info = InspectShaderLanguage(source);
if (!info.HasVersionDirective()) return false;
// Only the set NormalizeVersionDirective downgraded: desktop core below 400. ES and
// compatibility shaders keep whatever they declared.
if (info.profile != ShaderProfile::Core || info.version >= 400) return false;
source.replace(info.versionDirectiveStart, info.versionDirectiveEnd - info.versionDirectiveStart,
"#version 460 core\n");
return true;
}
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
@@ -20,6 +20,14 @@ namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
void PreprocessShaderSource(ShaderStage stage, String& source);
// Rewrites a "#version 330 core" directive that PreprocessShaderSource normalized down
// from a legacy desktop version back up to "#version 460 core". Returns false (leaving
// the source untouched) for anything else: ES, compatibility, or an already-modern
// declaration. Exists so a shader that only parses under the laxer 460 rules - e.g. it
// uses 420-era syntax without the matching #extension line, which real drivers tend to
// accept - can be retried instead of failing to compile.
Bool RetargetLegacyVersionDirectiveTo460(String& source);
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
@@ -0,0 +1,119 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.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 "StripUboMemberRelaxedPrecisionPass.h"
#include "spirv.hpp"
#include "source/opt/def_use_manager.h"
#include "source/opt/instruction.h"
#include "source/opt/ir_context.h"
#include "source/opt/module.h"
#include <unordered_set>
#include <vector>
namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
namespace {
using spvtools::opt::Instruction;
using spvtools::opt::IRContext;
// Marks `typeId` and every struct type reachable through its members
// (following arrays) for decoration stripping.
void CollectStructTypes(IRContext* context, uint32_t typeId,
std::unordered_set<uint32_t>& structTypeIds) {
Instruction* typeInst = context->get_def_use_mgr()->GetDef(typeId);
if (typeInst == nullptr) return;
switch (typeInst->opcode()) {
case spv::Op::OpTypeStruct: {
if (!structTypeIds.insert(typeId).second) return; // already visited
for (uint32_t member = 0; member < typeInst->NumInOperands(); ++member) {
CollectStructTypes(context, typeInst->GetSingleWordInOperand(member), structTypeIds);
}
break;
}
case spv::Op::OpTypeArray:
case spv::Op::OpTypeRuntimeArray:
CollectStructTypes(context, typeInst->GetSingleWordInOperand(0), structTypeIds);
break;
default:
break;
}
}
} // namespace
spvtools::opt::Pass::Status StripUboMemberRelaxedPrecisionPass::Process() {
auto* irContext = context();
auto* defUseMgr = irContext->get_def_use_mgr();
// Uniform blocks: StorageClass Uniform variables whose pointee struct carries
// the Block decoration (BufferBlock/StorageBuffer SSBOs are left alone - they
// are not stage-matched by member precision in this pipeline's ESSL output).
std::unordered_set<uint32_t> blockStructIds;
for (Instruction& annotation : irContext->module()->annotations()) {
if (annotation.opcode() != spv::Op::OpDecorate) continue;
if (static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(1)) != spv::Decoration::Block) {
continue;
}
blockStructIds.insert(annotation.GetSingleWordInOperand(0));
}
if (blockStructIds.empty()) return Status::SuccessWithoutChange;
std::unordered_set<uint32_t> structTypeIds;
for (Instruction& variable : irContext->module()->types_values()) {
if (variable.opcode() != spv::Op::OpVariable) continue;
if (static_cast<spv::StorageClass>(variable.GetSingleWordInOperand(0)) !=
spv::StorageClass::Uniform) {
continue;
}
Instruction* pointerType = defUseMgr->GetDef(variable.type_id());
if (pointerType == nullptr || pointerType->opcode() != spv::Op::OpTypePointer) continue;
uint32_t pointeeId = pointerType->GetSingleWordInOperand(1);
// Instance-arrayed blocks: unwrap the array around the block struct.
Instruction* pointee = defUseMgr->GetDef(pointeeId);
while (pointee != nullptr && (pointee->opcode() == spv::Op::OpTypeArray ||
pointee->opcode() == spv::Op::OpTypeRuntimeArray)) {
pointeeId = pointee->GetSingleWordInOperand(0);
pointee = defUseMgr->GetDef(pointeeId);
}
if (pointee == nullptr || pointee->opcode() != spv::Op::OpTypeStruct) continue;
if (blockStructIds.find(pointeeId) == blockStructIds.end()) continue;
CollectStructTypes(irContext, pointeeId, structTypeIds);
}
if (structTypeIds.empty()) return Status::SuccessWithoutChange;
std::vector<Instruction*> decorationsToRemove;
for (Instruction& annotation : irContext->module()->annotations()) {
if (annotation.opcode() != spv::Op::OpMemberDecorate) continue;
if (static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(2)) !=
spv::Decoration::RelaxedPrecision) {
continue;
}
if (structTypeIds.find(annotation.GetSingleWordInOperand(0)) == structTypeIds.end()) continue;
decorationsToRemove.push_back(&annotation);
}
if (decorationsToRemove.empty()) return Status::SuccessWithoutChange;
for (Instruction* decoration : decorationsToRemove) {
irContext->KillInst(decoration);
}
return Status::SuccessWithChange;
}
spvtools::Optimizer::PassToken
StripUboMemberRelaxedPrecisionPass::CreateStripUboMemberRelaxedPrecisionPass() {
return spvtools::Optimizer::PassToken(MakeUnique<StripUboMemberRelaxedPrecisionPass>());
}
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
@@ -0,0 +1,44 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.h
// 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
#pragma once
#include "source/opt/pass.h"
#include "spirv-tools/optimizer.hpp"
#include <Includes.h>
namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
// Removes RelaxedPrecision member decorations from every struct type reachable
// from a uniform-block variable (the block struct itself and any structs nested
// in it through members or arrays).
//
// Rationale: ESSL requires matched uniform blocks to declare members with
// identical precision in every stage, but SPIRV-Cross prints a member's
// qualifier relative to the stage's DEFAULT precision (highp in the vertex
// stage, mediump in the fragment stage). A RelaxedPrecision member therefore
// comes out as an explicit "mediump" in the vertex shader but UNQUALIFIED in
// the fragment shader - and once ForceSupporterOutput swaps the fragment
// header to "precision highp float;", that unqualified member reads back as
// highp and the ES driver refuses to link ("definitions of uniform block ...
// do not match", GL CTS KHR-GL33.shaders.uniform_block struct sub-groups).
// Dropping the hint promotes the member to highp in BOTH stages, which is
// always conformant and matches the std140 data layout either way. Only meant
// for the DirectGLES transpile path - block member precision is a per-member
// hint with no layout effect, and no other emission behavior changes.
class StripUboMemberRelaxedPrecisionPass : public spvtools::opt::Pass {
public:
const char* name() const override { return "strip-ubo-member-relaxed-precision"; }
Status Process() override;
static spvtools::Optimizer::PassToken CreateStripUboMemberRelaxedPrecisionPass();
};
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL

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