Compare commits

..
Author SHA1 Message Date
swung0x48 4322427e78 [Fix] (DirectGLES): lower gl_ClipDistance for Adreno's ESSL compiler - shadow the builtin in a Private array with constant-index flushes before EmitVertex/return, loop-copy gl_in clip distances through dynamic indices (whole-array reads segfault the Qualcomm compiler, constant-index element reads miscompile), strip the SPIRV-Cross redeclaration Adreno rejects, and split const struct-array LUT initializers so they stay dynamically indexable; quirk-gated to Qualcomm with MOBILEGL_QUIRK_CLIP_DISTANCE override 2026-07-26 19:56:45 -04:00
swung0x48 203d4bce5e [Fix] (DirectGLES): piglit fixes batch 1 - keep enabled-but-unsourceable vertex attribs disabled on the backend VAO (Adreno memcpy-from-NULL SIGSEGV on gl-3.1-vao-broken-attrib), content-sync READ-framebuffer texture attachments before blits, clamp out-of-bounds access-chain indices via GraphicsRobustAccessPass before ESSL transpile (Adreno poisons whole-shader output on constant OOB), and fold ConstOffset into the coordinate for 1D texelFetch (SPIRV-Cross emulates 1D as 2D but leaves the scalar offset, which ESSL rejects) 2026-07-26 19:17:25 -04:00
swung0x48 761114d022 Merge remote-tracking branch 'origin/cts-gl33' into dev 2026-07-26 18:29:02 -04:00
swung0x48 9caf34d5b1 [Fix] (Logging): rank WARN/ERROR above INFO so release builds keep them - the old ordering (DEBUG=0, WARN=1, ERROR=2, INFO=3) compiled every MGLOG_W/MGLOG_E out of the default MOBILEGL_LOG_LEVEL_INFO build, silently hiding backend shader-compile failures, unsupported-path skips, and enum-conversion fallbacks during the piglit runs 2026-07-26 18:14:56 -04:00
swung0x48 5e676b338b [Fix] (DirectVulkan): back legacy low-bit formats (RGB565/RGB5A1/RGBA4/R3G3B2/RGB4/RGBA2/RGB10/12) with their UNorm8/16 canonical shadow layouts and add capability fallbacks - they mapped to VK_FORMAT_UNDEFINED and crashed or wedged the GPU on upload; also admit 2DMSArray/CubeMap/3D color attachment targets in the render pass 2026-07-26 13:28:30 -04:00
swung0x48 db01bfa3e8 [Fix] (DirectVulkan): general (format,type) readback conversion - hoist the CTS-verified StoreWideRowsToClient into shared ReadbackImpl and decode any color VkFormat to wide RGBA rows; readback previously supported only RGB/BGR/RGBA/BGRA x UNSIGNED_BYTE/FLOAT and silently returned zeros for everything else 2026-07-26 13:28:30 -04:00
swung0x48 cc3dcfd80e [Fix] (DirectVulkan): support UBO instance arrays as arrayed descriptors - uniform Block{...}b[N] reflected as one binding with descriptorCount=N, per-element GL block mapping, per-element buffer infos and dynamic offsets; non-UBO descriptor arrays now fail program creation cleanly instead of continuing corrupt 2026-07-26 12:25:24 -04:00
swung0x48 c8632dfefe [Test] (piglit-android): add on-device piglit harness for MobileGL - patched waffle (WAFFLE_EGL_LIBRARY/WAFFLE_GL_LIBRARY overrides so waffle drives libMobileGL.so directly, AImageReader-backed windows for DirectVulkan since Android ICDs lack VK_EXT_headless_surface, WAFFLE_FORCE_GL_CONTEXT_VERSION to upgrade piglit's low compat context requests to 3.3 core, meson cross fixes) and patched piglit (Android platform support, EGL support decoupled from the X11-dependent EGL tests, and a dispatch-init fix: the waffle resolvers were never installed because gl_fw is NULL during framework construction, so gl* silently bound to the system driver via the DT_NEEDED libEGL's eglGetProcAddress), plus the adb chunked runner with PIGLIT-result parsing, a results comparator, cross-file examples, and the piglit-on-android skill 2026-07-26 11:57:11 -04:00
swung0x48 d9556ff041 [Fix] (DirectVulkan): implement color renderbuffer attachments - render pass/pipeline/blit/copy/readback/clear paths treated color renderbuffers as absent (writes masked to VK_ATTACHMENT_UNUSED, glClear dropped, readback zeros) 2026-07-26 11:30:54 -04:00
swung0x48 b8a8a660e1 [Refactor] (Lifecycle): own MobileGL's lifecycle from the EGL layer instead of ELF static ctor/dtor - the first EGL/WGL entry point lazily initializes via a thread-safe, re-init-capable EnsureInitialized (AutoInit is gone), the last eglTerminate with no initialized display and nothing current tears the whole library down deterministically inside the EGL lifecycle, and the global singletons move to leak-at-exit heap storage so process exit runs no backend destructors at all (AutoDestroy and the Windows DllMain abandon hook are gone); fixes the exit-time SIGABRT from undefined static-destruction order - the DirectGLES buffer-pool mutex abort on Android clean exits, and the pre-existing macOS QueryTest/ProgramTest 'Subprocess aborted' gtest failures now pass (ctest 411/411) 2026-07-26 10:59:14 -04:00
swung0x48 39f21e52ea [Test] (CTS): isolate the DirectVulkan renderbuffer-FBO readback defect so the rest of KHR-GL33 can be measured 2026-07-26 10:43:48 -04:00
swung0x48 0e933b8f2f [Test] (CTS): run VK-GL-CTS KHR-GL33 against MobileGL on Android via a standalone glcts binary 2026-07-26 08:05:02 -04:00
swung0x48 7ab83861ca [Fix] (DirectVulkan): suspend presentation while the window is zero-area - a minimized window's out-of-date swapchain used to keep Present submitting on a signaled fence and presenting never-acquired images (adversarial review); also drop logging from the process-detach abandon path 2026-07-26 07:59:40 -04:00
swung0x48 eaeba556a3 [Test] (WGL): add manual Windows smoke tests - hand-rolled WGL bootstrap and GLFW-driven variant covering the zero-area helper-window path 2026-07-26 07:21:36 -04:00
swung0x48 72fa1221a5 [Fix] (DirectVulkan): survive zero-area windows at renderer init - skip the eager first acquire when RecreateSwapchain's minimize guard left no swapchain (GLFW's hidden helper window), and let Present bring the swapchain up once the window has real size 2026-07-26 07:16:11 -04:00
swung0x48 c4254c4bbd [Feat] (WGL): add Windows host layer - drop-in opengl32.dll with WGL over EGLImpl, Win32 window backend plumbing for both backends, ANGLE loader path, and leak-at-exit process teardown 2026-07-26 06:46:29 -04:00
swung0x48 199164c2e0 [Perf] (DirectGLES): route the per-upload GL_PIXEL_UNPACK_BUFFER unbinds through the unpack binding cache - the six texture-upload sites re-issued glBindBuffer(UNPACK, 0) on every upload; with the resting-0 shadow they now no-op after the first 2026-07-21 19:54:55 -04:00
swung0x48 e87063e90c [Fix] (DirectGLES): route default-framebuffer binds through the FBO-binding shadow - the raw glBindFramebuffer(0) in BindCurrentFBO/SyncAndBindFramebufferObject left the shadow claiming the previous user FBO, false-skipping its next re-bind and letting scoped guards restore a stale binding (caught by adversarial review); also scrub buffer-binding shadows when VAO client-attribute staging buffers are deleted, include cube-map arrays in the pack-image-params gate, and make the delete-recording test hook assertion-unwind safe 2026-07-21 19:54:54 -04:00
swung0x48 122da27249 [Fix] (DirectGLES): overhaul readback/copy/blit driver-state handling with shadow-backed RAII guards - pixel-PACK/UNPACK PBO binding caches resting at 0 (the old bind-then-query 'restore' left the user PBO bound forever, capturing later client-memory readbacks), a PACK pixel-store shadow replacing per-readback glGetIntegerv syncs, a driver FBO-binding shadow behind all scoped binders (per-instance prev slots, nest-safe), scratch-FBO attachment shadows that detach cross-aspect residue exactly when present (depth CopyTex* attachments used to wedge later GetTexImage color reads and vice versa), scissor guards around emulation blits (app scissor clipped depth copies), stale-driver-error drains before single-shot glGetError consumers (fallbacks silently dropped readbacks / GenerateMipmap raised phantom app errors in production builds), ClearBufferiv missing BindCurrentFBO(Draw), cube-face glBindTexture INVALID_ENUM cache poisoning, per-slice GL_PACK_IMAGE_HEIGHT/SKIP_IMAGES semantics on 3D GetTexImage, backend texture ids deleted on wrapper destruction with cache/scratch-FBO scrubs (ids used to leak for the context lifetime and dangling cache pointers could false-skip binds), and context-death/MakeCurrent invalidation for all new shadows; regression tests drive the shadows against a recording mock GLES table 2026-07-21 19:54:54 -04:00
swung0x48 bc2d698b3e [Fix] (DirectGLES): apply the read buffer when one FBO is bound as both draw and read
- SyncCurrentFBO skips the READ-target pass when the same GL FBO is bound as
  both draw and read (the common GL_FRAMEBUFFER case), but the read buffer
  (glReadBuffer) is only applied inside SyncToBackend's READ path — so the skip
  silently dropped every glReadBuffer change, leaving the backend read buffer
  stuck at COLOR_ATTACHMENT0.
- Extract the read-buffer application into BackendFramebufferObject::
  SyncReadBufferToBackend and invoke it from the skip branch (target == Read)
  as well as from SyncToBackend, so reads always target the right attachment.
- Bind the backend FBO as READ inside the helper before glReadBuffer, since the
  skip path only bound it as DRAW.
- Fixes KHR-GL3x.draw_buffers.draw_buffers_1 (reading COLOR_ATTACHMENT1 while
  the FBO stays GL_FRAMEBUFFER-bound returned attachment 0's value); the render
  was already correct, only the readback resolved the wrong attachment.
2026-07-21 12:18:55 -04:00
swung0x48 3049c4b82b [Fix] (ShaderTranspiler): stop blanking block comments in the source handed to glslang
- BlankBlockComments replaced comment chars with spaces but preserved interior newlines, so a block comment spanning a newline inside a #define truncated the macro body (VALUE became empty)
- glslang has a conformant preprocessor and collapses a block comment to one space across newlines, so the delivered source now keeps comments intact and lets glslang handle them
- Fixes KHR-GL3x.shaders.preprocessor multiline_comment_define / redefine_object_multiline_comment / function_redefinition_3 (6 cases, both devices)
- FilterUnsupportedGpuShaderInt64 relied on the blanking to skip commented-out #extension lines; it now masks comments locally (MaskCommentsAndQuotedText) like the sibling passes, collecting edits and applying them back-to-front
- conditional_inclusion.basic_2 (defined() via macro expansion) stays failing by design: glslang rejects it as UB and working around it would mean re-running preprocessing MobileGL defers to glslang
2026-07-21 05:27:01 -04:00
swung0x48 2b3850b76b [Fix] (DirectGLES): upload RGB565/RGB5_A1 shadow data as packed 16-bit types
- The 8-bit unorm shadow was uploaded as GL_UNSIGNED_BYTE, leaving the 8->5/6-bit requantization to the driver
- That rounding direction is implementation-defined: Adreno rounds to nearest (lossless round trip), Mali floors
- On Mali mid-range texels drifted one 5-bit step down, failing all 20 KHR-GL33.pixelstoragemodes.teximage3d rgb565/rgb5a1 cases (eps 1/32); layers with exact values (0.125/0.25/1.0-ish) passed, matching the observed 0,1,7-valid pattern
- PreparePackedNormUpload repacks shadow rows to GL_UNSIGNED_SHORT_5_6_5 / 5_5_5_1 with round-to-nearest, which exactly recovers the original 5/6-bit values (the shadow expansion is injective), so the driver stores them verbatim
- Idempotent across each region's level loop (glType is shared); RGBA4 exempt since its 8-bit expansion (v*17) is exact under either rounding
- Wired at all four SyncMipmapsToBackend upload regions (append-mipmaps, immutable TexSubImage, mutable full, dirty-level update)
2026-07-21 04:30:46 -04:00
swung0x48 2a0ae743a0 [Fix] (DirectGLES): glFinish before the glGetTexImage temp-FBO readback
- Mali (tile-based) does not resolve a texture's render into memory when it is read back through a different (temp) FBO than the one it was rendered with
- The cross-FBO glReadPixels raced the deferred tile resolve and returned pre-render clear contents
- Distinct render targets read back byte-identical, so KHR-GLxx.glsl_noperspective failed on Mali-G715 (all four programs read as the clear colour)
- glGetTexImage is already a CPU/GPU sync point so the extra drain is negligible; Adreno resolves eagerly and was unaffected
2026-07-21 03:34:33 -04:00
swung0x48 6839219c10 [Fix] (GLImpl): report GL_NO_ERROR from glGetGraphicsResetStatus
- The generic export stub returned (GLenum)1; dEQP reads any non-zero status as a lost device
- It is polled after every case (gl3cTestPackages.cpp:121) and sets QP_TEST_RESULT_DEVICE_LOST
- Under the default --deqp-terminate-on-device-lost=enable that tears the whole CTS run down
- MobileGL tracks no GPU resets, so GL_NO_ERROR ("no reset detected") is the honest, spec-correct answer
- Routed through GLImpl::GetGraphicsResetStatus like every other entry point, no inline body in Definitions.cpp
2026-07-21 02:40:15 -04:00
swung0x48 52ddb440ca [Feat] (SelfTest/DriverPost): add a noperspective correctness check to the GLES POST - render a strong-perspective quad and read the centre texel to verify the varying interpolates screen-linear (not perspective-correct), carried through the native GL_NV_shader_noperspective_interpolation path when present or MobileGL's exact gl_Position.w/gl_FragCoord.w emulation when absent. PASS = native and correct; WARN = emulated and correct (the fallback path shipping packs hit on such devices); FAIL = interpolation wrong/perspective-correct, or the program will not build. The ESSL header matches the device version because noperspective is rejected at #version 300 es on some drivers even with the extension enabled 2026-07-21 02:09:17 -04:00
swung0x48 79feeffd25 [Feat] (ShaderTranspiler, DirectGLES): emulate noperspective on GLES devices lacking GL_NV_shader_noperspective_interpolation - EmulateNoPerspectivePass pre-multiplies each NoPerspective output by gl_Position.w in the vertex stage and recovers each input via gl_FragCoord.w in the fragment stage (exact screen-linear L = P(a*w)*gl_FragCoord.w, handling whole-variable and component/access-chain reads, scalar and vector varyings), forces highp on emulated varyings, and strips what it cannot emulate; replaces the smooth-strip fallback so no NV extension is ever required. Restricts the vertex pre-multiply to the entry function so a non-inlined helper cannot double-scale 2026-07-20 23:48:48 -04:00
swung0x48 202037b5a3 [Fix] (DirectVulkan): shrink the blended depth-write quirk to MIN/MAX extremum blends only - a fixture-wide trace sweep showed the additive ONE+ONE arm never fires on the 26.3 OIT chain (its accumulation passes disable depth writes themselves) and only hit unrelated additive glow content; also fail reflection toward the gl_FragDepth exemption and zero phantom default-FBO blend slots so stale indexed state cannot trigger the strip 2026-07-20 23:39:28 -04:00
swung0x48 bce9c48c8e [Feat] (ShaderTranspiler, DirectGLES): support noperspective conformantly instead of stripping it - let the qualifier reach glslang as the core SPIR-V NoPerspective decoration (native on DirectVulkan; SPIRV-Cross emits ESSL noperspective + GL_NV_shader_noperspective_interpolation on DirectGLES), and for GLES devices lacking that extension add StripNoPerspectivePass to drop the decoration and fall back to smooth; the old naked substring erase discarded the interpolation shader packs need and mangled identifiers containing the word 2026-07-20 22:59:43 -04:00
swung0x48 b6a7807a3a [Fix] (MG_Util/ShaderTranspiler): reject malformed #version directives instead of legalizing them - an unrecognized version number (329/331), a bad profile keyword, a float or trailing token used to be rewritten to "#version 330 core" (or rescued to 460 by the retry); now InspectShaderLanguage marks such directives invalid so NormalizeVersionDirective and RetargetLegacyVersionDirectiveTo460 leave them for glslang to reject, while every valid version still normalizes as before 2026-07-20 22:03:36 -04:00
swung0x48 48ba622387 [Fix] (MG_Util/ShaderTranspiler): keep #line directives instead of deleting them, dropping only the GLSL-illegal quoted filename and the ones that precede #version, so __LINE__ and compiler diagnostics follow the application's own numbering 2026-07-20 21:06:38 -04:00
swung0x48 05260d1262 [Fix] (MG_Util/ShaderTranspiler): blank block comments lexically instead of erasing them - a '//*** banner ***' line opened a comment the old scanner never closed, so it deleted the rest of the shader, and a commented-out builtin definition renamed every genuine call to a name nothing defines 2026-07-20 21:06:37 -04:00
swung0x48 6eb5ff51c5 [Fix] (MG_Impl/GLImpl): reject the RGTC internal formats on 3D texture targets - RGTC compresses 4x4 blocks of a 2D image and has no 3D form, and the check must run on the raw enum because RGTC now resolves to plain R8/RG8 storage 2026-07-20 21:05:57 -04:00
swung0x48 e526f8e8ac [Fix] (MG_Util/Converters): resolve the GL_COMPRESSED_* internal formats to the uncompressed storage that backs them instead of rejecting them as unknown - GL prescribes this base-format fallback for the six generic formats, and RGTC stores uncompressed because ES exposes no compressor 2026-07-20 21:05:57 -04:00
swung0x48 e724e88eec [Fix] (MG_State, MG_Impl/GLImpl): allocating a mipmap level no longer truncates the chain above it - AllocateLevel now only grows and the callers that genuinely redefine the whole level set (glTexStorage*, mip regeneration, multisample storage, level-0 respecification) drop the tail explicitly 2026-07-20 21:02:54 -04:00
swung0x48 3b175fb88a [Fix] (MG_Impl/GLImpl): a multisample sample count above the format's maximum is INVALID_OPERATION, not INVALID_VALUE - matching both the spec and the native Adreno driver 2026-07-20 21:02:53 -04:00
swung0x48 b5a4e7075a [Fix] (MG_Impl/GLImpl): record a GL error from the unimplemented compressed texture entry points instead of throwing - a C++ exception unwinding through the C GL ABI hard-crashes any caller, and glGetCompressedTexImage reported success while writing nothing 2026-07-20 21:02:53 -04:00
swung0x48 520c2b6750 [Fix] (MG_Backend/DirectGLES): sync GL_TEXTURE_SWIZZLE_* on multisample targets - the early return meant to skip the sampler-only parameters dropped every swizzle write, which the frontend already treats as legal on those targets 2026-07-20 21:02:52 -04:00
swung0x48 57cc652b1d [Fix] (MG_Impl/GLImpl): glIsTransformFeedback reports GL_FALSE instead of claiming every name it is handed is a live transform feedback object 2026-07-20 21:02:52 -04:00
swung0x48 65ea54da9e [Refactor] (ShaderTranspiler, DirectVulkan): replace the hand-rolled SPIR-V word walkers with a DecoratePositionInvariantPass and SPIRV-Reflect-based InstanceIndex detection 2026-07-20 08:35:00 -04:00
swung0x48 c81dd04f08 [Test] (CI, trace_replay): force the blended depth-write quirk on the Linux DirectVulkan OIT retrace lane and tighten that case's SSIM threshold to 0.995 2026-07-20 07:39:25 -04:00
swung0x48 c158bfa584 [Fix] (DirectVulkan): narrow the blended depth-write quirk to order-independent accumulation blends, exempting sorted-transparency, gl_FragDepth writers and fully masked attachments 2026-07-20 07:39:25 -04:00
swung0x48 f9f455144c [Refactor] (MG_Config, DirectVulkan): route the blended depth-write quirk through the FeaturesTable as MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE instead of an ad-hoc getenv 2026-07-20 04:37:23 -04:00
swung0x48 64e4840de2 [Fix] (DirectVulkan): fall back to immutable images when the mutable probe fails, gate robustBufferAccess behind MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS, decode R32/RG32/R16-class readback formats, and warn when shaderStorageImage*WithoutFormat is unavailable 2026-07-20 02:36:26 -04:00
swung0x48 bf7b5755cc [Refactor] (ShaderTranspiler, MG_Backend, MG_Util): gate the subgroup prefix-scan rewrite behind a generic device-quirk registry with GPU vendor detection and MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN override, warn on template mismatch, and block ARB/NV subgroup spellings 2026-07-20 02:36:26 -04:00
swung0x48 293f64b3c2 [Fix] (MG_Impl, DirectGLES): correct ARB_clear_texture error codes, reject cube maps in CopyTextureSubImage2D, advertise the extension on Espryt, and pin the error contracts with tests 2026-07-20 02:36:25 -04:00
swung0x48 f0cc07c937 [Perf] (DirectVulkan): bound vertex-stream conversion by the draw's real fetch range, reuse cached prefixes, pin cached source buffers, and stop repacking client arrays for pointer alignment 2026-07-20 02:36:25 -04:00
swung0x48 4658536652 [Perf] (DirectVulkan): keep the render pass alive for steady-state storage-image draws and skip storage-image collection for programs without them 2026-07-20 02:36:24 -04:00
swung0x48 04b4627c65 [Fix] (DirectVulkan): pass depth/stencil formats through sampled-view resolution so D24S8/D32FS8 samplers stop dropping draws, and memoize per-binding view-format resolution 2026-07-20 02:36:24 -04:00
swung0x48 68e13705c8 [Fix] (MG_Backend/DirectVulkan, ShaderTranspiler, MG_Test, TraceReplay): make iterationRP retrace pass on 64-lane Vulkan devices with subgroup-width emulation and format-aware readback 2026-07-20 02:36:23 -04:00
swung0x48 e5388c0e7e [Fix] (MG_Backend, MG_Impl, ShaderTranspiler, MG_Test): support iterationRP custom images and storage format reinterpretation 2026-07-20 02:35:30 -04:00
swung0x48 8bc4808b1a [Test] (TraceReplay): match the iterationRP fixture name published on the mirror 2026-07-19 23:09:29 -04:00
swung0x48 5d8a5387e2 [Test] (TraceReplay): try the hit.moe mirror before miawa and Git LFS 2026-07-19 22:29:55 -04:00
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 5b38f61961 [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 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 8bdab8005b [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 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 273c7ebcf0 [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 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 3a40778c4b [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 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 37a7f35a27 [Chore] (MG_Backend/DirectGLES): demote UBO ring creation log to debug level 2026-07-16 00:10:14 -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 c6ed9429be [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 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 cb6af44984 [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 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 7464179249 [Chore] (MG_Backend/DirectGLES): log global-UBO ring creation 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 315e9cb194 [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 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 e8d9a913d8 [Fix] (MG_Util/SelfTest): preserve POST checks before format probing 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 4453f1910d [Feat] (MG_Backend, android-plugin): show format capability tables in POST 2026-07-16 00:10:11 -04:00
swung0x48 8c89b1618a [Feat] (MG_Backend, android-plugin): show format capability tables in POST 2026-07-16 00:10:11 -04:00
swung0x48 1b0be9a997 [Docs] (android-plugin): specify POST format capability tables 2026-07-15 22:36:56 -04:00
swung0x48 3e4ce5caa7 [Refactor] (MG_Impl/GLImpl): replace Flywheel dispatch sync hack with MOBILEGL_COHERENT_AS_FLUSH 2026-07-15 21:51:20 -04:00
swung0x48 f80f6f4a62 [Fix] (CI): retain fixtures for failed retraces 2026-07-15 04:36:15 -04:00
swung0x48 66caf907fd [Fix] (CI): remove intermediate trace artifacts 2026-07-15 02:52:03 -04:00
swung0x48 d3150399c7 [Fix] (CI): download direct trace APK artifact 2026-07-15 02:01:53 -04:00
swung0x48 f1c6a12c06 [Fix] (CI): use full commit SHAs 2026-07-15 01:33:56 -04:00
swung0x48 947442ec78 [Fix] (CI): resolve trace APK by SHA 2026-07-15 01:27:09 -04:00
swung0x48 165dd003d7 [Chore] (CI): clean intermediate artifacts 2026-07-15 00:53:50 -04:00
swung0x48 b852ced3c1 [Chore] (CI): upgrade GitHub Actions runtimes 2026-07-15 00:36:08 -04:00
swung0x48 4a03d62b91 [Fix] (CI): publish APK artifacts directly 2026-07-15 00:34:52 -04:00
swung0x48 056574eebe [Fix] (CI): name APK artifacts by commit 2026-07-15 00:22:12 -04:00
swung0x48 e61685547a [Fix] (CI): locate unified trace APK 2026-07-15 00:17:08 -04:00
swung0x48 7ebaf43282 [Refactor] (Android plugin): unify renderer APK with Plugin V2 DSL 2026-07-14 23:37:21 -04:00
swung0x48 15580ff6a6 [Fix] (Retrace): preserve bundled ANGLE binaries 2026-07-15 08:46:28 +08:00
swung0x48 fd6f5bca83 [Fix] (Retrace): rely on APK signing for bundled ANGLE 2026-07-15 07:50:40 +08:00
swung0x48 b6d311f20b [Fix] (Retrace): bundle and select signed ANGLE variants 2026-07-15 06:48:10 +08:00
swung0x48 9c0d5517bd [Refactor] (MG_Backend/DirectVulkan): replace null-renderer guards with MOBILEGL_ASSERT
Drops the if (!pVulkanRenderer) { return; } / !MG_State::pGLContext early-return guards across DirectVulkan.cpp in favor of MOBILEGL_ASSERT, matching the pattern already used by the rest of the backend. Legitimate runtime conditions (index bounds, sync/query handle nullness, renderer-generation mismatch, timer-query support) are kept as real checks; only the null-pointer defenses are converted.
2026-07-14 03:55:41 -04:00
swung0x48 3445ab9304 [Refactor] (MG_Backend/DirectVulkan, trace-replay): extract dump-image capture out of MobileGL backend
Moves snapshot capture entirely into the apitrace retrace layer (glReadPixels + PNG encode). Drops the MOBILEGL_PRESENT_DUMP_PATH / MOBILEGL_PRESENT_STATS / MOBILEGL_PRESENT_DUMP_CALL / MOBILEGL_PRESENT_CURRENT_CALL / MOBILEGL_TRACE_CURRENT_CALL_OVERRIDE plumbing from Config, ConfigLoader, VulkanRenderer (GetPresentedDumpPixel/WritePresentedDumpPpm + present-stats readback), the EGL/GLX/Android ws shims, and the Android trace_replay_core PPM reader.

DirectVulkan ReadPixels on the default framebuffer now remaps raw swapchain pixels (top-left origin, preTransform-rotated) to GL orientation (bottom-left origin) so the retrace snapshot matches the golden; SwapchainObject also resizes the default-FBO stencil attachment to the swapchain extent to fix GL_INVALID_FRAMEBUFFER_OPERATION under the glReadPixels completeness check.
2026-07-14 02:40:20 -04:00
swung0x48 533219ede7 [Fix] (Retrace): load ANGLE through LD_LIBRARY_PATH 2026-07-13 21:20:02 -04:00
swung0x48 b1f55026af [Chore] (MobileGL/ConfigLoader): rename/clean up more env var clutter 2026-07-13 20:45:40 -04:00
swung0x48 a26e9aaf25 [Refactor] (MG_Config, MG_Backend, trace-replay): remove unused stats instrumentation 2026-07-13 20:33:31 -04:00
swung0x48 a55a0645e2 [Refactor] (MG_Config, MG_Backend/DirectVulkan, trace-replay): centralize Magma env parsing and rename R11G11B10F fallback 2026-07-13 19:58:06 -04:00
swung0x48 e529e12d27 [Perf] (MG_Backend/DirectVulkan): bind GetBoundObject/GetSamplerObject const-ref returns by reference in BindProgramUniformBuffers resolve paths (UBO/SSBO/texel/sampler override) instead of copying the SharedPtr; ResolveUniformBufferPayload 2.2%->1.6% 2026-07-13 08:27:12 -04:00
swung0x48 e78eee972e [Perf] (MG_Backend/DirectVulkan): store the resolved TextureResource pointer in the per-draw sync memo so repeat SyncTextureAndGetDescriptor calls skip the resource-map lookup 2026-07-13 06:53:37 -04:00
swung0x48 37cd5b42de [Perf] (MG_Backend/DirectVulkan): drop redundant per-draw work in UploadAndBindVertexBuffers - pass programObj from SetupDraw, use the VAO attribute's buffer SharedPtr directly instead of re-resolving by external index; 8.5%->6.2% 2026-07-13 05:21:57 -04:00
swung0x48 7fe5247626 [Fix] (MG_Backend/DirectVulkan): key the sampled-set walk-skip on a program lifetime id, not the recyclable GL name, so a deleted+recreated program can't false-hit the cache 2026-07-13 04:58:05 -04:00
swung0x48 f098983c9f [Perf] (MG_Backend/DirectVulkan): skip the per-draw sampled-texture walk when the bound set is unchanged (texture-bind generation + program state version); CollectSampledTextures 5.0%->0.2%, fps 228->249 2026-07-13 04:41:14 -04:00
swung0x48 516d2a659e [Perf] (MG_Backend/DirectVulkan): cache resolved VkSampler per binding to skip the per-draw sampler key hash, keyed on a new sampler lifetime id 2026-07-13 02:04:57 -04:00
swung0x48 808c5dcc46 [Perf] (MG_Backend/DirectVulkan): re-land content-version texture early-out; bump content version on glGenerateMipmap so cached sampled views re-sync (fixes Iris shader retrace) 2026-07-13 00:50:09 -04:00
swung0x48 ecea8054a6 Revert content-version texture early-out (e8e1521): the SyncTexture cross-draw skip breaks Iris shader retrace validation (correctness regression) 2026-07-12 23:27:54 -04:00
swung0x48 b0076af9bd [Perf] (MG_Backend/DirectVulkan): raw-ptr in ResolveSamplerDescriptor drops the per-draw SharedPtr refcount on the sampler descriptor path 2026-07-12 22:48:05 -04:00
swung0x48 24cf1e3a7f [Perf] (MG_Backend/DirectVulkan): frames-in-flight from MOBILEGL_MAGMA_FRAMESINFLIGHT env (fallback 3), clamped to surface maxImageCount at init 2026-07-12 20:35:05 -04:00
swung0x48 e5ee4cde4f [Perf] (MG_Backend/DirectVulkan): deepen frame pipeline 2->3 to hide GPU-completion latency; cross-frame glClientWaitSync fence stalls -28% 2026-07-12 19:42:35 -04:00
swung0x48 e8e1521972 [Perf] (MG_Backend/DirectVulkan): skip cross-draw re-sync of unchanged textures via a content-version early-out; SyncTextureAndGetDescriptor 8.9%->2.4% 2026-07-12 18:40:48 -04:00
swung0x48 6f53b9a6bb [Perf] (MG_Backend/DirectVulkan): raw-ptr sampled-texture walk skips SharedPtr refcount churn per draw 2026-07-12 10:51:15 -04:00
swung0x48 acaa9f6dc7 [Perf] (MG_Backend/DirectVulkan): zero-copy UBO bind - point descriptor at the app's persistent VkBuffer instead of a per-draw transient copy; fps 127->166 2026-07-12 10:11:49 -04:00
swung0x48 375f2df694 [Perf] (MG_Backend/DirectVulkan): skip per-draw pipeline resolution when pipeline state unchanged; SetupDraw 54%->51%, fps 109->127 2026-07-12 09:28:30 -04:00
swung0x48 542e50be33 [Perf] (MG_Backend/DirectVulkan): skip per-draw render-pass hash when framebuffer state unchanged; SetupDraw 60%->54%, fps 96->109 2026-07-12 08:18:37 -04:00
swung0x48 ad9ee99521 [Perf] (MG_Backend/DirectGLES): dedup per-draw indexed UBO/SSBO binds with a shadow cache; BindCurrentProgramWithResources 5.1% -> 3.1% 2026-07-12 06:32:18 -04:00
swung0x48 25395a9f9a [Docs] (MG_Backend/DirectGLES): TODO for buffer-pool Phase 2 orphan-on-respecify 2026-07-12 05:37:03 -04:00
swung0x48 d7029952bb [Perf] (MG_Backend/DirectGLES): recycle idle GL buffers via a fence-gated size pool instead of glDeleteBuffers; pinned fps 184->220 2026-07-12 05:28:34 -04:00
swung0x48 340449b77e [Perf] (MG_State, MG_Backend/DirectGLES): skip never-touched buffer bind points via high-water mark; SyncNeccessaryBuffers 15.7% -> 4.2% 2026-07-12 04:31:34 -04:00
swung0x48 527e229ac8 [Perf] (MG_Backend/DirectGLES): drop redundant per-draw UBO binding-point sync; BindCurrentProgramWithResources already rebinds them 2026-07-12 04:11:23 -04:00
swung0x48 d5bc753764 [Perf] (MG_Backend/DirectGLES): skip scratch bind + upload for fully-synced mipmap textures 2026-07-12 04:11:22 -04:00
swung0x48 436f7f7e86 [Perf] (DirectGLES): shadow-track unpack state instead of glGetIntegerv
ScopedDefaultUnpackState saved the backend GL unpack state with 6 glGetIntegerv
calls on every construction. glGetIntegerv forces a driver pipeline sync, and
because it ran per dirty texture per frame in the texture upload path, it
dominated the DirectGLES draw path - and stalling the pipeline serialized CPU-GPU
work far beyond its raw CPU cost.

The backend unpack state is set only by MobileGL's own save/restore helpers
(ScopedDefaultUnpackState, TempPixelStoreParameterSync, the R32F copy path), all
of which restore to the resting GL default, so it can be shadow-tracked: read the
previous state from a static shadow (no query), pin the backend to the known
default once up front, and set state with compare-and-set so the paired
glPixelStorei calls also usually no-op.

Device-verified on Adreno 830 (MC 26.3-snapshot3, Espryt, CPU pinned to 1.56/1.96
GHz for a thermally-comparable measurement): rendering correct; fps 105 -> 147
(+40%); render-thread profile: glGetIntegerv ~9% -> below noise, SyncNeccessary-
Textures 25% -> 12%, SyncMipmapsToBackend 23% -> 9%.
2026-07-12 03:11:28 -04:00
swung0x48 009e37ec6f [Perf] (DirectVulkan): reuse descriptor set across draws with identical bindings
BindProgramUniformBuffers rebuilt a fresh descriptor set and called
vkUpdateDescriptorSets on every draw, even when consecutive draws bound the exact
same textures/samplers/buffers (common in MC: many draws share a program + atlas).
Now, after resolving the bindings (still needed for the UBO dynamic offset),
compute a cheap word-wise signature of the resolved descriptor content + layout;
when it matches the previous draw, reuse that descriptor set and skip
AcquireDescriptorSet + vkUpdateDescriptorSets - only the bind-time dynamic offsets
differ.

Correct by construction: bindings are re-resolved every draw so the signature
always reflects current state and reuse only happens on an exact match; the reused
set is never re-acquired within a frame (the acquire cursor only advances); the
descriptor set layout is in the signature so reuse never crosses programs; the
cache resets each frame in BeginFrame when the frame's sets are recycled; sampler
overrides (blits) bypass and invalidate it. The signature hashes 64-bit words (the
Vk*Info payloads are 8-byte-multiple sized and value-initialized) so its own
per-draw cost stays small.

Device-verified on Adreno 830 (MC 26.3-snapshot3, optimized -O2 Magma): rendering
correct, no validation errors. Render-thread wall-clock profile:
BindProgramUniformBuffers 22.85% -> 19.82% (vkUpdateDescriptorSets ~5% dropped below
noise; word-wise signature adds ~0.6% self), SetupDraw 62% -> 60%.
2026-07-12 01:55:31 -04:00
swung0x48andClaude Opus 4.8 b253df881d [Perf] (DirectVulkan): memoize per-draw texture sync in SetupDraw
Each sampled texture was resolved ~3x per draw: SetupDraw's layout-probe
loop, its post-transition loop, and again inside ResolveSamplerDescriptor.
No GL texture mutation happens mid-SetupDraw, and layout is tracked on the
TextureResource independently of SyncTexture, so the repeat SyncTexture work
(mip-completeness / resource+view resync / dirty scan) is pure redundancy.

Add a per-draw memo in VkTextureManager (BeginDrawSyncScope/EndDrawSyncScope
+ RAII DrawSyncScope guard around SetupDraw): after the first successful sync
of a texture in a draw, repeat SyncTextureAndGetDescriptor calls short-circuit
to the already-synced resource.

Device-verified on Adreno 830 (MC 26.3-snapshot3, Magma): rendering correct,
no validation errors; wall-clock profile of the render thread shows
SyncTextureAndGetDescriptor dropping from 15.2% to ~5% and SetupDraw from
43.7% to 28.9%.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 22:06:28 -04:00
swung0x48 c1743aa42d [Refactor] (MG_State, MG_Backend): PipeResource storage layer + zero-copy coherent persistent maps
Introduce a Mesa pipe_resource-style PipeResource that owns a GL buffer's bytes
and its backend GPU resource, abstracting WHERE the authoritative bytes live:
 - Shadow mode (non-persistent buffers): a CPU Vector; the backend keeps its own
   GPU copy in sync via BufferBackendOps, exactly as before.
 - Persistent mode (coherent GL_MAP_PERSISTENT maps): the backend's host-visible,
   COHERENT, persistently-mapped GPU memory is the single source of truth. The app
   writes into it directly, every reader resolves against it, and NO per-write
   backend transfer happens. The CPU shadow is released.

BufferObject no longer owns a raw shadow Vector; it holds a PipeResource and
exposes one accessor, MappedData(), that all readers go through. Every buffer-data
consumer (UBO payload, PBO texture upload, indirect draws, resident/streamed
uploads, both backends) was migrated from GetDataReadOnly()->data() to
MappedData(), so a persistent buffer's readers see GPU memory - not a stale
shadow. That stale-shadow inconsistency is what corrupted rendering (wrong UBOs ->
misplaced/"lost" vertices) in the first zero-copy attempt (625c8a6, reverted in
896cafc); routing every consumer through one accessor makes it structurally
impossible.

Backends provide the map via BufferBackendOps::AcquirePersistentMap:
 - DirectVulkan: a HOST_VISIBLE|HOST_COHERENT (required, not just requested),
   persistently mapped resident VkBuffer carrying every usage, seeded from the
   shadow, never recreated; AcquireResidentSlice binds it directly.
 - DirectGLES: EXT_buffer_storage immutable persistent+coherent glMapBufferRange,
   falling back to the shadow when the extension is absent.

Fixes the ~7GB GpuMemory OOM + 100%-CPU/ANR running modern Blaze3D Minecraft on
both Magma and Espryt (per-draw whole-buffer re-upload of the coherent persistent
ring buffer), without the coherency/stale-read hazards of the reverted attempt.

BufferTest: zero-copy stress guard (15,360 draws -> 0 per-draw transfers, and every
reader resolves to GPU memory) + a shadow-fallback test. Host suite: 203/203 pass.
Device verification pending.
2026-07-11 20:18:38 -04:00
swung0x48 0f99d93300 [Feat] (MobileGL): full dual-source blending across state, transpiler, and both backends
Wire GL_SRC1_* dual-source blend factors (glBlendFunc) end to end with the
glBindFragDataLocationIndexed color index, so a fragment shader can drive both
dual-source blend inputs.

State + converters:
  - RenderState BlendFactor gains Src1Color/OneMinusSrc1Color/Src1Alpha/
    OneMinusSrc1Alpha; GLToMG/MGToGL/MGToVk/MGToStr converters map them to
    GL_SRC1_*, VK_BLEND_FACTOR_SRC1_*, and readable names.

Transpiler layout(index = N):
  - ProgramAttrib carries explicitFragmentOutIndices; ProgramObject threads
    m_explicitFragDataIndex into it at both link sites.
  - TMglGlslIoResolver applies the color index as TQualifier.layoutIndex on the
    fragment output, emitting layout(index = 1) via the glslang Index decoration
    -> SPIRV-Cross path. Only the non-zero (dual-source) index is emitted: index 0
    is the GL default and an explicit "index = 0" would demand
    GL_EXT_blend_func_extended on GLES for ordinary single-source outputs.

Feature detection, POST, and hard-fail at use time (no silent fallback):
  - Vulkan: dualSrcBlend is detected at device creation and cached; a draw whose
    enabled blend state uses a SRC1 factor without the feature throws at pipeline
    build with the reason and a pointer to the POST row.
  - GLES: GL_EXT_blend_func_extended detected at load into
    GLESCapabilities.SupportsDualSourceBlend; a draw enabling blend with a SRC1
    factor without it throws in the blend-state sync with the same guidance.
  - DriverPost adds a dual-source-blend row for both backends (Pass/Warn).

Tests:
  - ProgramTest.CompileAndLinkWithExplicitFragmentOut now asserts the transpiled
    fragment shader carries layout(location = 0, index = 1) after a re-link with
    glBindFragDataLocationIndexed(index 1), and still omits any index qualifier
    for the plain index-0 output.
2026-07-11 01:32:07 -04:00
swung0x48 e9fa99e16b [Feat] (MG_Backend): wire primitive restart into both backends; detect dualSrcBlend
Make GL_PRIMITIVE_RESTART[_FIXED_INDEX] actually take effect at draw time,
following the detect-at-init / POST / fallback-or-hard-fail discipline.

DirectVulkan:
- Thread primitiveRestartEnable through the pipeline (payload + hash +
  input-assembly), set from the GL_PRIMITIVE_RESTART / _FIXED_INDEX caps.
- Detect and enable primitiveTopologyListRestart
  (VK_EXT_primitive_topology_list_restart) at device creation; cache it.
  Strip/fan restart needs no feature; a *list* topology with restart and
  no feature hard-fails at the draw with the reason.
- Vulkan only restarts on the fixed all-ones index value, so an arbitrary
  GL_PRIMITIVE_RESTART index that is not that value hard-fails in
  UploadAndBindIndexBuffer (where the index type is known).
- Also detect+enable and cache the dualSrcBlend base feature (groundwork
  for GL_SRC1_* dual-source blending).

DirectGLES:
- Sync GL_PRIMITIVE_RESTART_FIXED_INDEX from either restart cap (GLES core
  has only the fixed-index form); an arbitrary non-fixed index hard-fails
  in the indexed draw paths with the reason.

POST: dualSrcBlend and primitiveTopologyListRestart capability rows (Pass
when supported, Warn with the fallback/hard-fail consequence otherwise).

Library builds clean; SanityTest 31/31. (The actual restart rendering and
the hard-fail paths need a real GPU and are not runtime-testable here.)
2026-07-11 01:11:23 -04:00
swung0x48 e18d369adf [Feat] (MG_Impl/GLImpl, MG_State): implement glPrimitiveRestartIndex
Store the primitive restart index as render state and report it through
glGetIntegerv(GL_PRIMITIVE_RESTART_INDEX), replacing the stub and the
hardcoded 0 in the getter.

- RenderState gains a PrimitiveRestartIndex field (default 0) with
  set/get accessors and GLContext wrappers.
- glPrimitiveRestartIndex accepts any GLuint and generates no error.
- glGetIntegerv(GL_PRIMITIVE_RESTART_INDEX) now reads the stored value.

This is the state layer only. The backends do not yet honor an arbitrary
restart index at draw time -- Vulkan and GLES support only the fixed
all-ones restart value (GL_PRIMITIVE_RESTART_FIXED_INDEX) -- so a non-
default index is tracked and queryable but not yet applied to indexed
draws.

Tests: RenderStateSanity round-trip (default 0, mid value, and the full
32-bit range). Full SanityTest sweep green (31/31).
2026-07-11 00:43:45 -04:00
swung0x48 22ac8a8c10 [Feat] (MG_Impl/GLImpl, MG_State): implement glBindFragDataLocationIndexed
Bind a fragment output to both a color number and a color index (0 or 1
for dual-source blending), and report the bound index back through
glGetFragDataIndex.

- ProgramObject now tracks a per-output color index alongside the
  location: SetExplicitFragmentOutIndex stores it, it is snapshotted into
  the linked map at link time (like the location map), and
  GetFragmentDataIndex returns it (0 by default) for an active output.
- glBindFragDataLocation becomes glBindFragDataLocationIndexed with index
  0, matching the GL definition, so it also resets a previously-bound
  index to 0.
- Validation: index must be 0 or 1 (GL_INVALID_VALUE); colorNumber is
  bounded by GL_MAX_DRAW_BUFFERS for index 0 and GL_MAX_DUAL_SOURCE_DRAW_BUFFERS
  (reported as 1) for index 1 (GL_INVALID_VALUE); a gl_ name is
  GL_INVALID_OPERATION.
- glGetFragDataIndex now returns the real bound index instead of a
  hardcoded 0.

The index is tracked for reflection but is not yet plumbed into dual-source
blend rendering, and shader-side layout(index=) qualifiers are not
reflected -- both documented at the call sites.

Tests: index round-trip through a re-link (bind 1 -> GetFragDataIndex == 1;
glBindFragDataLocation resets to 0), plus the validation error table;
mutation-verified end to end. ProgramTest 24/24.
2026-07-11 00:39:23 -04:00
swung0x48 bebe534bad [Fix] (MG_Impl/GLImpl): stop double-recording GL errors for a bad program handle
glBindFragDataLocation, glGetFragDataLocation and glGetFragDataIndex each
recorded a redundant GL_INVALID_OPERATION on top of the error that
TryToGetProgramObject already recorded (GL_INVALID_VALUE for an unknown
name, GL_INVALID_OPERATION for a non-program object). One bad call thus
queued two errors, so an app calling glGetError twice saw a spurious
second error, and any following code that expects a clean error queue
(e.g. a later test) picked up the stale one.

Drop the second RecordError from all three call sites and rely on the
single error TryToGetProgramObject already reports -- matching the clean
`if (!programObject) return;` pattern the rest of GL_Program.cpp uses. The
first, app-visible error is unchanged; only the redundant second is gone.

ProgramTest's invalid-handle case now asserts exactly one error (mutation-
verified: reintroducing the second record fails it) and keeps a defensive
error-queue drain. ProgramTest 24/24.
2026-07-11 00:19:33 -04:00
swung0x48 9ffcb23877 [Feat] (MG_Impl/GLImpl): implement glGetFragDataIndex
Fill the stubbed GL 3.3 Core glGetFragDataIndex, mirroring its already-
implemented sibling glGetFragDataLocation: validate the program object and
link status, then return the fragment color index the name binds to.

Every active user-defined output uses color index 0. MobileGL does not yet
track dual-source (index 1) bindings -- glBindFragDataLocationIndexed and
the layout(index = 1) qualifier are unsupported -- so the result is exact
for any program that does not use dual-source blending; a name that is not
an active output (including gl_ built-ins) returns -1.

Tests: assertions on the existing linked-program test (valid output -> 0,
unknown name -> -1) plus a standalone invalid-handle case. The invalid-
handle test drains the error queue it produces so no stale error leaks
into a later test (the ProgramTest fixture does not reset it). ProgramTest
24/24.
2026-07-11 00:03:08 -04:00
swung0x48 dc3c2cc5c7 [Feat] (MG_Impl, MG_Backend, MG_State): implement glMultiDrawArrays and glGetBufferSubData
Two previously-stubbed GL 3.3 Core entry points.

glMultiDrawArrays: mirrors the existing glMultiDrawElements(BaseVertex)
architecture end to end -- a new MultiDrawArrays backend function-table
slot dispatched from the frontend after program/primitive-mode validation
(plus a drawcount < 0 -> GL_INVALID_VALUE guard).
- DirectGLES: PrepareForDraw once, then loop native glDrawArrays with the
  same per-range client-side array upload the single DrawArrays does.
- DirectVulkan: build a MultiDrawCmd payload and hand it to a new
  VulkanRenderer::MultiDrawArrays, which does one SetupDraw over the union
  of the sub-draw vertex ranges and then a vkCmdDraw per range (mirrors
  VulkanRenderer::MultiDrawElements).

glGetBufferSubData: reads a range of the bound buffer's CPU shadow into
client memory via a new BufferObject::DownloadSubData, with the same
validation shape as BufferSubData (INVALID_VALUE for negative/overflowing
range, INVALID_OPERATION for no bound buffer or a non-persistent mapped
buffer). The shadow reflects CPU writes and backend write-backs but not
arbitrary GPU-side writes, which is documented on the method.

Tests: 2 BufferTest cases for glGetBufferSubData (round-trip read of a
middle range and the whole buffer, plus out-of-range/negative/no-buffer
errors). BufferTest 32/32, SanityTest 30/30, VertexArrayTest 42/42;
library builds clean. (The glMultiDrawArrays draw paths are not
runtime-testable on this host and are compile-verified against the tested
MultiDrawElements pattern.)
2026-07-10 23:46:14 -04:00
swung0x48 4dd2b2216c [Feat] (MG_Impl, MG_State, MG_Backend, MG_Util): packed 2_10_10_10 and GL_BGRA vertex array formats
glVertexAttribPointer now accepts the GL 3.3 Core packed types
GL_INT_/GL_UNSIGNED_INT_2_10_10_10_REV and the GL_BGRA size, clearing the
two long-standing "// TODO: implement GL_BGRA support" markers. Adds the
format end to end across the frontend, VAO state, and both backends.

- DataType: add Int2101010Rev / Uint2101010Rev with GLToMG / MGToGL /
  MGToStr converter cases.
- Validation (ValidateVertexAttribFormat): the full glVertexAttribPointer /
  glVertexAttribIPointer error table -- size is 1..4 or GL_BGRA (else
  INVALID_VALUE, which takes precedence); a packed type requires size 4 or
  GL_BGRA (else INVALID_OPERATION); GL_BGRA requires GL_UNSIGNED_BYTE or a
  packed type AND normalized == GL_TRUE (else INVALID_OPERATION); the
  integer path rejects packed types (INVALID_ENUM) and GL_BGRA size
  (INVALID_VALUE).
- VAO: store GL_BGRA as size 4 plus a new IsBgra flag (reset on the
  binding-format path).
- DirectVulkan: map the packed/BGRA formats to
  VK_FORMAT_A2B10G10R10_* (normal) and VK_FORMAT_A2R10G10B10_* /
  VK_FORMAT_B8G8R8A8_UNORM (BGRA reversed), fold IsBgra into the pipeline
  hash, and size packed/BGRA elements as one 4-byte word via
  GetAttributeByteSize. (Vulkan *_SNORM decodes with the GL 4.2 symmetric
  rule, a documented deviation from the 3.3 signed formula.)
- DirectGLES: round-trip the packed enum through the loader, pass GL_BGRA
  as the driver size argument, and size client uploads with the packed
  4-byte word.

Tests: 4 VertexArrayTest cases covering packed/BGRA storage and the full
float/integer error table; the packed-size hard-fail is mutation-verified.
VertexArrayTest 42/42, SanityTest 30/30, library builds clean.
2026-07-10 23:25:25 -04:00
swung0x48 0cada09aa7 [Feat] (MG_Impl/GLImpl): implement 8 packed glVertexAttribP*ui current-value setters
glVertexAttribP{1,2,3,4}ui and their *uiv forms set the CURRENT generic
vertex attribute value from a packed 2_10_10_10_REV word (they are the
packed members of the immediate VertexAttrib* family, not the array-format
path), so they funnel into SetCurrentVertexAttributeFloat and reuse the
existing index validation.

- Add DecodePacked2101010: unpacks x=[0..9], y=[10..19], z=[20..29] (10-bit)
  and w=[30..31] (2-bit) from one 32-bit word. Signed fields are two's-
  complement (sign-extended per width); normalized conversion uses the
  GL 3.3 (2c+1)/(2^b-1) form (10-bit /1023, 2-bit /3), matching the
  existing NormalizeSigned* helpers -- NOT the GL 4.2 clamp form.
- type accepts only GL_INT_2_10_10_10_REV / GL_UNSIGNED_INT_2_10_10_10_REV
  (GL_INVALID_ENUM otherwise; the 4.4-era 10F_11F_11F_REV is not legal in
  3.3). P1/P2/P3 consume the first 1/2/3 components; the rest take the
  (0,0,0,1) defaults and are cleared each call. The *uiv forms dereference
  a single packed word, not an array.

Tests: 4 VertexArrayTest cases (unsigned decode, signed GL-3.3 formula,
component-count/defaults, type/index/uiv validation). The signed test is
mutation-verified: z==0 -> 1/1023 fails against the GL 4.2 form.
VertexArrayTest 38/38, SanityTest 30/30.
2026-07-10 23:07:13 -04:00
swung0x48 3ff8cafac6 [Feat] (MG_Util/SelfTest): POST rows for polygon-mode and indexed-color-mask capabilities
Surface the device features that glPolygonMode and glColorMaski depend on,
so a missing capability (and the resulting FILL / draw-buffer-0 fallback)
is visible in the driver POST instead of silently degrading.

- DirectVulkan checklist: fillModeNonSolid (GL_LINE/GL_POINT rasterization)
  and independentBlend (per-draw-buffer color masks) rows, read from the
  physical device features already queried by the probe.
- DirectGLES checklist: "Polygon mode" (GL_NV/ANGLE_polygon_mode) and
  "Indexed color mask" (ES 3.2 core or draw_buffers_indexed) rows, read
  from the cached GLESCapabilities flags.

Each row passes when supported and warns (not fails) when absent, since
the fallback still renders correctly. Builds clean; SanityTest sweep green
(30/30).
2026-07-10 22:31:20 -04:00
swung0x48 041de6cba3 [Feat] (MG_Backend/DirectGLES, MG_Util/Loader): wire glPolygonMode and glColorMaski into GLES sync
Neither entry point exists in unextended OpenGL ES core, so both are
gated on optional extensions detected and cached at init, with a runtime
fallback when absent.

Loader:
- Add glPolygonModeNV/glPolygonModeANGLE and glColorMaskiEXT/glColorMaskiOES
  to the GLES function table, loaded via a new INIT_GLES_FUNC_OPTIONAL
  macro that does not log an error when the driver lacks them.
- Cache GLESCapabilities.SupportsPolygonMode and SupportsIndexedColorMask
  from whether the entry points loaded (glColorMaski is GLES 3.2 core with
  no extension string, so pointer presence is the reliable signal).

Sync (SyncRenderState):
- Color mask: uniform masks keep using the non-indexed glColorMask (works
  everywhere); divergent per-draw-buffer masks use glColorMaski (core /
  EXT / OES, whichever loaded) when SupportsIndexedColorMask, else fall
  back to broadcasting draw buffer 0. Mirrors the existing indexed-blend
  block's all-same-vs-per-buffer structure.
- Polygon mode: new sync block calls glPolygonModeNV/ANGLE(GL_FRONT_AND_BACK,
  mode) when SupportsPolygonMode; without the extension the mode stays FILL
  and non-FILL requests are dropped.

Library builds clean; full SanityTest sweep green (30/30).
2026-07-10 22:27:21 -04:00
swung0x48 aa5c33a42d [Feat] (MG_Backend/DirectVulkan): wire glPolygonMode and glColorMaski into pipeline creation
Consume the polygon mode and per-draw-buffer color write masks that the
frontend already tracks, with runtime fallback for the device features
they require.

glPolygonMode:
- Add ConvertPolygonModeToVkEnum (GL_FILL/LINE/POINT -> VkPolygonMode).
- Thread a polygonMode field through PipelineCreatePayload, fold it into
  the pipeline cache hash (distinct modes need distinct pipelines), and
  apply it in PipelineFactory instead of the hardcoded VK_POLYGON_MODE_FILL.
- LINE/POINT require the fillModeNonSolid device feature: detect and
  enable it at device creation, cache m_fillModeNonSolidFeatureEnabled,
  and fall back to FILL at pipeline-build time when it is absent.

glColorMaski:
- The per-attachment color-blend loop now reads GetColorMaskIndexed(i)
  instead of the broadcast GetColorMask(), so each draw buffer gets its
  own write mask (already covered by the pipeline hash).
- Divergent per-attachment masks require independentBlend: cache
  m_independentBlendFeatureEnabled (was enabled but never recorded) and
  fall back to draw buffer 0's mask for every attachment when it is absent.

The internal depth-mipmap utility pipeline keeps VK_POLYGON_MODE_FILL (not
GL-driven). Library builds clean; full SanityTest sweep green (30/30).
2026-07-10 22:16:47 -04:00
swung0x48 f892f609c2 [Fix] (MG_Backend/DirectVulkan+DirectGLES, MG_State): cap per-stage GL_MAX_TEXTURE_IMAGE_UNITS to 32
Adreno/Qualcomm report a huge maxPerStageDescriptorSampledImages, and the
per-stage texture-unit limits were clamped only to the combined array capacity
(TextureState::MAX_TEXTURE_IMAGE_UNITS = 192). glGetIntegerv thus advertised 192
for GL_MAX_TEXTURE_IMAGE_UNITS, but host code treats it as an array bound:
Minecraft's Blaze3D GlStateManager.TEXTURES[] holds 128 entries and Iris iterates
[0, GL_MAX_TEXTURE_IMAGE_UNITS) over it in CompositeRenderer.renderAll, throwing
ArrayIndexOutOfBoundsException: Index 128 out of bounds for length 128.

Introduce MAX_PER_STAGE_TEXTURE_IMAGE_UNITS = 32 (desktop-driver value) and clamp
the per-stage sampler limits to it in both backends (DirectGLES previously did not
clamp at all), keeping the combined limit at the array capacity. Update SanityTest.
2026-07-10 21:53:04 -04:00
swung0x48 5e8106114f [Feat] (MG_Impl/GLImpl, MG_State, MG_Backend): implement glColorMaski
Promote the color writemask to per-draw-buffer state and implement the
indexed glColorMaski entry point (previously a stub), plus its read-back
through glGetBooleani_v.

- RenderState: replace the single BoolVec4 ColorMask with an array of
  MAX_DRAW_BUFFERS masks, all initialized to true. SetColorMask now
  broadcasts to every draw buffer (glColorMask semantics); GetColorMask
  returns draw buffer 0. Add indexed set/get accessors + GLContext
  wrappers.
- glColorMaski sets only the addressed draw buffer; out-of-range index
  raises GL_INVALID_VALUE (buf is a GLuint, so no GL_INVALID_ENUM path),
  mirroring the indexed blend entry points' MAX_DRAW_BUFFERS bound.
- glGetBooleani_v(GL_COLOR_WRITEMASK, i) reports draw buffer i's four
  booleans; the non-indexed glGetBooleanv still reports draw buffer 0.
- Fix GLboolean coercion in the color-mask path: any nonzero value
  enables the component (was == GL_TRUE, which wrongly rejected e.g. 2).
- DirectGLES sync reads ColorMasks[0] (GLES core has only non-indexed
  glColorMask).

Tests: ColorMaskIndexedStoresAndReadsBack covers the per-buffer vs
broadcast semantics, buffer-0 read-back, out-of-range INVALID_VALUE, and
the GLboolean coercion (mutation-verified: == GL_TRUE fails it). Full
SanityTest sweep green (30/30).
2026-07-10 21:30:57 -04:00
swung0x48 95876d9d8c [Feat] (MG_Impl/GLImpl, MG_State): implement glClampColor and glPolygonMode
Fill the two empty // TODO state handlers with GL 3.3 Core-conformant
behavior, backed by new RenderState fields and glGet* read-back.

glClampColor:
- Accept only GL_CLAMP_READ_COLOR (compat GL_CLAMP_VERTEX/FRAGMENT_COLOR
  rejected); clamp is one of GL_TRUE / GL_FALSE / GL_FIXED_ONLY. Note the
  Khronos man page wrongly omits GL_FIXED_ONLY from the accepted set, but
  it is legal AND the default, so it is accepted here.
- Default GL_FIXED_ONLY; both error paths are GL_INVALID_ENUM with no
  state change. glGetIntegerv returns the raw tri-state enum; GetFloatv/
  GetDoublev widen it and GetBooleanv converts nonzero to GL_TRUE via the
  existing fall-through, so one GetIntegerv case serves every getter.

glPolygonMode:
- Core accepts only face == GL_FRONT_AND_BACK (GL_FRONT/GL_BACK were
  removed in 3.1 core); mode is GL_POINT / GL_LINE / GL_FILL. Both errors
  are GL_INVALID_ENUM with no state change.
- Keep separate front/back slots so GL_POLYGON_MODE round-trips its two
  values (identical under a core context). The raster effect (VkPolygonMode
  + fillModeNonSolid) remains a backend follow-up; this is the state layer.

Tests: two RenderStateSanity round-trips; the glClampColor GL_FIXED_ONLY
acceptance assertion is mutation-verified (rejecting it fails the test).
Full SanityTest sweep green (29/29).
2026-07-10 21:21:40 -04:00
swung0x48 e460536119 [Feat] (MG_Impl/GLImpl, MG_State): implement glHint, glPointParameter*, glPixelStoref, glGetDoublev
Six pure-state entry points that were stubs or empty // TODO bodies, all backed by new
context state and read back through glGet*.

* glHint: Hint_State was an empty TODO. Store the 4 GL 3.3 core hint targets (LINE_SMOOTH,
  POLYGON_SMOOTH, TEXTURE_COMPRESSION, FRAGMENT_SHADER_DERIVATIVE), default GL_DONT_CARE.
  Validate target and mode (FASTEST/NICEST/DONT_CARE) -> GL_INVALID_ENUM otherwise. The
  compatibility-only targets (GL_PERSPECTIVE_CORRECTION_HINT, GL_POINT_SMOOTH_HINT, GL_FOG_HINT,
  GL_GENERATE_MIPMAP_HINT) are rejected. The glGetIntegerv hint cases, previously hardcoded to
  GL_DONT_CARE, now read the stored value; glGetBooleanv on a hint is always GL_TRUE.

* glPointParameter{f,i,fv,iv}: the scalar _State bodies were empty TODOs and the *v forms were
  stubs. Only the 2 core pnames are accepted: GL_POINT_FADE_THRESHOLD_SIZE (float, default 1.0,
  GL_INVALID_VALUE if negative) and GL_POINT_SPRITE_COORD_ORIGIN (GL_LOWER_LEFT/GL_UPPER_LEFT,
  default GL_UPPER_LEFT, GL_INVALID_ENUM on a bad value -- note the different error code from the
  fade case). The compat pnames (POINT_SIZE_MIN/MAX, POINT_DISTANCE_ATTENUATION) are rejected. All
  four forms funnel through one (pname, float) handler. glGetIntegerv(GL_POINT_FADE_THRESHOLD_SIZE)
  was hardcoded to 1; it now rounds the stored float, glGetFloatv reads the float directly (keeping
  the fractional part), and GL_POINT_SPRITE_COORD_ORIGIN gained a getter case (it had none).

* glPixelStoref: funnels into the existing glPixelStorei state, but converts per type -- boolean
  pnames (PACK/UNPACK_SWAP_BYTES/LSB_FIRST) by a zero-test so 0.4 -> TRUE, integer pnames by
  round-to-nearest. A blanket round would wrongly turn a fractional true flag into false.

* glGetDoublev: funnels through glGetFloatv and widens, writing exactly the pname's component count
  (1/2/4) so a single-component query cannot overrun the caller's buffer. MobileGL stores no native
  double state (depth range/clear are float), so widening from float matches its real resolution.

State added to RenderStateParameters + RenderState Set/Get + GLContext wrappers, following the
existing LineWidth/DepthRange pattern. Covered by 4 SanityTest cases (set-then-get round trips, the
core-vs-compat enum rejections, the two different error codes, and the glPixelStoref boolean
zero-test, which was verified to fail against a blanket-round implementation).
2026-07-10 20:35:08 -04:00
swung0x48 561d8992bc [Feat] (MG_Impl/GLImpl, MG_State): implement glGetActiveUniformsiv (UBO reflection query)
Completes the uniform-block reflection chain: glGetUniformIndices, glGetActiveUniformName
and glGetActiveUniformBlockiv were already implemented; glGetActiveUniformsiv was the last
stub. Supports all 8 GL 3.3 Core pnames:

* GL_UNIFORM_TYPE / SIZE / NAME_LENGTH / BLOCK_INDEX / OFFSET / ARRAY_STRIDE come straight from
  glslang's TObjectReflection (the same reflection the existing uniform queries use).
* GL_UNIFORM_IS_ROW_MAJOR from the member's TType layout qualifier, guarded by isMatrix() so a
  scalar in a layout(row_major) block does not wrongly report 1.
* GL_UNIFORM_MATRIX_STRIDE is derived: glslang exposes no matrix stride, so it is computed from the
  std140 rule (each column/row vector rounded up to a vec4), which matches the std140 layout
  MobileGL's SPIR-V path emits. Evaluates to 16 for every GL 3.3 float matrix.

The -1-vs-0 distinction is handled explicitly: OFFSET / ARRAY_STRIDE / MATRIX_STRIDE / BLOCK_INDEX
return -1 for a default-block uniform (glslang gives arrayStride 0 there, so it is gated on block
membership), while ARRAY_STRIDE / MATRIX_STRIDE return 0 for a non-array / non-matrix member that IS
in a block. Errors: GL_INVALID_VALUE for uniformCount<0, any index >= active uniform count, or a
never-generated program name; GL_INVALID_OPERATION for a live shader name; GL_INVALID_ENUM for an
unaccepted pname (e.g. the GL 4.2 GL_UNIFORM_ATOMIC_COUNTER_BUFFER_INDEX). All validation runs before
any write, so params is untouched on error. There is no "not linked" error -- an unlinked program has
zero active uniforms, so any index raises GL_INVALID_VALUE.

Also fix GetActiveUniformArraySize, which returned glslang's TObjectReflection.size verbatim: that
field only carries the element count for a non-block array and reports 1 for a block array member,
so GL_UNIFORM_SIZE (and glGetActiveUniform's size out-param, and glGetProgramResourceiv's
GL_ARRAY_SIZE) wrongly reported 1 for an array inside a UBO. Take the count from the TType instead,
which is authoritative for both cases.

Covered by 3 ProgramTest cases (std140 block with scalar/array/mat4 + a default-block sampler, a
row_major variant, and the six error cases) that link real shaders and assert every pname value.
2026-07-10 19:54:33 -04:00
swung0x48 d5e19cb7ba [Feat] (MG_Impl/GLImpl): implement 42 stubbed glVertexAttrib*/glGetVertexAttribdv current-value entry points
These set (or query) the current generic vertex attribute value, GL_CURRENT_VERTEX_ATTRIB.
All funnel into the existing, correct primitives -- VertexAttrib4f / VertexAttribI4i /
VertexAttribI4ui, and GetVertexAttribfv for the double query -- so the new bodies add only a
null-pointer guard; index validation (incl. the deliberate index-0 rejection) is inherited.

Families implemented (of the 49 core glVertexAttrib* setter stubs, all but the 8 packed
glVertexAttribP*ui, which need a real 2_10_10_10 DataType and are left for later):

* d / dv / s / sv and 4bv / 4iv / 4uiv / 4usv: value-preserving conversion to float. These do
  NOT normalize -- only the N forms do.
* 4Nbv / 4Nsv / 4Niv / 4Nusv / 4Nuiv: normalized. Signed normalization uses the GL 3.3 Core
  formula f = (2c + 1) / (2^b - 1), which maps the full signed range onto exactly [-1, 1] (byte
  -128 -> -1.0, 127 -> +1.0) and cannot represent 0 exactly (0 -> 1/(2^b-1)). This is NOT the
  GL 4.2 revision f = max(c/(2^(b-1)-1), -1); using that here would be a conformance bug.
  Unsigned normalization is the version-independent c/(2^b-1). The 32-bit forms compute in double
  because 2*INT_MAX overflows int32 and neither 2^32-1 nor 2^31-1 is representable as float.
* VertexAttribI{1,2,3}{i,iv,ui,uiv} and I4{bv,sv,ubv,usv}: integer forms, writing the integer
  current-value view verbatim (never the float one). Signed sign-extend to VertexAttribI4i,
  unsigned zero-extend to VertexAttribI4ui; w defaults to the integer 1. I4ubv/I4usv route to the
  unsigned setter (distinct from the normalized-float 4Nubv).
* glGetVertexAttribdv mirrors GetVertexAttribfv: reads the float view as four doubles for
  GL_CURRENT_VERTEX_ATTRIB (no bound VAO required), one value for the array pnames, same error rules.

Covered by 5 new round-trip tests whose boundary values (byte -128 -> -1.0 exact, 0 -> 1/255,
INT_MIN/MAX endpoints exact, ushort 65535 non-normalized -> 65535.0, integer w == 1) discriminate
the correct formulas; the signed-normalization test was verified to fail against the GL 4.2 form.
2026-07-10 12:23:23 -04:00
swung0x48 d40f753983 [Fix] (MG_State, MG_Impl, MG_Backend): conformant current generic vertex attribute values
GL 3.3 Core: a shader input whose generic attribute array is disabled reads that
attribute's current value (per-context state, default (0,0,0,1)). Four defects made
that path non-conformant, three of them silently.

* Out-of-bounds current-value reads. m_currentVertexAttributes held 16 entries while
  the DirectVulkan draw path walked shader input locations 0..31 and GL_MAX_VERTEX_ATTRIBS
  was advertised straight from the device (commonly 32). The only guard was MOBILEGL_ASSERT,
  which expands to nothing outside debug builds. Grow the storage capacity to 32, advertise
  min(device limit, capacity), validate against that dynamic limit, and give the accessors
  real runtime bounds checks. Replace the literal 32 loops with the constant, and pin
  MAX_VERTEX_ATTRIBS to the Uint32 mask width and to vertexInputTypes' bound with
  static_asserts so the two can no longer drift apart -- that drift was the bug.

* DirectGLES never fed current values to the driver. Values were stored in MG_State only,
  so a disabled attribute always rendered as the ES driver's own untouched (0,0,0,1) while
  DirectVulkan rendered it correctly: identical GL code, different pixels per backend.
  Add SyncCurrentVertexAttributeValues() to the draw prologue, and hoist the
  glType -> (base type, component count) dispatch into MG_State::GLState so both backends
  resolve the semantics from one place instead of it living inside VulkanRenderer.

* Enabled arrays the backend could not map were silently demoted to the current value.
  ToVkVertexFormat had no DataType::Float16 case, so a GL_HALF_FLOAT array fell to
  VK_FORMAT_UNDEFINED, dropped out of the vertex input state, and became indistinguishable
  from a disabled array: the geometry rendered a constant colour with GL_NO_ERROR. Add the
  Float16 mapping, track an unsupportedAttribMask, and hard-fail the draw before pipeline
  creation so no synthetic attribute is baked into a cached VkPipeline.

* glGetVertexAttrib{fv,iv,Iiv,Iuiv}(GL_CURRENT_VERTEX_ATTRIB) returned before any index
  validation, reading past the array instead of raising GL_INVALID_VALUE.

Also resolve ProgramObject::DoReflection's "TODO: get from backend" 16-location clamp,
which capped the new DirectGLES sync at locations 0..15; report GL_MAX_VERTEX_ATTRIBS
through the same helper the validators use, so the clamp cannot be bypassed; and bound
vertex binding indices by the same dynamic limit, since the default attribute -> binding
mapping is the identity.

Add a "Vertex attributes" driver POST row to both backends: FAIL below the GL 3.3 Core
minimum of 16, WARN above MobileGL's storage capacity (clamped, extra attributes unusable),
PASS in between -- making the driver/host mismatch that caused the out-of-bounds read
visible instead of silently swallowed.

Covered by 7 new regression tests (each verified to fail against the previous behaviour).
2026-07-10 11:23:16 -04:00
swung0x48 eb090c6170 [Fix] (MG_Backend/DirectVulkan): fence-backed GL sync objects and per-frame descriptor rewind
- Track every graphics-queue submission with a real fence: pooled fences
  for mid-frame flushes, the frame slot's fence for Present and readback.
  Completion advances a submit counter via vkGetFenceStatus polls,
  slot-fence waits, and device-idle points, and raises the buffer-manager
  serial floor from the frame serial each submission carried.
- GL sync objects now capture the submission index that will carry the
  commands recorded so far; ClientWaitSync honors
  GL_SYNC_FLUSH_COMMANDS_BIT with a mid-frame submit (gated on the index
  still being unsubmitted so poll loops cannot split the render pass), and
  blocking waits flush then vkWaitForFences with the caller timeout.
- FlushPendingCommands retires the submitted command buffer and restarts
  recording on a fresh one; retired buffers are freed once the slot fence
  is next waited, so an executing buffer is never reset.
- Rewind descriptor-set cursors exactly once per frame in Present (after
  the slot-fence wait), plus after the synchronous readback drain,
  replacing the ten lazy per-draw-path rewinds.

Verified: host tests 168/168, trace-replay 70/70.
2026-07-10 10:55:10 +00:00
swung0x48andClaude Fable 5 7b00255b11 [Feat] (MG_Util/SelfTest): grouped POST rows, merged probes, MobileGL-reported strings
Rows in each backend section now sort FAIL -> WARN -> PASS -> INFO
(stable within groups), with identity strings always last: the device
strings renamed to 'Backend driver reported GL_*' and a new bottom
group 'MobileGL reported GL_VENDOR/GL_VERSION/GL_RENDERER/GL_EXTENSIONS'
showing exactly what MobileGL advertises to applications on that
backend, assembled from the same sources as GL_Getter and the backend
objects (extension-list construction extracted into shared helpers so
POST cannot drift from the real advertisement).

Rows probing the same subject are merged into single verdicts whose
details keep every sub-fact and causal chain: the six EGL setup steps
become one 'ES3 context' row, extension presence + functional probe
become one 'Timer queries' row per backend (including the
MOBILEGL_DISABLE_TIMERQUERY override explanation), and the Vulkan
loader/instance, surface-extension pair, and physical-device/queue/API
chains each collapse into one row.

Capability rows previously dumped as INFO now carry verdicts: index
type uint8 (WARN when absent - uint8 index buffers have no conversion
fallback), VK_KHR_draw_indirect_count (WARN when absent - count draws
degrade to CPU readback loops); buffer_storage/base_instance stay
honest INFO when absent since no MobileGL path degrades.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 03:28:50 +00:00
swung0x48andClaude Fable 5 f0dd5d667b [Feat] (MG_Util/SelfTest): timer-query rows and functional probes in POST
GLES section reports GL_EXT_disjoint_timer_query and, when present,
runs a real TIME_ELAPSED span (paced availability polling matching the
runtime path) and reports the observed nanoseconds. Vulkan section
reports timestampValidBits/timestampPeriod and runs a full functional
probe - logical device, command buffer, two vkCmdWriteTimestamp into a
fresh query pool, submit, fenced wait, read-back - with hung-GPU-safe
teardown (a timed-out fence skips vkDeviceWaitIdle and leaks
deliberately rather than hanging the POST). Both sections note when
MOBILEGL_DISABLE_TIMERQUERY suppresses the feature.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 01:08:59 +00:00
swung0x48andClaude Fable 5 7eb3994b02 [Feat] (MG_Impl, MG_Backend): GL_ARB_timer_query on both backends
Implements GL timer queries end to end: a frontend query registry
(modeled on the sync module - mutex-guarded objects wrapping opaque
backend handles behind optional function pointers) serving
glGenQueries/glBeginQuery/glEndQuery(GL_TIME_ELAPSED)/glQueryCounter
(GL_TIMESTAMP)/glGetQueryObject*/glGetQueryiv with GL 3.3 error
semantics and a graceful zero-result fallback when a backend cannot
time.

DirectGLES backs spans with GL_EXT_disjoint_timer_query (context-
generation-stamped handles, bounded result waits). DirectVulkan gets a
VkTimerQueryManager: per-frame-in-flight timestamp query pools reset at
command-buffer begin (outside render passes), records harvested by
frame serial before their pool recycles, elapsed = masked tick delta x
timestampPeriod; handles are stamped with a renderer generation that
also now guards fence syncs across renderer recreation. GL_QUERY_
COUNTER_BITS reports 0 unless the live backend can actually time
(dynamic IsTimerQuerySupported hook), and a failed blocking read keeps
the handle alive so the real value stays reachable once the frame
submits.

GL_ARB_timer_query is advertised only when the device supports timing
and MOBILEGL_DISABLE_TIMERQUERY is unset - LWJGL keys Minecraft's F3
'GPU: x%' line off exactly that extension string; verified on device
(Adreno 830) on both backends.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 01:08:38 +00:00
swung0x48andClaude Fable 5 7d31a6fcd7 [Refactor] (MG_Config): centralize env-var and driver-feature reads
MG_Config::FeaturesTable snapshots every MOBILEGL_* toggle once in
ConfigLoader::Init with a single truthy rule (non-empty, not '0', not
'false' case-insensitively), replacing 13 scattered std::getenv sites
that used four different parsing conventions. Renderer-derived bits
(IsAngleRenderer/IsAngleLlvmpipeRenderer/AvoidSamplerMipmapMinFilter)
move into GLESCapabilities, set once in FillInGLESCapabilities, so hot
paths (glMemoryBarrier ANGLE flush, sampler min-filter sync) stop doing
per-call string scans. MOBILEGL_PRESENT_DUMP_CALL/_CURRENT_CALL stay
live getenv (the retrace harness mutates them at runtime) and
MOBILEGL_LOG_FILE_PATH stays in Log.cpp (log init precedes config
init); both are documented in Config.h. Known semantic unification:
MOBILEGL_DISABLE_SUBGROUP previously required exactly 'true' and
MOBILEGL_PRESENT_STATS exactly '1'; both now follow the shared rule
(CI's 0/1 values parse identically). Also bumps CoreVersion to 26.07.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 01:08:06 +00:00
swung0x48andClaude Fable 5 096d6f591b [Chore] (android-plugin): make versionCode monotonic within a month
major*100 + minor collides for multiple releases in the same month, and
Android refuses to install a package whose versionCode is not strictly
greater than the installed one. Encode as year*1_000_000 + month*10_000 +
monthly-revision (commits since the month start), so every build upgrades
cleanly; the month weight dwarfs the per-month reset on rollover.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 22:26:29 +00:00
swung0x48andClaude Fable 5 19348631ab [Chore] (android-plugin): calendar versioning 26.07 with commit-hash build id
versionCode = major * 100 + minor (2607); versionName = 26.07.<short git
hash> (e.g. 26.07.4e558ee, -trace suffixed for trace flavors), replacing
the placeholder versionCode 1 / versionName 'dev'.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 22:21:48 +00:00
swung0x48andClaude Fable 5 4e558ee142 [Docs] (trace-replay): refresh fixture-authoring skill from the Create fixture work
Registration now documents the trace_cases.json registry (the CMakeLists /
apk.yml instructions were stale). Adds the field-tested guidance from
authoring the Create fixtures: in-tree apitrace fork requirements (frametrim
DSA/multi-bind, persistent-map shadowing) and the Windows wgltrace wrapper,
frozen-world + unfocused-window capture discipline, late-frame selection,
trim verification, brotli repack (with the stale-archive trap), golden
content verification, Android signing/stale-package/emulator-flake and
stale-result pitfalls.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 14:26:52 +00:00
swung0x48andClaude Fable 5 effdaabab3 [Feat] (android-plugin): table-style POST report with tap-to-expand details
Checks render as a two-column table (name | colored status chip) with
alternating row stripes; per-check detail text is hidden until the row
is tapped, and the raw JSON report collapses behind a bottom toggle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 13:55:14 +00:00
swung0x48andClaude Fable 5 a394fe1af3 [Test] (MG_Test): cover the indirect gl_InstanceID probe and shader rewrite
BackendLoaderTest drives ProbeIndirectInstanceIdIncludesBaseInstance
(now externally linked) against a fake GLES function table: conforming
and ANGLE-style leaking drivers, the no-vertex-SSBO skip, draw-error
inconclusiveness, object cleanup, and the FillInGLESCapabilities wiring
end-to-end. SanityTest gains PromoteDrawParameterGlobalsToUniforms
cases pinning the mg_ZeroBasedInstanceID rewrite and the
last-SSBO-binding computation against a non-default binding count,
with RAII capability restoration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 13:44:25 +00:00
swung0x48andClaude Fable 5 d16b7ccd6a [Feat] (MG_Util/SelfTest, android-plugin): driver POST self-test screen
Opening a MobileGL plugin APK now shows a POST screen that probes the
device's GLES and Vulkan drivers independently against MobileGL's
expectations - a device may satisfy only one backend - and reports a
per-backend verdict (OK / DEGRADED / UNSUPPORTED) with per-check rows.

The GLES probe builds its own ES3 pbuffer context on the system driver
and reuses FillInGLESCapabilities, including the indirect-draw
gl_InstanceID semantics probe; the Vulkan probe checks instance/device
requirements and the optional features each DirectVulkan path degrades
without. Results serialize as ASCII-safe JSON through a JNI entry in
libMobileGL.so; PostActivity renders them and caches the run per
process (single-flight, rotation-safe). PluginActivity keeps its
NoDisplay stub but the launcher entry moves to the POST screen; FCL
plugin discovery reads application meta-data and is unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 13:44:14 +00:00
swung0x48andClaude Fable 5 28facc1c3f [Feat] (trace-replay): add Create flywheel indirect and instancing fixtures
Two 1.21.1 NeoForge Create in-world captures facing water wheels and a
large cogwheel, one per flywheel backend (/flywheel backend indirect and
instanced). The indirect trace exercises the compute scatter/cull
pipeline, glMultiDrawElementsIndirect with GPU-written commands, and
draw-parameter emulation; captured with persistent-map shadowing so the
unflushed scatter descriptors Flywheel writes are recorded. Both trimmed
to a single frame and brotli-repacked (~7 MiB each).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 12:14:17 +00:00
swung0x48andClaude Fable 5 85b68a9969 [Fix] (MG_Backend/DirectGLES): rebase gl_InstanceID for native indirect draws on ANGLE
ES keeps gl_InstanceID zero-based and ignores the indirect command's
'reserved, must be zero' word, but ANGLE-on-Vulkan forwards the command
verbatim to vkCmdDraw*Indirect and compiles gl_InstanceID to SPIR-V
InstanceIndex, which includes firstInstance. Shaders computing
gl_BaseInstance + gl_InstanceID (Flywheel indirect) then add the base
twice, scrambling instance-to-mesh association.

Probe the actual driver semantics at capability-fill time with a tiny
indirect draw (an ES indirect draw needs a non-default VAO) and, on
leaking drivers, rewrite vertex shaders that use the native indirect
SSBO machinery so gl_InstanceID subtracts the command's baseInstance
word during native indirect draws.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 12:14:02 +00:00
swung0x48andClaude Fable 5 a6a5edf573 [Perf] (MG_Backend/DirectGLES, MG_Impl): cache link-time lookups, bound unit scans, honor eglSwapInterval
- BackendProgramObjectImpl::CacheResourceLocations resolves every
  glGetUniformBlockIndex / glGetUniformLocation string query once per
  link and establishes the block binding points there. Per draw,
  BindCurrentProgramWithResources now uses the cached indices, re-issues
  glUniform1i only when a sampler's unit actually changed (program state
  persists), uploads the global UBO only when its content version moved,
  and skips redundant glUseProgram binds (guard reset on program-name
  reuse, MakeCurrent, and every explicit glUseProgram(0)). The caches are
  invalidated through ProgramObject's link version, which also makes a
  relinked program finally re-sync its backend program.
- Track a texture-unit high-water mark (fed by glBindTexture /
  glBindTextureUnit / glBindSampler / glBindImageTexture) so the two
  per-draw unit scans (MAX_TEXTURE_IMAGE_UNITS is 192) and the
  texture-deletion unbind loop only walk units that were ever touched.
- Forward the app's eglSwapInterval to the native EGL surface through a
  new BackendObject::SetEGLSwapInterval hook (applied immediately when
  the surface exists, otherwise deferred to surface creation /
  MakeCurrent). "VSync off" finally reaches the hardware - DirectGLES
  was hard-locked to the display refresh before.

The driver-side cost of the per-draw string lookups was about half of a
30% Adreno driver hotspot; libMobileGL's share of the vanilla render
thread fell from 22% to 9% (simpleperf, Adreno 830).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 12:12:51 +00:00
swung0x48andClaude Fable 5 bd208783d7 [Perf] (MG_Backend/DirectVulkan, MG_State): stop re-deriving per-draw state
- Memoize the program content hash on ProgramObject (keyed by the backend
  state version + compile flags; relinking and binding changes invalidate
  it) and the vertex-input hash on VertexArrayObject (keyed by a new
  aggregate config version bumped by every attribute mutation). Full-SPIRV
  XXH64 hashing fell from 13.7% to 1.4% of the render thread.
- ProgramObject also gains a link version and a global-UBO content version
  (bumped by uniform writes and on relink, wrap-safe around the backends'
  "never uploaded" sentinel) for backends to gate uploads and link caches.
- Reuse member scratch vectors in SetupDraw, UploadAndBindVertexBuffers,
  GetOrCreatePipeline and BindProgramUniformBuffers instead of allocating
  per draw (~12% of render-thread time was in the allocator).
- Replace hot-path dynamic_cast with AsMipmapTexture (storage-type tag +
  static_cast); TextureObjectMipmap is the only Mipmap-tagged branch.
- Register/prune texture aliases only when a new (texture, lifetimeId)
  identity appears instead of scanning the entire alive map on every
  sampled-texture sync.
- Make the fallback VkPresentModeKHR log strings report the actual mode.

Vanilla render-thread share of libMobileGL dropped from 48% to 35% on
DirectVulkan (simpleperf, Adreno 830).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 12:12:50 +00:00
swung0x48andClaude Fable 5 08e808ef20 [Perf] (MG_State/FastSTL): make object deletion cheap again
- GLContext::MarkBufferObjectForDeletion now detaches the deleted buffer
  only from the currently bound VAO (GL 4.6 5.1.2 semantics; other VAOs
  keep their shared_ptr attachments alive). The old every-VAO scan was
  O(VAOs) per delete - with one VAO per chunk section, vanilla chunk
  churn made it dominate the render thread and FPS decay over minutes.
- Bump FastSTL: erase(key) destroys in place instead of building the
  discarded successor iterator (a linear bucket-array scan), and switch
  the buffer/framebuffer/renderbuffer deletion paths to the key overload.

Together these removed the 34% render-thread deletion overhead measured
in aged vanilla sessions (simpleperf, Adreno 830 / DirectVulkan).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 12:12:50 +00:00
swung0x48andClaude Fable 5 bb3a18c627 [Fix] (MG_Backend/DirectGLES): rebind image uniforms to frontend image units
139de763 started preserving layout(binding) on SSBO/image declarations in
transpiled ESSL (ES cannot rebind either through the API). That is correct
for SSBOs and for images whose GL source carries an explicit binding
(Flywheel), but wrong for image uniforms without one: glslang auto-assigns
a binding during transpile, while the app addresses the unit through
desktop-GL semantics - the link-time default (0) or glUniform1i, which ES
forbids on image uniforms. Iris/Photon picks image units with glUniform1i,
so its compute passes (auto exposure / colored light) read and wrote the
transpiler-invented units instead: the photon-v1.3b retrace came out dark
and orange-tinted (ssim 0.65 vs golden).

Rewrite every image uniform declaration's binding qualifier to the
frontend-tracked unit (layout binding reflected at link, overridden by any
later glUniform1i) when transpiling for the backend. Flywheel's explicit
bindings rewrite to the same value; Iris packs get the unit the app
actually bound with glBindImageTexture.

Verified on llvmpipe DirectGLES: photon-v1.3b retrace 0.652 -> 0.9988,
photon-v1.1 control stays at 0.9991, all 147 unit tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 09:06:01 +00:00
swung0x48andClaude Fable 5 74ae1e4a29 [Fix] (MG_Test): expect resolved effective offset in DSA VAO test
Since the ARB_vertex_attrib_binding state model (9fbb708e), the flat
VertexAttribute view backends consume holds the resolved effective
offset (binding offset + relative offset), so
glVertexArrayVertexBuffer(offset=16) + glVertexArrayAttribFormat(
relativeoffset=12) yields Offset == 28. The old expectation of 12
encoded the pre-refactor bug where the binding offset was clobbered
by the last call.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 07:41:32 +00:00
swung0x48andClaude Fable 5 139de76347 [Fix] (MG_State/MG_Impl/MG_Backend): render Flywheel instanced+indirect on both backends
Create 6 / Flywheel 1.0.6 now renders correctly with both flywheel:instancing
and flywheel:indirect on DirectGLES and DirectVulkan (verified in-game on
Adreno 830: waterwheels and cogwheels solid, animated, correct pairing, no
crashes across all four combinations).

- MG_State/MG_Impl: sync explicitly-ranged SSBO bindings of FLUSH_EXPLICIT
  persistent maps to the backend before compute dispatches. Flywheel writes
  its scatter-copy descriptors into the staging ring's persistent map and
  never flushes that span (UB per spec, works on drivers whose maps alias
  GPU-visible memory); our maps alias the CPU shadow, so the descriptors
  never reached the GPU: the scatter compute copied nothing (GLES: empty
  draw commands) or stale garbage (Vulkan: wild indirect commands ending in
  VK_ERROR_DEVICE_LOST).
- MG_Impl/MG_Backend: real glFenceSync objects backed by backend fences
  (GLES: native ES syncs guarded by context generation and owner thread;
  Vulkan: buffer-manager frame serials), replacing always-signaled stubs
  that let Flywheel reclaim staging memory the GPU still reads.
- MG_Backend/DirectGLES: compute dispatches now run the same per-program
  resource sync as draws (uniform-block bindings and sampler units must be
  re-established through the API because layout(binding) is stripped from
  transpiled ESSL) and rebind texture units afterwards; the cull shader
  used to read a stale _FlwFrameUniforms binding and the depth-pyramid
  downsample sampled a stale unit-0 texture, zeroing the Hi-Z pyramid and
  occlusion-culling all Flywheel geometry. Image uniforms are excluded from
  glUniform1i (ES bakes their unit via layout(binding)); image-unit sync is
  clamped to the device limit; eliminated/SSBO-classified uniform blocks
  are skipped.
- MG_Backend/DirectGLES: gl_BaseInstance in native indirect draws reads the
  GPU-written command buffer through an injected mg_IndirectParams SSBO
  view addressed per draw instead of the zero CPU shadow; layout(binding)
  is preserved for SSBO/image declarations (ES has no API rebinding for
  them); the ES context ownership claim moved to a global atomic owner
  thread with an EGL ground-truth check, and deferred buffer op state is
  mutex-guarded, so ops cannot silently no-op after context migration.
- MG_Backend/DirectVulkan: new RebaseInstanceIndexPass rewrites vertex
  InstanceIndex loads to (InstanceIndex - BaseInstance). glslang's relaxed
  Vulkan mode aliases gl_InstanceID to InstanceIndex, which includes
  firstInstance, but GL's gl_InstanceID is zero-based - draws with nonzero
  baseInstance paired meshes with wrong instance data (cogwheel drawn as a
  waterwheel, another wheel collapsed invisible). Gated on the
  shaderDrawParameters device feature. Sampled-read barriers additionally
  cover the compute stage (the Hi-Z downsample samples the depth
  attachment from compute), and short uniform-buffer ranges keep the
  existing zero-padding.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 06:30:10 +00:00
swung0x48andClaude Fable 5 2395a6ded2 [Fix] (MG_Backend/DirectGLES): always use native indirect draws with a bound buffer
Adreno (830) exposes no GL_EXT_base_instance, and gating the native path
on it sent Flywheel's whole MDI call to the CPU loop, which reads the
stale shadow instanceCount (0) and draws nothing. A non-zero reserved
word is benign on mobile drivers, instanced arrays were never
baseInstance-offset in the emulation anyway, and the CPU loop can never
see GPU-written commands - native is strictly better.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 13:47:45 +00:00
swung0x48andClaude Fable 5 41b15955b0 [Fix] (MG_Impl): export real glNamedFramebufferTextureLayer
The GLImpl implementation existed but the exported symbol was still a
stub; Flywheel's indirect OIT framebuffer attaches array-texture layers
through it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 13:35:34 +00:00
swung0x48andClaude Fable 5 07055bb531 [Fix] (MG_Backend/DirectGLES): bind GL_DRAW_INDIRECT_BUFFER for native indirect draws
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 13:34:31 +00:00
swung0x48andClaude Fable 5 638999213e [Feat] (MG_Backend/DirectVulkan): native GPU indirect draws + draw parameters
- Enable multiDrawIndirect and shaderDrawParameters device features when
  supported (the latter via VkPhysicalDeviceShaderDrawParametersFeatures on
  Vulkan 1.1+), so DrawIndex/BaseInstance SPIR-V builtins are valid and
  vkCmdDrawIndexedIndirect(Count) may draw more than one command.
- Plain glMultiDrawElementsIndirect no longer requires a GL_PARAMETER_BUFFER
  (it previously drew nothing for the standard Flywheel call); it now issues
  a native vkCmdDrawIndexedIndirect, with a per-command loop fallback when
  the multiDrawIndirect feature is unavailable.
- glDrawElementsIndirect / glDrawArraysIndirect / glMultiDrawArraysIndirect
  read the live GPU buffer via native indirect draws instead of the CPU
  shadow (which cannot see compute-written commands); the CPU path remains
  only for client-memory commands.
- Advertise the same five extensions as DirectGLES for Flywheel's
  capability probe.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 13:33:05 +00:00
swung0x48andClaude Fable 5 5ea49f3c50 [Feat] (MG_Backend/DirectGLES): support Flywheel indirect rendering
- Advertise ARB_gpu_shader5 / ARB_multi_bind / ARB_shading_language_420pack /
  ARB_vertex_attrib_binding / ARB_shader_image_size so LWJGL reports
  SUPPORTS_INDIRECT.
- New LowerDrawParametersPass demotes DrawIndex/BaseInstance/BaseVertex
  builtins to Private globals (mg_DrawID/mg_BaseInstance/mg_BaseVertex) for
  the ESSL transpile; SPIRV-Cross otherwise throws for ES profiles. The
  program manager promotes the emitted globals to uniforms and feeds them
  per (sub-)draw.
- Indirect draws now execute natively on the GPU (glDrawElementsIndirect /
  glDrawArraysIndirect per command) when an indirect buffer is bound, so
  compute-written command fields (Flywheel culling updates instanceCount)
  are honored; detects GL_EXT_base_instance and falls back to the CPU loop
  when the command's baseInstance cannot be consumed natively.
- Sync SSBO binding points for graphics draws, not just compute (Flywheel
  vertex shaders read instance data from SSBOs).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 13:33:05 +00:00
swung0x48andClaude Fable 5 9fbb708e64 [Feat] (MG_State/MG_Impl): ARB_vertex_attrib_binding + ARB_multi_bind state model
Add a separate binding-point model to VertexArrayObject with eager
resolution into the flat per-attribute view backends already consume.
Implements glBindVertexBuffer(s), glVertexAttrib(I)Format,
glVertexAttribBinding, glVertexBindingDivisor and the DSA variants
(glVertexArrayAttribBinding, glVertexArrayBindingDivisor,
glVertexArrayVertexBuffers), fixing glVertexArrayVertexBuffer which
previously conflated binding index with attribute index. Multi-bind
(glBindBuffersBase/Range) loops over the single-bind entry points.

Needed by Flywheel's indirect backend (GlVertexArrayDSA setup path).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 13:32:40 +00:00
swung0x48andClaude Fable 5 f355080b6f [Fix] (MG_Backend/DirectGLES): survive ES context recreation in buffer ops
Track context generation + synced change serial per resource; re-register
ops on MakeCurrent. Fixes frozen buffer contents after the trace replayer's
probe context teardown.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 15:02:38 +08:00
swung0x48andClaude Fable 5 b8ffd25148 [WIP] Mesa-style buffer overhaul: resource abstraction + immediate transfer ops
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 14:47:51 +08:00
swung0x48 2ed96e9678 [Fix] (MG_Backend/DirectVulkan): pad short uniform buffer ranges 2026-07-08 10:50:14 +08:00
swung0x48 fbbf3c4beb [Chore] (trace-replay): document bliss ANGLE workaround 2026-07-08 07:14:45 +08:00
swung0x48 b88066b73b [Fix] (trace-replay): accept Android SDK licenses explicitly 2026-07-08 01:56:45 +08:00
swung0x48 83d475eb02 [Fix] (trace-replay): stabilize ANGLE retrace cases 2026-07-08 01:07:20 +08:00
swung0x48 292576d2a1 [Fix] (MG_Backend/DirectGLES): avoid ANGLE llvmpipe mipmap hangs 2026-07-07 21:19:50 +08:00
swung0x48 0c8af978db [Chore] (MG_Backend): lower format capability logs 2026-07-07 09:58:30 +08:00
swung0x48 23671f1a99 [Chore] (trace-replay): cache Android retrace AVD 2026-07-07 06:30:15 +08:00
swung0x48 94e882762e [Chore] (trace-replay): reduce retrace CI swap 2026-07-07 06:28:15 +08:00
swung0x48 672538f4f1 [Chore] (trace-replay): add swap to retrace CI 2026-07-07 05:21:50 +08:00
swung0x48 a9763639ed [Chore] (trace-replay): add Derivative Android ANGLE golden 2026-07-06 21:12:41 +08:00
swung0x48 616e694bdd [Fix] (MG_Backend/DirectGLES): flush ANGLE memory barriers 2026-07-06 12:39:51 +08:00
swung0x48 0bee379b61 [Fix] (MG_Util/ShaderTranspiler): keep decomposed workgroup types before globals 2026-07-06 03:58:26 +08:00
swung0x48 d35e452368 [Fix] (MG_Util/ShaderTranspiler): rewrite workgroup vec3 composite loads 2026-07-06 01:25:44 +08:00
swung0x48 86f322e252 [Fix] (MG_Util/ShaderTranspiler): keep workgroup vec3 pass no-op clean 2026-07-06 00:25:49 +08:00
swung0x48 b40def47eb [Refactor] (MG_Util/ShaderTranspiler): replace Photon shared vec3 regex hack with SPIR-V pass
Replace the application-specific PackPhotonSharedVec3Memory GLSL regex
patch with a general DecomposeWorkgroupVec3Pass SPIR-V optimization pass.

The new pass decomposes vec3/ivec3/uvec3/bvec3 Workgroup (shared) memory
variables into scalar arrays (e.g. shared vec3 arr[N][M] -> shared float
arr[N][M][3]), rewriting whole-vector loads/stores into per-component
scalar loads/stores. Component-level accesses (e.g. arr[i].x) are
unchanged since a trailing component index into a float[3] yields the
same scalar pointer as it did for a vec3.

Unlike the regex hack, the pass is application-agnostic: it does not
match on variable names, array dimensions, or shader pack identity, and
runs at the SPIR-V level before SPIRV-Cross decompilation.

Registered in SanitizeAndOptimizeBinary after AggressiveDCE so dead
workgroup accesses are already eliminated. Asserts on unsupported
OpAtomic*/OpCopyMemory targeting vec3 workgroup pointers.

Adds ProgramUtilTest.DecomposeWorkgroupVec3InSpirvPass covering array
declaration, +=, whole load/store, component access, and row-copy loop.
2026-07-05 23:48:27 +08:00
swung0x48 93cf3559e1 [Fix] (MG_Backend/DirectGLES): fix Photon image replay 2026-07-05 18:41:32 +08:00
swung0x48 8f947253ad [Chore] (trace-replay): add Minecraft 1.17 menu fixture 2026-07-05 15:18:54 +08:00
swung0x48 8cce59302b [Fix] (MG_Util/ShaderTranspiler): keep GL shaders off Vulkan macro path 2026-07-05 09:15:47 +08:00
swung0x48 74ad6d76eb [Feat] (trace-replay): add macOS window retrace harness 2026-07-04 23:35:27 +08:00
swung0x48 95f2f5bab9 [Fix] (MG_Impl/Getter): don't ask system GL for context flags 2026-07-04 21:14:04 +08:00
swung0x48 e5ef9b2ace [Fix] (trace-replay): avoid Android startup race 2026-07-04 08:55:43 +08:00
swung0x48 0a138276f8 [Fix] (trace-replay): align Android retrace harness 2026-07-04 08:21:46 +08:00
swung0x48 1549598e52 [Fix] (trace-replay): request compatibility EGL profile 2026-07-04 08:20:39 +08:00
swung0x48 1902518cd6 [Fix] (trace-replay): enable Vulkan fallback in APK CI 2026-07-04 07:51:49 +08:00
swung0x48 bcb8a9b57b [Chore] (trace-replay): skip 26.2 cases in CI 2026-07-03 20:42:46 +08:00
swung0x48 bdb276cd68 Revert "[Fix] (MG_Backend/DirectVulkan): separate EGL surface lifecycle"
This reverts commit 45f1a13cc3.
2026-07-03 15:16:46 +08:00
swung0x48 79aa381722 [Fix] (MG_Backend/DirectVulkan, trace-replay): gate R11G11B10F fallback 2026-07-03 13:00:01 +08:00
swung0x48 45f1a13cc3 [Fix] (MG_Backend/DirectVulkan): separate EGL surface lifecycle 2026-07-03 12:20:40 +08:00
swung0x48 afdbf0a194 [Fix] (MG_Backend/DirectVulkan): support layered texture framebuffers 2026-07-03 12:05:20 +08:00
swung0x48 940ab5fd8e [Fix] (MG_State/GLState, MG_Backend): track layered framebuffer textures 2026-07-03 10:38:51 +08:00
swung0x48 6418561d3c [Fix] (MG_State/EGLState, MG_Backend): defer current EGL surface destruction 2026-07-03 10:14:26 +08:00
swung0x48 ce2b5a793f [Fix] (MG_Backend): support GLES depth-stencil readback 2026-07-03 10:02:49 +08:00
swung0x48 6ecefaec75 [Fix] (MG_Backend): track EGL backend surfaces 2026-07-03 09:15:26 +08:00
swung0x48 bdf29fc4d5 [Fix] (MG_Impl/GLImpl): validate DSA texture parameters 2026-07-03 09:05:52 +08:00
swung0x48 51d9fa91ed [Fix] (MG_Impl/GLImpl, MG_Backend): fix piglit texture and buffer cases 2026-07-03 08:51:22 +08:00
swung0x48 5cdc6c902e [Fix] (MG_Impl/EGLImpl, MG_Backend): track EGL surface lifecycle 2026-07-03 07:45:40 +08:00
swung0x48 03696f8a1a [Fix] (MG_Impl/GLImpl, MG_State, MG_Backend): validate image uniform state 2026-07-03 02:03:23 +08:00
swung0x48 7c26ff1b81 [Fix] (MG_Impl/GLImpl): report fragment image uniform limits 2026-07-03 01:32:59 +08:00
swung0x48 d472d8c32e [Fix] (MG_State/EGLState, MG_Impl/GLImpl): report OpenGL context flags 2026-07-03 01:10:37 +08:00
swung0x48 75e5fe1dd3 [Fix] (MG_State/GLState): allow combined texture unit bindings 2026-07-03 00:19:03 +08:00
swung0x48 fb1da4bbd0 [Fix] (MG_Impl/GLImpl, MG_Backend): avoid image uniform piglit traps [skip ci] 2026-07-03 00:02:11 +08:00
swung0x48 ac43c0224c [Fix] (MG_Impl/GLImpl, MG_State/GLState): fix Vulkan depth-only fragment outputs 2026-07-02 23:46:14 +08:00
swung0x48 011b2ad6f8 Revert "[Fix] (MG_Impl/GLImpl): reject default VAO draws [skip ci]"
This reverts commit 632a4f0859.
2026-07-02 22:50:48 +08:00
swung0x48 e83e6ed76e [Fix] (MG_State/GLState, MG_Impl/GLImpl): track immutable texture storage [skip ci] 2026-07-02 22:49:54 +08:00
swung0x48 edec6e4e62 [Fix] (MG_State/GLState, MG_Util/ShaderTranspiler): validate compute local size [skip ci] 2026-07-02 22:18:56 +08:00
swung0x48 cd96db7829 [Chore] (trace-replay): gate retrace jobs on prerequisites 2026-07-02 21:26:38 +08:00
swung0x48 ca38d8fe86 [Chore] (trace-replay): fetch fixture from mirror 2026-07-02 21:12:56 +08:00
swung0x48 f10f7df389 [Chore] (trace-replay): use manifest file to organize retrace fixtures 2026-07-02 20:51:14 +08:00
swung0x48 1cd75f7f54 [Fix] (MG_Impl/GLImpl): validate named renderbuffer storage [skip ci] 2026-07-02 20:03:16 +08:00
swung0x48 1c84422f8e [Fix] (MG_Impl/GLImpl, MG_Backend/DirectVulkan): allow color renderbuffer framebuffers [skip ci] 2026-07-02 19:59:48 +08:00
swung0x48 ba330cd70a [Fix] (MG_Impl/GLImpl): validate read framebuffer buffers [skip ci] 2026-07-02 19:33:10 +08:00
swung0x48 4439162fea [Fix] (MG_Impl/GLImpl): refresh generated mipmap storage [skip ci] 2026-07-02 19:22:39 +08:00
swung0x48 91120d86ba [Fix] (MG_Backend/DirectVulkan): ignore clear buffer without target [skip ci] 2026-07-02 19:16:14 +08:00
swung0x48 71f5ba9601 [Fix] (MG_Impl/GLImpl): handle program resource names [skip ci] 2026-07-02 19:10:50 +08:00
swung0x48 2e14b44349 [Fix] (MG_Impl/GLImpl): validate compute program queries [skip ci] 2026-07-02 19:04:23 +08:00
swung0x48 ac66ea7790 [Fix] (MG_Impl/GLImpl): validate program interface queries [skip ci] 2026-07-02 18:56:30 +08:00
swung0x48 632a4f0859 [Fix] (MG_Impl/GLImpl): reject default VAO draws [skip ci] 2026-07-02 18:51:11 +08:00
swung0x48 e2e4b6e579 [Fix] (MG_Impl/GLImpl): attach depth-stencil renderbuffers [skip ci] 2026-07-02 18:41:08 +08:00
swung0x48 37255523b1 [Fix] (MG_Impl/GLImpl): report incomplete framebuffer attachments [skip ci] 2026-07-02 18:29:26 +08:00
swung0x48 c3a830e9e6 [Fix] (MG_Impl/GLImpl): validate DSA texture unit binding [skip ci] 2026-07-02 18:05:53 +08:00
swung0x48 8266376838 [Fix] (MG_Impl/GLImpl, MG_State/GLState, MG_Backend): fix DSA renderbuffer defaults [skip ci] 2026-07-02 17:55:15 +08:00
swung0x48 d8e3c29744 [Fix] (MG_Impl/GLImpl): ignore legacy texture enable caps [skip ci] 2026-07-02 17:33:00 +08:00
swung0x48 ef3273674b [Fix] (MG_Impl/GLImpl): validate create texture targets [skip ci] 2026-07-02 17:15:19 +08:00
swung0x48 633a25b456 [Fix] (MG_Impl/GLImpl, MG_State/GLState): validate frag data link locations [skip ci] 2026-07-02 16:47:13 +08:00
swung0x48 b9de562491 [Fix] (MG_Impl/GLImpl, MG_State/GLState): fix frag data location queries [skip ci] 2026-07-02 16:20:43 +08:00
swung0x48 76f5a23b7f [Fix] (replay-trace, MG_Impl/GLImpl): fix OpenRA Android retrace 2026-07-02 16:08:43 +08:00
swung0x48 4a3a226f27 [Fix] (replay-trace) document frame-based fixture trimming 2026-07-02 14:51:41 +08:00
swung0x48 ced7f28898 [Fix] (MG_Impl/GLImpl, MG_State/GLState): reject unlinked empty programs [skip ci] 2026-07-02 14:37:29 +08:00
swung0x48 bc2db26e07 [Fix] (MG_Backend/DirectVulkan): validate shader storage block bindings [skip ci] 2026-07-02 14:25:40 +08:00
swung0x48 6649241193 [Fix] (MG_Impl/GLImpl): report compute shader minimum limits [skip ci] 2026-07-02 14:17:16 +08:00
swung0x48 fea8e615f9 [Fix] (MG_Impl/GLImpl): fix indexed shader storage buffer queries [skip ci] 2026-07-02 13:44:11 +08:00
swung0x48 57eb9bd415 [Fix] (MG_Impl/GLImpl): normalize integer texture border colors [skip ci] 2026-07-02 13:33:28 +08:00
swung0x48 0cd236414e [Fix] (MG_Impl/GLImpl, MG_State/GLState): fix texture integer border color queries [skip ci] 2026-07-02 13:17:08 +08:00
swung0x48 e4957e089a [Fix] (MG_State/GLState): ignore inactive frag data bindings [skip ci] 2026-07-02 12:51:51 +08:00
swung0x48 3c643d943a [Fix] (MG_Impl/GLImpl): fix more OpenGL 3.x piglit cases 2026-07-02 12:36:58 +08:00
swung0x48 9b06475811 [Fix] (replay-trace) refresh Minecraft 26.2 main menu fixture 2026-07-02 11:02:46 +08:00
swung0x48 fabae2465b [Fix] (MG_Impl/GLImpl, MG_Backend): fix OpenGL 3.1 piglit cases 2026-07-02 10:34:17 +08:00
swung0x48 233277d94b [Fix] (replay-trace) add Minecraft 26.2 vanilla fixtures 2026-07-02 09:07:20 +08:00
swung0x48 59976a7f7b [Feat] (MG_Impl/EGLImpl): expose surfaceless platform extensions 2026-07-02 08:09:56 +08:00
swung0x48 4613167abb [Fix] (replay-trace) hide removed mod cases in reports 2026-07-01 17:01:03 +08:00
swung0x48 f5f63aa044 [Feat] (replay-trace): add NeoForge earlydisplay fixture 2026-07-01 15:45:37 +08:00
swung0x48 6495c6dad9 [Fix] (replay-trace) fetch CI mod trace fixtures 2026-07-01 13:32:52 +08:00
swung0x48 2d1b8cdd30 [Fix] (replay-trace) run normal-world mod traces in CI 2026-07-01 13:07:35 +08:00
swung0x48 d2cbd2f596 [Fix] (replay-trace) recapture normal-world Fabric mod fixtures 2026-07-01 12:49:56 +08:00
swung0x48 6c7c5a1bc7 [Fix] (MG_Util/ShaderTranspiler): rename "sampler" in parameter name to avoid driver compiler issues 2026-06-30 22:31:49 +08:00
swung0x48 5b116696f0 [Fix] (MG_Impl/EGLImpl, MG_Backend): handle EGL current handoff for NeoForge earlydisplay
Treat valid-display no-surface eglMakeCurrent calls as EGL release requests, keep EGLState and backend current records consistent across threads, and rebind the native DirectGLES EGL context during attach/release.

Add EGLState coverage for cross-thread owner transfer and same-thread release/reattach behavior.
2026-06-30 16:44:38 +08:00
swung0x48 2f1949e093 [Feat] (iOS): support MobileGL builds and Metal surfaces 2026-06-30 07:25:20 +08:00
swung0x48 cca4df17d9 [Fix] (MG_Impl): preserve macOS surface during resize 2026-06-29 21:07:12 +08:00
swung0x48 ef06d90b6b [Fix] (MG_Backend/DirectGLES): record GenerateMipmap errors 2026-06-29 16:51:16 +08:00
swung0x48 e7e6888768 [Fix] (MG_Util/PixelStoreProcessor, MG_Backend/DirectVulkan): normalize packed RGBA uploads 2026-06-29 16:02:27 +08:00
swung0x48 a762346a3b [Fix] (TraceReplay): support local Android retrace reports 2026-06-29 13:53:30 +08:00
swung0x48 d78892cad0 [Fix] (replay-trace): enable GLES mod retraces and crop minimap 2026-06-29 10:17:30 +08:00
swung0x48 3fc6357f28 [Fix] (MG_Impl): handle Retina drawable resize on macOS 2026-06-28 23:22:33 +08:00
swung0x48 65ff056b2e [Docs] add macOS build and use instructions 2026-06-28 21:59:18 +08:00
swung0x48 f121aeb57f [Feat] (MG_Impl): add macOS CGL and NSOpenGL frontends 2026-06-28 20:33:14 +08:00
swung0x48 d61a0b6904 [Fix] (Tools/TraceReplay): show window surface after first present 2026-06-28 15:00:00 +08:00
swung0x48 0ef9c76224 [Feat] (MG_Backend/DirectVulkan): support macOS Metal surfaces 2026-06-28 14:19:51 +08:00
swung0x48 46f3192c67 [CI] add normal-world per-mod retrace fixtures 2026-06-28 13:00:12 +08:00
swung0x48 b2aafc3f95 [CI] wrap retrace summary status counts 2026-06-28 11:36:50 +08:00
swung0x48 531ebb3537 [CI] improve retrace summary mobile rendering 2026-06-28 11:30:17 +08:00
swung0x48 6a11f96a5b [CI] add per-mod common mod retrace fixtures 2026-06-28 10:40:52 +08:00
swung0x48 4fa2e0a58d [CI] improve retrace summary overview 2026-06-28 10:24:32 +08:00
swung0x48 c3d08a2125 [CI] stream retrace summary rendering 2026-06-28 10:24:31 +08:00
swung0x48 56be5318ab [CI] add common mod retrace fixtures 2026-06-28 07:40:31 +08:00
swung0x48 db388e64b9 [Fix] (MG_Backend/DirectVulkan): disable pipeline cache on PowerVR 2026-06-28 01:21:49 +08:00
swung0x48 5af927224f [CI] skip submodules for artifact-only jobs 2026-06-28 00:34:56 +08:00
swung0x48 b425b37e19 [CI] count crashed retrace summaries as failures 2026-06-28 00:18:48 +08:00
swung0x48 0132781fff [CI] isolate retrace summary images by device 2026-06-27 22:57:24 +08:00
swung0x48 9db9513c30 [CI] find retrace summary goldens from fixtures 2026-06-27 21:09:33 +08:00
swung0x48 2f62b90d7b [CI] show GPU names in retrace summary 2026-06-27 21:00:50 +08:00
swung0x48 f68c7296a6 [Fix] (MG_Backend/DirectGLES): clamp UNORM fallback writes 2026-06-27 20:46:02 +08:00
swung0x48 8fd25acbb6 [CI] upload self-contained retrace summary 2026-06-27 20:05:37 +08:00
swung0x48 59c9b94d76 [CI] add Mali main menu golden 2026-06-27 19:46:24 +08:00
swung0x48 d23e08f564 [CI] add remaining retrace fixtures 2026-06-27 19:20:23 +08:00
swung0x48 9a48c3f10c [CI] upload retrace PDFs without archives 2026-06-27 18:01:54 +08:00
swung0x48 790b542163 [CI] upload retrace summary PDFs separately 2026-06-27 17:32:13 +08:00
swung0x48 5032148cf0 [CI] wrap retrace summary counts 2026-06-27 17:27:17 +08:00
swung0x48 1c14c7b3ba [CI] remove retrace missing image count 2026-06-27 15:11:09 +08:00
swung0x48 82fabe90b3 [CI] clarify retrace summary missing assets 2026-06-27 15:08:07 +08:00
swung0x48 acf7341fb8 [CI] add retrace summary artifacts 2026-06-27 14:50:19 +08:00
swung0x48 ac33292e1b [Fix] (MG_Backend/DirectGLES): fix raw depth fetch sampler on GLES 2026-06-27 13:01:52 +08:00
swung0x48 5d6cb7dfed [Fix] (MG_Backend/DirectGLES): fix Super Duper Vanilla on Adreno
- emulate RGB16 SNORM fallback writes
2026-06-27 00:06:13 +08:00
swung0x48 195330ccea [Fix] (MG_Backend/DirectGLES): fix Complementary shaders on Mali
- emulate RGBA8 SNORM fallback writes
2026-06-26 23:33:46 +08:00
swung0x48 f6114c9e15 [CI] keep OpenRA fixture out of LFS 2026-06-26 20:15:50 +08:00
swung0x48 d2f2a0039f [Fix] (trace-replay): shorten iterationT fixtures 2026-06-26 15:58:53 +08:00
swung0x48 943600edb2 [Feature] (MG_Backend): log format caveat fallbacks 2026-06-26 14:49:51 +08:00
swung0x48 76acae9889 [Fix] (MG_Backend/DirectGLES): make format caveats probe-driven 2026-06-26 14:12:24 +08:00
swung0x48 9cca0a8753 [Feature] (MG_Backend): print format capability tables 2026-06-26 10:44:55 +08:00
swung0x48 8fe8d096fb [CI] fetch trace fixtures on demand 2026-06-25 23:50:24 +08:00
swung0x48 496fa50a23 [Fix] (MG_Impl/GLImpl): report backend format capabilities 2026-06-25 23:25:31 +08:00
swung0x48 9137396eae [Fix] (trace-replay): render DirectGLES replay onscreen by default 2026-06-23 21:42:24 +08:00
swung0x48 2f52264f01 [Fix] (trace-replay): use SSIM for golden validation 2026-06-23 21:26:55 +08:00
swung0x48 f0f6d1e5fa [Fix] (MG_Backend/DirectVulkan): fix prerotated default framebuffer 2026-06-23 20:00:43 +08:00
swung0x48 18d19a9a8b [Fix]: fix Complementary
- add a DirectGLES ANGLE fallback control for 8-bit SNORM texture formats

- normalize SNORM8 textures to float storage so ANGLE can render Complementary intermediate framebuffers

- reuse the normalized upload conversion path for SNORM8 and existing norm16 float fallbacks
2026-06-23 07:37:25 +08:00
swung0x48 3f53041ed2 [Fix]: fix BSL
- keep ANGLE RGBA16 textures on the native norm16 path

- add separate RGB16 and SNORM16 fallback controls for DirectGLES format normalization

- convert RGB16 fallback uploads to float when using RGB32F storage
2026-06-22 23:00:15 +08:00
swung0x48 c08ac7db72 [Fix]: fix Sundial Lite
- detach source texture from synced GLES framebuffers before mipmap generation

- bind a complete scratch framebuffer while calling glGenerateMipmap

- force ANGLE norm16 texture fallback and convert 16-bit normalized uploads

- raise Sundial Lite retrace tolerance for software DirectGLES validation
2026-06-22 22:07:06 +08:00
swung0x48 7419f62159 [Fix] (trace-replay): avoid attached FBO during GLES mipmap generation 2026-06-22 19:39:25 +08:00
swung0x48 252e59334d [Fix] (trace-replay): pass ANGLE path to APK retrace 2026-06-22 12:30:10 +08:00
swung0x48 565dc90bf0 [Fix] (trace-replay): use ANGLE for DirectGLES APK retrace 2026-06-22 12:18:33 +08:00
swung0x48 84eddaef2f [Fix] (ci): use software GPU for GLES APK retrace 2026-06-22 10:17:00 +08:00
swung0x48 641bfb1dd9 [Docs] (trace-replay): add mismatch retrace debugging guide 2026-06-22 09:48:49 +08:00
swung0x48 b87b698148 [Fix] (ci): cache native compilation 2026-06-22 09:24:21 +08:00
swung0x48 dac5f8964f [Chore] (ci): Use supported GPU settings 2026-06-22 08:49:27 +09:00
swung0x48 85ffcb74d8 [Fix] (ci): ignore optional trace artifact copy failures 2026-06-21 01:58:27 +08:00
swung0x48 9e719461e2 [Fix] (ci): fail fast on trace replay process exit 2026-06-21 00:59:36 +08:00
swung0x48 f270988e03 [Fix] (ci): harden Android trace replay CI 2026-06-21 00:07:13 +08:00
swung0x48 b4f9401395 [Fix] (ci): use ANGLE for APK retrace 2026-06-21 00:07:12 +08:00
swung0x48 a1e2007b82 [Fix] (trace-replay): support alternate golden images
- compare actual output against primary and alternate golden images

- record the matched golden path in trace replay results

- allow APK and Linux retrace fixtures to pass alternate golden paths

- keep nostalgia validation accepting both Mesa and PC goldens
2026-06-20 19:04:52 +08:00
swung0x48 e92a57011f [Fix] (ci): share AVD home across runner steps 2026-06-20 18:27:42 +08:00
swung0x48 3736e1fc38 [Fix]: fix Vulkan depth mipmap fallback
- add shader fallback for depth-only mipmap generation when format blit is unsupported

- choose native blit or shader path from Vulkan format features

- clean up temporary depth mipmap render resources per frame
2026-06-20 18:10:13 +08:00
swung0x48 2660e1669c [Fix] (ci): resolve Android SDK tools in AVD runner 2026-06-20 17:55:57 +08:00
swung0x48 be6818effe [Fix] (trace-replay): relax fixture tolerance 2026-06-20 17:15:24 +08:00
swung0x48 a49a463acf [Fix] (ci): split APK retrace AVD lifecycle
- expose AVD create, launch, retrace, diagnostics, and stop as separate workflow steps

- add bounded waits for emulator adb connection, boot, and trace replay execution

- collect emulator diagnostics for APK retrace artifacts
2026-06-20 14:27:11 +08:00
swung0x48 317b3602c3 [Fix] (ci): fix matrix retrace GLES runtime
- install EGL/GLES development runtime in retrace matrix jobs

- assert libEGL.so and libGLESv2.so are available before running trace replay

- remove native build cache wiring from workflows
2026-06-20 12:40:43 +08:00
swung0x48 b637962da0 [Chore] (ci): cache native builds 2026-06-20 12:15:34 +08:00
swung0x48 44f9dcb3a2 [Feat] (trace-replay): run retrace fixtures as matrix jobs 2026-06-20 11:44:41 +08:00
swung0x48 1a2b337618 [Fix] (trace-replay): fix APK retrace CI shell execution 2026-06-20 11:00:16 +08:00
swung0x48 d219ac3f6f [Docs] (trace-replay): add trace fixture authoring guide 2026-06-20 10:51:36 +08:00
swung0x48 1a2f867ba9 [Feat] (trace-replay): run all fixtures in APK retrace CI 2026-06-20 09:40:15 +08:00
swung0x48 0329f40df5 [Feat] (trace-replay): add Minecraft Sundial Lite shader fixture 2026-06-20 07:10:26 +08:00
swung0x48 193204de52 [Feat] (trace-replay): add Minecraft Derivative Main shader fixture 2026-06-20 06:54:40 +08:00
swung0x48 3f945dcfeb [Feat] (trace-replay): add Minecraft Photon v1.3b shader fixture 2026-06-20 06:43:04 +08:00
swung0x48 2f9ccb84d9 [Feat] (trace-replay): add Minecraft Super Duper Vanilla shader fixture 2026-06-20 06:32:25 +08:00
swung0x48 f10ea4ebcd [Chore] (trace-replay): remove local progress notes 2026-06-20 06:08:18 +08:00
swung0x48 ada6923039 [Fix] (trace-replay): fix Nostalgia golden alpha
- strip alpha from the Linux Mesa golden image
2026-06-20 05:59:43 +08:00
swung0x48 f4cf398651 [Feat] (trace-replay): validate remaining Minecraft shader fixtures
- register Chocapic with a Linux Mesa golden

- register Nostalgia with a Linux Mesa golden

- register Photon shader fixture
2026-06-20 05:44:43 +08:00
swung0x48 84dba77275 [Fix]: fix Bliss
- enable glslang NaN min/max/clamp semantics

- register Bliss retrace fixture

- track remaining fixture validation queue
2026-06-19 21:20:02 +08:00
swung0x48 19e4ba386d [Fix]: fix Chocapic V6 Lite
- prune unused SPIR-V interface variables before GLES transpilation

- remap shader varyings through glslang IO resolver bindings

- initialize opaque uniforms from explicit sampler bindings only

- avoid side effects in texture binding assertions

- register Chocapic V6 Lite retrace fixture
2026-06-19 18:42:32 +08:00
swung0x48 7d101182cd [Feat] (trace-replay): validate Minecraft Complementary Unbound shader fixture 2026-06-19 10:50:40 +08:00
swung0x48 e60b044ff7 [Fix] (trace-replay): validate Complementary Reimagined shader fixture
- clear stale temporary framebuffer attachments before DirectGLES color/depth blits

- add Complementary Reimagined in-world trace to retrace validation
2026-06-19 10:28:47 +08:00
swung0x48 4366909cf0 [Feat] (trace-replay): validate Minecraft iterationT no-DSA shader fixture 2026-06-19 09:25:20 +08:00
swung0x48 7b1d8ce9dd [Feat] (trace-replay): validate Minecraft iterationT shader fixture 2026-06-19 09:14:33 +08:00
swung0x48 94fdeb633f [Fix] (trace-replay): relax Minecraft BSL tolerance 2026-06-19 08:21:50 +08:00
swung0x48 50eda634e0 [Fix]: fix iterationT
- implement glCopyImageSubData frontend export and backend dispatch

- add DirectGLES depth-only CopyImageSubData via framebuffer depth blit

- add DirectGLES R32F CopyImageSubData fallback for GLES drivers rejecting native copy

- allocate DirectGLES generated mipmap storage for depth-only and R11FG11FB10F manual generation

- generate DirectGLES depth-only mipmaps with explicit depth blits

- generate DirectGLES R11FG11FB10F mipmaps with explicit color blits

- add DirectVulkan CopyImageSubData with explicit image copy and layout transitions

- use native Vulkan blit for depth-only mipmap generation

- remove unused Vulkan depth mipmap shader fallback path
2026-06-19 08:14:57 +08:00
swung0x48 2bf75537d8 [Feat] (trace-replay): add Minecraft iterationT no-DSA shader fixture 2026-06-18 21:09:23 +08:00
swung0x48 f780736f29 [Feat] (trace-replay): add Minecraft Nostalgia shader fixture 2026-06-18 08:04:22 +08:00
swung0x48 85e71622f8 [Feat] (trace-replay): add Minecraft Complementary Unbound shader fixture 2026-06-18 07:29:10 +08:00
swung0x48 ea02a2fac9 [Feat] (trace-replay): add Minecraft Complementary Reimagined shader fixture 2026-06-18 07:01:26 +08:00
swung0x48 8368901bf9 [Feat] (trace-replay): add Minecraft Chocapic shader fixture 2026-06-18 06:30:36 +08:00
swung0x48 a18170cdb1 [Feat] (trace-replay): add Minecraft Bliss shader fixture 2026-06-18 06:24:07 +08:00
swung0x48 e70fb39cb9 [Feat] (trace-replay): add Minecraft Photon shader fixture 2026-06-18 06:13:22 +08:00
swung0x48 e7ba689ca2 [Feat] (trace-replay): add Minecraft iterationT shader fixture 2026-06-18 05:42:48 +08:00
swung0x48 78dcf43c72 [Feat] (trace-replay): add Minecraft Mellow shader fixture 2026-06-17 23:51:17 +08:00
swung0x48 70951f46e5 [Feat] (trace-replay): add Minecraft MakeUP shader fixture 2026-06-17 20:09:21 +08:00
swung0x48 a6b4c4b049 [Feat] (trace-replay): add Minecraft BSL shader fixture 2026-06-17 17:52:30 +08:00
swung0x48 db3d569ed0 [Feat] (trace-replay): add Minecraft Sodium in-world fixture 2026-06-17 16:38:12 +08:00
swung0x48 ec0a1a0b70 [Fix] (ci): fetch trace fixtures from LFS 2026-06-17 14:11:28 +08:00
swung0x48 42e3cce8c3 [Fix] (ci): force info MobileGL log level 2026-06-17 13:59:38 +08:00
swung0x48 c82062a51b [Fix] (MobileGL): restore default log level 2026-06-17 13:50:48 +08:00
swung0x48 61349ac0c2 [Fix] (trace-replay): shorten Minecraft in-world fixture 2026-06-17 12:34:32 +08:00
swung0x48 102bd2cfd2 [Fix] (MG_Backend/DirectVulkan): support Sundial Lite shader pack 2026-06-17 11:39:20 +08:00
swung0x48 e7bb46e819 [Fix] (MG_Backend/DirectGLES): support Sundial Lite shader pack 2026-06-17 10:58:12 +08:00
swung0x48 02f8c7ab56 [Fix] (ci): use ANGLE for Android Vulkan retrace 2026-06-17 07:51:35 +08:00
swung0x48 96ecabc38b [Fix] (trace-replay): write opaque Android actual images 2026-06-17 07:26:15 +08:00
swung0x48 57600f2bd3 [Fix] (ci): use host GPU for Android Vulkan retrace 2026-06-17 06:59:15 +08:00
swung0x48 a8628feb3c [Fix] (ci): use non-deprecated Android emulator GPU 2026-06-17 06:14:22 +08:00
swung0x48 e2695235ab [Fix] (ci): allow Android Vulkan in-world variance 2026-06-17 05:41:29 +08:00
swung0x48 44c3ea7844 [Feat] (trace-replay): add Minecraft in-world fixture 2026-06-17 00:05:09 +08:00
swung0x48 7c81906459 [Fix] (ci): run all Android retrace fixtures 2026-06-16 20:36:53 +08:00
swung0x48 4e3679c7b7 [Fix] (ci): validate debuggable retrace APKs 2026-06-16 19:31:34 +08:00
swung0x48 19c0ae4e33 [Fix] (ci): make retrace APK debuggable 2026-06-16 18:09:46 +08:00
swung0x48 565649e3b6 [Fix] (ci): split APK retrace validation steps 2026-06-16 17:54:40 +08:00
swung0x48 527ef9653a [Fix] (ci): run APK retrace from parameterized script 2026-06-16 17:47:01 +08:00
swung0x48 23bb5245bd [Fix] (ci): run APK retrace script with bash 2026-06-16 17:04:57 +08:00
swung0x48 e3c4b94b11 [Fix] (ci): package retrace APK for all ABIs 2026-06-16 14:36:17 +08:00
swung0x48 3fa89640cd [Fix] (ci): split APK retrace job 2026-06-16 14:10:55 +08:00
swung0x48 722bddf916 [Feat] (trace-replay): test retrace APK on Android 2026-06-16 14:07:33 +08:00
swung0x48 294dad1773 [Fix] (ci): rename test workflow 2026-06-16 13:21:37 +08:00
swung0x48 b33ae1c481 [Feat] (trace-replay): add Minecraft main menu trace 2026-06-16 13:00:01 +08:00
swung0x48 0dc3a0916e [Feat] (trace-replay): add Minecraft startup trace 2026-06-16 12:22:27 +08:00
swung0x48 24372c8558 [Fix] (ci): rename retrace artifact 2026-06-16 11:06:40 +08:00
swung0x48 49faa5e749 [Fix] (ci): package existing Linux runtime files 2026-06-16 10:11:55 +08:00
swung0x48 69a3985f9d [Fix] (ci): reuse MobileGL artifact for retrace 2026-06-16 09:32:53 +08:00
swung0x48 d335131f52 [Fix] (ci): restore isolated retrace job 2026-06-16 09:14:53 +08:00
swung0x48 fe1b6db653 [Feat] (ci): reuse build outputs across jobs 2026-06-16 08:46:43 +08:00
swung0x48 4c2c4cb565 [Fix] (trace-replay): stabilize GL4ES OpenRA validation 2026-06-15 22:27:39 +08:00
swung0x48 bc8109b695 [Fix] (trace-replay): fix Android Vulkan pbuffer build 2026-06-15 21:01:55 +08:00
swung0x48 a88ca75c14 [Fix] (trace-replay): run Vulkan retrace headlessly 2026-06-15 20:37:40 +08:00
swung0x48 e453a75ea9 [Fix] (trace-replay): validate OpenRA on both backends 2026-06-15 19:59:21 +08:00
swung0x48 627f737bbe [Fix] (trace-replay): checkout nested submodules in CI 2026-06-15 19:35:48 +08:00
swung0x48 ef3e8da1b5 [Feat] (trace-replay): add Linux retrace CI 2026-06-15 18:08:55 +08:00
swung0x48 c2d6134cfa [Feat] (android-plugin): add standalone trace replay profile 2026-06-15 16:57:50 +08:00
swung0x48 d51723c153 [Fix] (MG_Backend/DirectGLES): use safer buffer upload path. MAY AFFECT PERFORMANCE!! 2026-06-11 10:28:39 +08:00
swung0x48 94b2a863bf [Chore] (MG_Impl/GLSync): stub out gl sync 2026-06-11 09:50:20 +08:00
swung0x48 442510f793 [Fix] (MG_Impl/GLImpl): split backend-dependent integer getters 2026-06-11 07:18:29 +08:00
swung0x48 75a0fc2c0c [Fix] (MG_Impl/GLImpl): fix DSA state queries and compatibility tests
Fix texture parameter getters and element array buffer binding queries.
Update framebuffer, texture, program, and VAO tests to match current OpenGL semantics, while preserving VAO 0 compatibility behavior.
2026-06-10 23:33:59 +08:00
swung0x48 72b1f50314 [Fix] (MG_Backend/DirectGLES): don't glFlush on present 2026-06-10 18:44:59 +08:00
swung0x48 66c36cd945 Merge pull request #9 from BZLZHH/Agent/CodexAudit 2026-06-10 12:33:01 +08:00
BZLZHH d091d9c460 [Misc] (...): Remove SOURCE_AUDIT.md 2026-06-10 09:57:07 +08:00
BZLZHH d3a2e2e666 [Chore] (...): Disable debug log. 2026-06-10 09:52:30 +08:00
BZLZHH a4261f8b19 [Fix] (MG_Backend/DirectVulkan, MG_State/TextureState, MG_Test): harden default-fbo clear lifetime tracking 2026-06-10 09:49:16 +08:00
BZLZHH 6051588458 [Feat] (MG_Backend/DirectVulkan): implement multisample texture backend 2026-06-10 01:56:37 +08:00
BZLZHH 5c1fd733da [Feat] (MG_Backend/DirectGLES): implement multisample texture backend 2026-06-10 01:41:44 +08:00
BZLZHH c499480692 [Feat] (MG_Impl/Texture, MG_State/TextureState, MG_Impl/Framebuffer, MG_Test/Texture): implement multisample frontend state 2026-06-10 01:37:59 +08:00
BZLZHH 3da0b1dfd4 [Fix] (MG_Backend/DirectVulkan, MG_Impl/Texture, MG_Test/Texture): fill advertised extension gaps 2026-06-10 01:09:39 +08:00
BZLZHH 720919455f [Fix] (MG_Backend/DirectGLES, MG_Impl/Texture, MG_Test/Texture): fix Voxy base-instance and bound texStorage2D 2026-06-10 00:07:20 +08:00
BZLZHH df90753d85 [Fix] (MG_Impl/Framebuffer, MG_Test/Framebuffer): avoid duplicate glFramebufferTexture attach 2026-06-09 22:18:28 +08:00
BZLZHH 7bee645b9a [Fix] (MG_Util/Texture): remove duplicate depth-stencil normalize case 2026-06-09 21:33:27 +08:00
BZLZHH 83a6f24f93 Merge remote-tracking branch 'origin/Feat/Backend-Direct-Vulkan' into Agent/CodexAudit
# Conflicts:
#	MobileGL/MG_Backend/BackendObject.h
#	MobileGL/MG_Backend/DirectGLES/Managers.cpp
#	MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp
#	MobileGL/MG_Backend/DirectVulkan/Renderer/FrameContext.cpp
#	MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp
#	MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp
#	MobileGL/MG_Impl/GLImpl/Buffer/GL_Buffer.cpp
#	MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp
#	MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp
#	MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp
#	MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.h
#	MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp
#	MobileGL/MG_Impl/GLImpl/Program/GL_Program.h
#	MobileGL/MG_Impl/GLImpl/Sync/GL_Sync.cpp
#	MobileGL/MG_Impl/GLImpl/Sync/GL_Sync.h
#	MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp
#	MobileGL/MG_Impl/GLImpl/VertexArray/GL_VertexArray.cpp
#	MobileGL/MG_Impl/GLImpl/VertexArray/GL_VertexArray.h
#	MobileGL/MG_State/GLState/BufferState/BufferObject.cpp
#	MobileGL/MG_State/GLState/BufferState/BufferObject.h
#	MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
#	MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.h
2026-06-09 21:18:12 +08:00
BZLZHH 8d35be14a0 [Fix] (MG_Backend/DirectGLES, MG_Util/BackendLoader): stop treating partial EGL/GLES symbol load as fatal 2026-06-09 19:57:30 +08:00
swung0x48 85bd0613ca [Fix] (MG_Backend/DirectGLES): support Voxy rendering
Implemented:

- Advertise Voxy-required DirectGLES extensions without raising the reported OpenGL version.

- Add DirectGLES multi draw indirect count emulation and preserve GL draw indirect baseInstance semantics on GLES.

- Add DirectGLES DSA framebuffer clear/blit paths used by Minecraft and Voxy presentation.

Fixed:

- Rewrite gl_BaseInstance in DirectGLES vertex shaders and provide a backend uniform for indirect draw emulation.

- Materialize framebuffer attachment textures during DirectGLES FBO sync so named framebuffer operations do not desync backend attachment state.

- Avoid redundant texture buffer rebinding and handle texture buffers without bound storage during backend sync.

Tests:

- Add MG_Test coverage for DirectGLES Voxy extension advertising, baseInstance shader rewriting, and DSA named framebuffer clear/blit backend wiring.
2026-06-09 17:20:23 +08:00
BZLZHH 727939af5b [Fix] (MG_Impl, MG_State, MG_Backend, MG_Util): Do source audit by Codex. 2026-06-09 15:34:19 +08:00
swung0x48 dd52f0381a [Fix] (MG_Backend/DirectVulkan): fix Voxy subgroup and indirect draw sync
- Implement Vulkan subgroup capability querying and expose KHR subgroup getter values.

- Fix DirectVulkan memory barriers so GL_COMMAND_BARRIER_BIT makes generated indirect draw commands visible.

- Keep Voxy on the DirectVulkan gpu_shader_int64 quad decode path while filtering unsupported optional int64 usage on backends that do not advertise it.

- Add MG_Test coverage for subgroup getters, Voxy subgroup/int64 shader probes, command barrier mapping, and indirect draw command layout.

- Check for whether driver supports shader subgroup operation, disable on demand, and provide env var `MOBILEGL_DISABLE_SUBGROUP` to explicitly disable subgroup features
2026-06-09 09:46:07 +08:00
swung0x48 cf165c0db5 [Fix] (MG_Backend/DirectVulkan): support Voxy rendering
Implemented:

- Advertise Voxy-required DirectVulkan extensions without raising the reported OpenGL version.

- Add DirectVulkan compute, indirect draw count, DSA, readback, and buffer state paths needed by Voxy.

Fixed:

- Enable Vulkan shaderInt64 and drawIndirectFirstInstance so Voxy baseInstance-driven LOD draws address the correct section data.

- Fix DirectVulkan synchronization, framebuffer, texture readback, and shader interface handling used by Voxy and Minecraft screenshots.

Tests:

- Add MG_Test coverage for DirectVulkan extension advertising, DSA buffer/texture/framebuffer/vertex-array behavior, persistent mapped readback, and shader/program paths.
2026-06-09 00:37:37 +08:00
swung0x48 ab9db43599 [Chore]: bump version to 26.06 2026-06-08 14:11:20 +08:00
swung0x48 d553e363a7 [Feat] (MG_Impl/GL_Buffer, MG_State/BufferState, MG_Backend): implement persistent mapping 2026-06-08 14:08:35 +08:00
swung0x48 be3c3eb9bb [Fix] (MG_Test/VertexArray): lossen too strict error test 2026-06-08 09:43:24 +08:00
swung0x48 357807666d [Fix] (MG_Backend/DirectVulkan, MG_Backend/DirectGLES): fix vulkan program
cache, get EGLSurfaceSize on viewport = 0
2026-06-08 05:41:38 +08:00
BZLZHH d1a5a4e39c [Fix] (MG_Util/BackendLoader): Add /usr/lib64 and /lib64 to library search paths for Fedora/RHEL compatibility. 2026-06-07 20:28:19 +08:00
swung0x48 a701c896f0 [Feat] (MG_Backend/DirectVulkan): wire up client-side buffer 2026-06-07 18:05:48 +08:00
swung0x48 b0de886f8e [Fix] (MG_Backend/DirectVulkan): make glmark2 work on Magma 2026-06-07 16:24:30 +08:00
swung0x48 ad1ca4ea92 [Feat] (MG_Impl/VertexArray): implement client-side buffer 2026-06-07 13:02:58 +08:00
swung0x48 fbaf5261e2 [Fix]: properly open X11 display for rendering 2026-06-07 12:26:35 +08:00
swung0x48 19ada4b8f9 [Fix]: fix glmark2 crash
- deal with legacy GLSL syntax (attribute/varying/gl_FragColor/texture2D/etc.)
- implement glGet GL_SHADER_SOURCE_LENGTH, and make sure returns
  original shader source
- expose proper extensions (GL_ARB_depth_texture)
- support env var MOBILEGL_LOG_FILE_PATH
- unit tests to test against those changes
2026-06-07 11:08:38 +08:00
swung0x48 ff41e59282 [Feat] (EGL): support Linux X11 + EGL 2026-06-07 08:21:20 +08:00
swung0x48 a15a13ca46 [Fix]: fix compilation on Linux 2026-06-06 23:18:27 +08:00
swung0x48 2937043e77 [Fix] (MG_Backend/DirectGLES): mark integer varyings flat 2026-06-06 18:58:27 +08:00
swung0x48 24efb0bb17 [Fix] (MG_Impl/Program): implement direct state uniform updates 2026-06-06 17:27:09 +08:00
swung0x48 dd2bd267c1 [Fix] (MG_State/RenderState): track front face mode 2026-06-06 08:46:43 +08:00
swung0x48 5cfe9c8998 [Feat] (MG_Backend/DirectVulkan): support compute shaders 2026-06-05 11:52:10 +08:00
swung0x48 3bd8a62aa8 [Feat] (MG_Backend/DirectGLES): support compute shaders 2026-06-05 10:41:23 +08:00
swung0x48 dcd37f3c38 [Fix] (MG_Util/ShaderTranspiler): preserve reflected uniform backing 2026-06-05 10:39:53 +08:00
swung0x48 a579fd2342 [Feat] (MG_State/ProgramState): attempting to do shader LTO (WIP) 2026-06-01 22:26:16 +08:00
swung0x48 f93ff00642 [Chore] (MG_State/ProgramState): add more remarks to GenerateBinary() 2026-06-01 21:35:28 +08:00
swung0x48 8a628631fb [Feat] (Backend/DirectVulkan): implement min/max lod
- This fixes water waves in Derivative d24.4.14
2026-06-01 05:38:28 +08:00
swung0x48 59733a2a26 [Fix] (MG_Backend/DirectVulkan): clear alpha as 1.0f when using RGB format on GL side. Transition to VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL for clearing 2026-05-24 19:23:36 +08:00
swung0x48 c7d385c03d [Fix] (CI): scan rather than hardcode apk file name 2026-05-24 18:45:24 +08:00
swung0x48 27a8aa057c [Feat] (MG_Impl/RenderState): implement some blend related states 2026-05-24 17:38:44 +08:00
swung0x48 e63bfbabf0 [Chore] (MG_Backend/DirectVulkan): enable vulkan validation layer only on DEBUG log level 2026-05-24 11:11:12 +08:00
swung0x48 c107d9edcf [Fix] (MG_Backend/DirectVulkan): use RGBA format as RGB format
- some drivers (Adreno as I tested) lacks
  VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BLEND_BIT on 24-bit RGB formats,
  use 32-bit ones as a fallback.
2026-05-24 10:07:28 +08:00
swung0x48 49e69c1c7d [CI] (android-plugin): build two (Espryt/Magma) variants 2026-05-24 08:57:02 +08:00
swung0x48 482b6d7bbf [CI] (android-plugin): specify MOBILEGL_LOG_ACTIVE_LEVEL=MOBILEGL_LOG_LEVEL_INFO define 2026-05-24 08:45:02 +08:00
swung0x48 dcfe91bdfc [Optimization] (MG_Backend/DirectVulkan): properly use vkCmdBlitImage() on rotations that
applicable, rather than shader-based blit
2026-05-24 00:14:05 +08:00
swung0x48 6072b5b703 [Fix] (MG_Backend/DirectVulkan): use shader-based default framebuffer blit to fix wrong orientation 2026-05-24 00:07:16 +08:00
swung0x48 9c50ac8a69 [CI]: use DirectVulkan as default backend 2026-05-23 23:01:48 +08:00
swung0x48 8b7acad257 [CI]: add android build key + signing process 2026-05-23 21:54:40 +08:00
swung0x48 cb8cb48a60 [CI]: build MobileGL android renderer plugin 2026-05-23 20:58:03 +08:00
swung0x48 8c00ae1d38 [Fix] (MG_Impl/GL_Texture): don't actually allocate large proxy texture to avoid large allocation makes some devices to crash 2026-05-12 16:33:30 +08:00
swung0x48 633b3d3b0a [Feat] (MG_State/RenderState, MG_Backend/DirectVulkan): make Distant
Horizon work
- Implement BlendEquation/CullFaceMode/PointSize/PolygonMode
- Implement GetFramebufferAttachmentParameter*
- Downgrade some color attachment resolve failure
- Downgrade some overly-strict shader stage linkage check (don't check
  on unused input var)
2026-05-12 12:38:19 +08:00
swung0x48 60199c0323 [Optimization] (MG_Backend/DirectVulkan): optimize CreatePipeline hot path with VkPipelineCache 2026-05-12 00:27:52 +08:00
swung0x48 9e0e03d4c0 [Optimaization] (MG_Backend/DirectVulkan): optimize perf
- Properly reuse descriptor sets rather than always allocating
- Add program lookup cache
2026-05-11 23:20:55 +08:00
swung0x48 4972ebf914 [Fix] (MG_Backend/DirectVulkan): fix 1.21.6+ intermittent crashing
- Try coerce vertex input format to please Vulkan driver
- Skip inactive UBO instead of hard fast-fail
2026-05-10 11:53:26 +08:00
swung0x48 4a36d63c1b [Fix] (MG_Backend/DirectVulkan): fix 26.2 texture bug.
- Fix texture object / buffer lifecycle issues
2026-05-10 10:03:03 +08:00
swung0x48 d7e768096e [Fix] (MG_Backend/DirectVulkan): fix Minecraft 26.2 startup crashes
- Support cube map face uploads with cube-compatible images and per-face array layers
- Add uniform texel buffer descriptor support for samplerBuffer bindings
- Cache transient vertex/index buffer uploads per frame to avoid VMA allocation failures
2026-05-10 08:50:55 +08:00
swung0x48 e13b5d0618 [Chore] (MG_Backend/DirectVulkan): make Photon fix a SPIR-V patch rather than source-level 2026-05-09 17:46:16 +08:00
swung0x48 c5bf0dc07d [Chore] (MG_Backend/DirectVulkan): exclude some validation logic from release build 2026-05-09 13:45:19 +08:00
swung0x48 9fbd83d602 Merge branch 'Feat/Backend-Direct-Vulkan' of https://github.com/MobileGL-Dev/MobileGL into Feat/Backend-Direct-Vulkan 2026-05-09 13:08:12 +08:00
swung0x48 39c5b28dfb [Fix] (MG_Backend/DirectVulkan): fixing Photon v1.1
- Flatten DailyWeatherVariation interface varyings
- Correct internal-format component counts
- Preserve GL draw-buffer slot semantics in render pass creation
- relax GL_NONE / vec4-to-RGB pipeline checks
2026-05-09 09:56:45 +08:00
BZLZHH be533994e5 Merge branch 'dev' into Feat/Backend-Direct-Vulkan 2026-05-09 08:32:33 +08:00
BZLZHH 368c089172 Merge branch 'Perf/Improvement1' into dev 2026-05-09 08:31:36 +08:00
swung0x48 407d4344c4 [Feat] (MG_Backend/DirectVulkan): generate mipmap, add fallback texture, implement shader-based depth mipmap blit, enumerate more vk physical device features
- Supporting iterationT
2026-05-08 09:25:41 +08:00
swung0x48 76e3950028 [Feat] (MG_Backend/DirectVulkan): GenerateMipmap WIP 2026-05-07 15:14:25 +08:00
swung0x48 7337b00ca8 [Feat] (MG_Backend/DirectVulkan): implement 3D texture handling 2026-05-07 12:28:14 +08:00
swung0x48 22582dea3e [Chore] (MG_Backend/DirectVulkan): get rid of 1x1 texture fallback path for unbinded sampler 2026-05-07 09:30:10 +08:00
swung0x48 f600a07404 [Fix] (MG_Backend/DirectVulkan): use fallback texture in case some
shaderpack failes to properly bind texture
2026-05-06 17:57:59 +08:00
swung0x48 ff790e1ff1 [Feat] (MG_Backend/DirectVulkan): implement vulkan backend for glCopyTex(Sub)Image2D 2026-05-06 15:39:59 +08:00
swung0x48 a345369269 [Fix] (MG_Backend/DirectVulkan): fix null dereference crash in VkClearManager 2026-05-06 13:15:08 +08:00
swung0x48 8e4a4359b4 [Chore] (MG_Backend/DirectVulkan): eliminate vague texture binding fallbacks 2026-05-05 18:34:44 +08:00
swung0x48 0a000d1628 [Feat] (MG_Backend/DirectVulkan): more built-in function renames 2026-05-05 18:33:20 +08:00
swung0x48 5bd8e369c4 [Feat] (MG_Backend/DirectVulkan): advertise GL_ARB_draw_buffers_blend for DirectVulkan backend 2026-05-05 18:02:20 +08:00
swung0x48 777d756d1b [Fix] (MG_Impl/GL_Texture): properly unbind texture when texture name == 0 2026-05-05 17:23:57 +08:00
swung0x48 7716a8e05d [Fix] (MG_Backend/DirectVulkan): more flexible ResolveSamplerDescriptor 2026-05-05 17:13:35 +08:00
swung0x48 94e93b171c [Feat] (MG_Backend/DirectVulkan): get real maxProgramBindings from VkDevice 2026-05-05 13:19:45 +08:00
swung0x48 1bf12be278 [Fix] (MG_Util/ShaderTranspiler): add preprocessing to remove name-collided glsl functions 2026-05-05 12:58:36 +08:00
swung0x48 99b753be9a [Feat] (MG_Backend/DirectVulkan): implement backend func for glCopyTex(Sub)Image2D 2026-05-05 12:02:33 +08:00
swung0x48 bc2f2d896b [Chore]: bump version to 26.05 2026-05-05 09:12:01 +08:00
swung0x48 659fbf26da [Fix] (MG_Backend/DirectVulkan): fix depth sampler state, fixing BSL
shadow
2026-05-05 00:40:17 +08:00
swung0x48 f61a7bb9c0 [Fix] (MG_Backend/DirectVulkan): fix some uniform/sampler binding 2026-05-04 22:48:50 +08:00
swung0x48 212309083a [Fix] (MG_State/EGLState): fix compilation error on 32-bit arch where EGLAttrib & EGLint are the same type and collides 2026-05-03 19:52:29 +08:00
swung0x48 c1ebd01a70 Merge pull request #8 from MobileGL-Dev/Feat/Backend-Direct-Vulkan
Feat/backend direct vulkan
2026-04-26 21:55:28 +08:00
swung0x48 af58e2c7b4 [Fix] (MG_Backend/DirectVulkan/VertexInputStateFactory): don't use SSCALED formats 2026-04-26 19:43:27 +08:00
425 changed files with 82492 additions and 4094 deletions
+4
View File
@@ -0,0 +1,4 @@
tools/trace_replay/fixtures/*.tgz filter=lfs diff=lfs merge=lfs -text
tools/trace_replay/fixtures/*.png filter=lfs diff=lfs merge=lfs -text
tools/trace_replay/fixtures/openra.tgz -filter -diff -merge -text
tools/trace_replay/fixtures/openra.0000031249.png -filter -diff -merge -text
+239
View File
@@ -0,0 +1,239 @@
#!/usr/bin/env bash
set -euo pipefail
if [ "$#" -lt 1 ] || [ "$#" -gt 2 ]; then
echo "usage: $0 <trace-case> [fixture-dir]" >&2
exit 2
fi
case_name="$1"
fixture_dir="${2:-tools/trace_replay/fixtures}"
python_bin="${PYTHON:-python3}"
# Fixture mirrors, tried in order before falling back to Git LFS. Override the
# whole list with MOBILEGL_TRACE_FIXTURE_MIRROR_BASES (whitespace separated);
# MOBILEGL_TRACE_FIXTURE_MIRROR_BASE still works and is tried first.
default_mirror_bases=(
"https://git.hit.moe/swung0x48/MobileGL/media/branch/dev/tools/trace_replay/fixtures"
"https://repo.miawa.cn/mgl/tools/trace_replay/fixtures"
)
if [ -n "${MOBILEGL_TRACE_FIXTURE_MIRROR_BASES:-}" ]; then
read -r -a mirror_bases <<< "${MOBILEGL_TRACE_FIXTURE_MIRROR_BASES}"
else
mirror_bases=("${default_mirror_bases[@]}")
fi
if [ -n "${MOBILEGL_TRACE_FIXTURE_MIRROR_BASE:-}" ]; then
mirror_bases=("${MOBILEGL_TRACE_FIXTURE_MIRROR_BASE}" "${mirror_bases[@]}")
fi
# Optional bearer token for mirrors that require authentication (private Gitea).
mirror_token="${MOBILEGL_TRACE_FIXTURE_MIRROR_TOKEN:-}"
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}" \
--fixture-root "${fixture_dir}")"
# Strip CR so the script also works when python emits CRLF (Git Bash on Windows).
mapfile -t files < <(printf '%s\n' "${fixture_list}" | tr -d '\r')
include="$(IFS=,; echo "${files[*]}")"
if [ "${case_name}" = "OpenRA" ]; then
echo "Fixture files for ${case_name} are stored in Git: ${include}"
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 "fixture should not be stored as an LFS pointer: ${file}" >&2
exit 1
fi
done
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
local curl_auth
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
curl_auth=()
if [ -n "${mirror_token}" ]; then
curl_auth=(--header "Authorization: token ${mirror_token}")
fi
if curl -L --fail --show-error --continue-at - "${curl_auth[@]}" --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
local name
local url
local base
local fetched=0
name="$(basename "${file}")"
for base in "${mirror_bases[@]}"; do
url="${base%/}/${name}"
echo "Fetching trace fixture from mirror: ${url}"
if fetch_file_from_mirror "${file}" "${url}"; then
fetched=1
break
fi
echo "Mirror did not serve ${name}; trying the next mirror" >&2
done
if [ "${fetched}" -ne 1 ]; then
return 1
fi
done
}
if fetch_from_mirror; then
echo "Fetched trace fixture files for ${case_name} from mirror: ${include}"
else
echo "All mirrors failed for ${case_name}; falling back to Git LFS: ${include}"
git lfs install --local
git lfs pull --include="${include}" --exclude=""
fi
for file in "${files[@]}"; do
metadata="$(get_lfs_metadata "${file}")"
read -r expected_oid expected_size <<< "${metadata}"
verify_fixture_file "${file}" "${file}" "${expected_oid}" "${expected_size}"
done
+75
View File
@@ -0,0 +1,75 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -ne 3 ]]; then
echo "Usage: $0 <aapt2> <plugin-apk> <trace-apk>" >&2
exit 64
fi
aapt2=$1
plugin_apk=$2
trace_apk=$3
require() {
local needle=$1
local content=$2
local description=$3
if ! grep -Fq -- "$needle" <<<"$content"; then
echo "::error::Missing ${description}: ${needle}" >&2
exit 1
fi
}
for apk in "$plugin_apk" "$trace_apk"; do
[[ -f "$apk" ]] || { echo "::error::APK not found: $apk" >&2; exit 1; }
done
plugin_manifest=$("$aapt2" dump xmltree --file AndroidManifest.xml "$plugin_apk")
plugin_resources=$("$aapt2" dump resources "$plugin_apk")
plugin_resource_text=$(tr -d '"' <<<"$plugin_resources")
trace_manifest=$("$aapt2" dump xmltree --file AndroidManifest.xml "$trace_apk")
plugin_contents=$(unzip -Z1 "$plugin_apk")
require 'top.mobilegl.plugin' "$plugin_manifest" 'plugin package name'
require 'MobileGL' "$plugin_manifest" 'plugin label'
require 'fclPlugin' "$plugin_manifest" 'legacy plugin marker'
require 'fclPlugin_V2' "$plugin_manifest" 'V2 plugin marker'
require 'LIBGL_ES=3:POJAV_RENDERER=opengles3:MOBILEGL_BACKEND_TYPE=DirectGLES' "$plugin_manifest" 'V1 DirectGLES fallback'
require 'string/config' "$plugin_resources" 'V2 renderer configuration resource'
require '{displayName:MobileGL,rendererId:opengles3' "$plugin_resource_text" 'V2 MobileGL entry and renderer ID'
require 'rendererGLPath:**|libMobileGL.so' "$plugin_resource_text" 'V2 GL library path'
require 'rendererEGLPath:**|libMobileGL.so' "$plugin_resource_text" 'V2 EGL library path'
require 'key:LIBGL_ES,value:3' "$plugin_resource_text" 'V2 fixed LIBGL_ES variable'
require 'key:MOBILEGL_BACKEND_TYPE' "$plugin_resource_text" 'V2 backend variable'
require 'defaultValue:DirectGLES' "$plugin_resource_text" 'V2 DirectGLES default'
require 'DirectVulkan' "$plugin_resource_text" 'V2 DirectVulkan option'
require 'key:MOBILEGL_DISABLE_TIMERQUERY' "$plugin_resource_text" 'V2 timer-query toggle'
require 'key:MOBILEGL_DISABLE_SUBGROUP' "$plugin_resource_text" 'V2 Vulkan subgroup toggle'
require 'key:MOBILEGL_MAGMA_R11G11B10F_FALLBACK' "$plugin_resource_text" 'V2 Magma format fallback toggle'
require 'key:MOBILEGL_MAGMA_FRAMESINFLIGHT' "$plugin_resource_text" 'V2 Magma frames-in-flight setting'
require 'key:MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER' "$plugin_resource_text" 'V2 sampler workaround toggle'
require 'key:MOBILEGL_COHERENT_AS_FLUSH' "$plugin_resource_text" 'V2 coherent-as-flush toggle'
require 'key:MOBILEGL_USE_ANGLE' "$plugin_resource_text" 'V2 ANGLE toggle'
if [[ $(grep -Fc 'fclPlugin_V2' <<<"$plugin_manifest") -ne 1 ]]; then
echo '::error::Plugin manifest must expose exactly one V2 descriptor' >&2
exit 1
fi
if ! grep -Eq '^lib/[^/]+/libMobileGL\.so$' <<<"$plugin_contents"; then
echo '::error::Plugin APK does not contain libMobileGL.so' >&2
exit 1
fi
require 'top.mobilegl.plugin.trace' "$trace_manifest" 'trace package name'
require 'top.mobilegl.plugin.TRACE_REPLAY' "$trace_manifest" 'trace replay action'
if grep -Fq 'fclPlugin' <<<"$trace_manifest"; then
echo '::error::Trace APK must not advertise renderer-plugin metadata' >&2
exit 1
fi
if grep -Fq 'android.intent.action.MAIN' <<<"$trace_manifest"; then
echo '::error::Trace APK must not expose a launcher activity' >&2
exit 1
fi
echo 'Validated unified MobileGL plugin APK and isolated trace APK.'
+560
View File
@@ -0,0 +1,560 @@
name: MobileGL APK
on:
push:
branches:
- dev
- Feat/Backend-Direct-GLES
- Feat/Backend-Direct-Vulkan
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
env:
CCACHE_BASEDIR: ${{ github.workspace }}
CCACHE_COMPRESS: "true"
CCACHE_DIR: ${{ github.workspace }}/.ccache
CCACHE_MAXSIZE: 4G
CCACHE_NOHASHDIR: "true"
MOBILEGL_CMAKE_COMPILER_LAUNCHER: ccache
steps:
- name: Checkout repo
uses: actions/checkout@v6
with:
submodules: recursive
- name: Set artifact metadata
run: |
echo "date_today=$(date +'%Y-%m-%d')" >> "$GITHUB_ENV"
- name: Set up JDK
uses: actions/setup-java@v5
with:
distribution: zulu
java-version: '17'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6
with:
gradle-version: 8.10.2
- name: Restore ccache
uses: actions/cache@v5
with:
path: .ccache
key: ${{ runner.os }}-apk-${{ github.job }}-ccache-${{ github.ref_name }}-${{ github.run_id }}
restore-keys: |
${{ runner.os }}-apk-${{ github.job }}-ccache-${{ github.ref_name }}-
${{ runner.os }}-apk-${{ github.job }}-ccache-
- name: Install ccache
run: |
sudo apt-get update
sudo apt-get install -y ccache
ccache --version
- name: Setup Android SDK
uses: android-actions/setup-android@v4
with:
accept-android-sdk-licenses: false
- name: Accept Android SDK licenses
run: yes | sdkmanager --licenses >/dev/null
- name: Install Android NDK
run: |
sdkmanager "ndk;27.3.13750724"
echo "ndk.dir=$ANDROID_HOME/ndk/27.3.13750724" >> android-plugin/local.properties
- name: Update glslang external sources
working-directory: 3rdparty/glslang
run: python update_glslang_sources.py
- name: Build plugin APK
run: gradle --no-daemon -p android-plugin :app:assemblePluginRelease -Pmobilegl.apkSuffix="${GITHUB_SHA}" -Pmobilegl.logLevel=MOBILEGL_LOG_LEVEL_INFO --parallel --max-workers "$(nproc)"
env:
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
- name: Download ANGLE x86_64 libraries
run: |
angle_dir="android-plugin/app/src/trace/jniLibs/x86_64"
rm -rf "${angle_dir}"
mkdir -p "${angle_dir}"
package_angle_variant() {
variant="$1"
commit="$2"
egl_sha="$3"
gles_sha="$4"
source_dir="${RUNNER_TEMP}/mobilegl-angle-${variant}"
base="https://raw.githubusercontent.com/FCL-Team/FoldCraftLauncher/${commit}/FCLauncher/src/main/jniLibs/x86_64"
mkdir -p "${source_dir}"
curl -L --fail --retry 3 -o "${source_dir}/libEGL_angle.so" "${base}/libEGL_angle.so"
curl -L --fail --retry 3 -o "${source_dir}/libGLESv2_angle.so" "${base}/libGLESv2_angle.so"
echo "${egl_sha} ${source_dir}/libEGL_angle.so" | sha256sum -c -
echo "${gles_sha} ${source_dir}/libGLESv2_angle.so" | sha256sum -c -
for library in libEGL_angle libGLESv2_angle; do
filename="${library}_${variant}.so"
cp "${source_dir}/${library}.so" "${angle_dir}/${filename}"
done
}
package_angle_variant \
ec889e6ea831 \
f2a3d510dffd8f6540a52e1a7d0c5787d151075b \
c41828768d089899fa058ec0bee711a91be88347f29bdb935223da6be1149c40 \
e4f820d99f94365c66df868c7740fef142fe5c0cd7c941790a9e30638857ca4d
package_angle_variant \
90a62123d794 \
bdcc96ac11c79001018ae4375eb73cb54a9f682f \
d0f4298ccc770cc801fc52e21733521646161e8a4adb3bd0052d9a1b57ee0ca8 \
66fdc867e552192d553d59095ea2e3cef4829de65c356f1fd826027b1905972e
- name: Build retrace APK
run: gradle --no-daemon -p android-plugin :app:assembleTraceRelease -Pmobilegl.apkSuffix="${GITHUB_SHA}" -Pmobilegl.abis=all -Pmobilegl.debuggableRelease=true -Pmobilegl.logLevel=MOBILEGL_LOG_LEVEL_INFO --parallel --max-workers "$(nproc)"
env:
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
- name: Show ccache stats
if: always()
run: ccache --show-stats
- name: Verify APK metadata and packaging
run: |
AAPT2="$(find "$ANDROID_HOME/build-tools" -name aapt2 -type f | sort -V | tail -n 1)"
plugin_apk="android-plugin/app/build/outputs/apk/plugin/release/MobileGL-plugin-release-${GITHUB_SHA}.apk"
trace_apk="android-plugin/app/build/outputs/apk/trace/release/MobileGL-plugin-trace-release-${GITHUB_SHA}.apk"
test -f "${plugin_apk}"
test -f "${trace_apk}"
bash .github/scripts/validate-plugin-apks.sh "$AAPT2" "$plugin_apk" "$trace_apk"
- name: Verify signed APKs
run: |
APKSIGNER="$(find "$ANDROID_HOME/build-tools" -name apksigner -type f | sort -V | tail -n 1)"
mapfile -t APKS < <(printf '%s\n' \
"android-plugin/app/build/outputs/apk/plugin/release/MobileGL-plugin-release-${GITHUB_SHA}.apk" \
"android-plugin/app/build/outputs/apk/trace/release/MobileGL-plugin-trace-release-${GITHUB_SHA}.apk")
for APK in "${APKS[@]}"; do
if [[ ! -f "$APK" ]]; then
echo "::error::Expected release APK was not produced: $APK"
exit 1
fi
done
for APK in "${APKS[@]}"; do
if [[ "$APK" == *-unsigned.apk ]]; then
echo "::error::Unsigned release APK produced: $APK"
exit 1
fi
"$APKSIGNER" verify --verbose "$APK"
done
- name: Upload plugin APK
uses: actions/upload-artifact@v7
with:
name: MobileGL-plugin-${{ env.date_today }}-${{ github.sha }}
path: android-plugin/app/build/outputs/apk/plugin/release/MobileGL-plugin-release-${{ github.sha }}.apk
archive: false
if-no-files-found: error
- name: Upload retrace APK
uses: actions/upload-artifact@v7
with:
name: MobileGL-retrace-apk-${{ env.date_today }}-${{ github.sha }}
path: android-plugin/app/build/outputs/apk/trace/release/MobileGL-plugin-trace-release-${{ github.sha }}.apk
archive: false
if-no-files-found: error
trace-cases:
name: trace case matrix
runs-on: ubuntu-latest
needs: build
outputs:
android: ${{ steps.trace-cases.outputs.android }}
names: ${{ steps.trace-cases.outputs.names }}
steps:
- name: Checkout repo
uses: actions/checkout@v6
- name: Load trace cases
id: trace-cases
run: |
echo "android=$(python3 tools/trace_replay/trace_cases.py --ci --format github-apk)" >> "$GITHUB_OUTPUT"
echo "names=$(python3 tools/trace_replay/trace_cases.py --ci --format names)" >> "$GITHUB_OUTPUT"
trace-fixtures:
name: trace fixture (${{ matrix.case }})
runs-on: ubuntu-latest
needs: trace-cases
strategy:
fail-fast: false
max-parallel: 4
matrix:
case: ${{ fromJSON(needs.trace-cases.outputs.names) }}
steps:
- name: Checkout repo
uses: actions/checkout@v6
- name: Fetch trace fixture
run: bash .github/scripts/fetch-trace-fixture-lfs.sh '${{ matrix.case }}'
- name: Stage trace fixture
run: |
safe_case="$(printf '%s' '${{ matrix.case }}' | sed 's/[^A-Za-z0-9._-]/_/g')"
stage_dir="trace-fixtures/${safe_case}"
mkdir -p "${stage_dir}"
python3 tools/trace_replay/trace_cases.py --format fixture-files --case '${{ matrix.case }}' |
while IFS= read -r file; do
cp "${file}" "${stage_dir}/"
done
- name: Upload trace fixture
uses: actions/upload-artifact@v7
with:
name: MobileGL-trace-fixture-${{ matrix.case }}
path: trace-fixtures/**
if-no-files-found: error
android-avd:
name: android avd image
runs-on: ubuntu-latest
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
- name: Setup Android SDK
uses: android-actions/setup-android@v4
with:
accept-android-sdk-licenses: false
- name: Accept Android SDK licenses
run: yes | sdkmanager --licenses >/dev/null
- name: Restore Android AVD cache
id: android-avd-cache
uses: actions/cache@v5
with:
path: |
${{ env.ANDROID_AVD_HOME }}
${{ 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'
run: |
sh android-plugin/run-avd-ci.sh create \
--api-level 35 \
--target google_apis \
--arch x86_64 \
--profile pixel_6 \
--avd-name "${AVD_NAME}"
retrace:
name: retrace (${{ matrix.backend.name }}, ${{ matrix.case.name }})
runs-on: ubuntu-latest
needs:
- build
- android-avd
- trace-cases
- trace-fixtures
if: ${{ always() && needs.build.result == 'success' && needs.android-avd.result == 'success' && needs.trace-cases.result == 'success' }}
timeout-minutes: 75
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
matrix:
backend:
- name: DirectGLES
gpu: software
- name: DirectVulkan
gpu: lavapipe
case: ${{ fromJSON(needs.trace-cases.outputs.android) }}
steps:
- name: Set Swap Space
uses: pierotofy/set-swap-space@v1.0
with:
swap-size-gb: 8
- name: Checkout repo
uses: actions/checkout@v6
- name: Download trace fixture
uses: actions/download-artifact@v8
with:
name: MobileGL-trace-fixture-${{ matrix.case.name }}
path: trace-fixture-download
- name: Install trace fixture
run: |
mkdir -p tools/trace_replay/fixtures
find trace-fixture-download -type f -exec cp {} tools/trace_replay/fixtures/ \;
- name: Set artifact metadata
run: |
echo "date_today=$(date +'%Y-%m-%d')" >> "$GITHUB_ENV"
echo "EMULATOR_LOG=${RUNNER_TEMP}/mobilegl-emulator.log" >> "$GITHUB_ENV"
echo "EMULATOR_PID_FILE=${RUNNER_TEMP}/mobilegl-emulator.pid" >> "$GITHUB_ENV"
- name: Setup Android SDK
uses: android-actions/setup-android@v4
with:
accept-android-sdk-licenses: false
- name: Accept Android SDK licenses
run: yes | sdkmanager --licenses >/dev/null
- name: Restore Android AVD cache
id: android-avd-cache
uses: actions/cache/restore@v5
with:
path: |
${{ env.ANDROID_AVD_HOME }}
${{ 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
with:
name: MobileGL-plugin-trace-release-${{ github.sha }}.apk
path: android-retrace-apks
- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Create AVD
if: steps.android-avd-cache.outputs.cache-hit != 'true'
run: |
sh android-plugin/run-avd-ci.sh create \
--api-level 35 \
--target google_apis \
--arch x86_64 \
--profile pixel_6 \
--avd-name "${AVD_NAME}"
- name: Launch Emulator
run: |
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
- name: Retrace and validate
env:
MOBILEGL_USE_ANGLE: ${{ matrix.backend.name == 'DirectGLES' && '1' || '0' }}
MOBILEGL_TRACE_ANGLE_VARIANT: ${{ matrix.case.name == 'minecraft-1.21.4-fabric-iris-bliss-in-world' && '90a62123d794' || 'ec889e6ea831' }}
MOBILEGL_MAGMA_R11G11B10F_FALLBACK: ${{ matrix.backend.name == 'DirectVulkan' && '1' || '0' }}
run: |
apk_file="android-retrace-apks/MobileGL-plugin-trace-release-${GITHUB_SHA}.apk"
test -f "${apk_file}"
extra_retrace_args=()
# Bliss needs the newer signed ANGLE variant plus sampler mipmap
# min-filter downgrading on ANGLE llvmpipe.
if [ "${{ matrix.backend.name }}" = "DirectGLES" ] && [ "${{ matrix.case.name }}" = "minecraft-1.21.4-fabric-iris-bliss-in-world" ]; then
extra_retrace_args+=(--avoid-angle-llvmpipe-sampler-mipmap-min-filter)
fi
if [ "${{ matrix.case.coherent_as_flush || false }}" = "true" ]; then
extra_retrace_args+=(--coherent-as-flush)
fi
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()
run: |
safe_case="$(printf '%s' '${{ matrix.case.name }}' | sed 's/[^A-Za-z0-9._-]/_/g')"
result_dir="android-retrace-result/${safe_case}-${{ matrix.backend.name }}"
mkdir -p "${result_dir}"
if [ -s "${{ matrix.case.golden }}" ]; then
cp "${{ matrix.case.golden }}" "${result_dir}/${safe_case}-${{ matrix.backend.name }}-golden.png"
fi
if [ -n "${{ matrix.case.alternate_golden || '' }}" ] && [ -s "${{ matrix.case.alternate_golden || '' }}" ]; then
cp "${{ matrix.case.alternate_golden || '' }}" "${result_dir}/${safe_case}-${{ matrix.backend.name }}-alternate-golden.png"
fi
- name: Collect emulator diagnostics
if: always()
run: |
mkdir -p android-retrace-result/diagnostics
adb devices -l > android-retrace-result/diagnostics/adb-devices.txt || true
timeout 30 adb logcat -d -t 1000 > android-retrace-result/diagnostics/logcat.txt || true
if [ -f "${EMULATOR_LOG}" ]; then
cp "${EMULATOR_LOG}" android-retrace-result/diagnostics/emulator.log
fi
- name: Stop Emulator
if: always()
run: |
sh android-plugin/run-avd-ci.sh stop \
--avd-name "${AVD_NAME}" \
--emulator-log "${EMULATOR_LOG}" \
--pid-file "${EMULATOR_PID_FILE}"
- name: Upload Android retrace result
if: always()
uses: actions/upload-artifact@v7
with:
name: MobileGL-android-retrace-result-${{ env.date_today }}-${{ github.sha }}-${{ matrix.backend.name }}-${{ matrix.case.name }}
path: android-retrace-result/**
if-no-files-found: warn
retrace-summary:
name: retrace summary
runs-on: ubuntu-latest
needs: retrace
if: always()
steps:
- name: Checkout repo
uses: actions/checkout@v6
- name: Set artifact metadata
run: |
echo "date_today=$(date +'%Y-%m-%d')" >> "$GITHUB_ENV"
- name: Set up Node.js
uses: actions/setup-node@v7
with:
node-version: '22'
- name: Download Android retrace results
uses: actions/download-artifact@v8
with:
pattern: MobileGL-android-retrace-result-*
path: retrace-artifacts
- name: Render retrace summary
run: |
node tools/trace_replay/render_retrace_summary.mjs \
--input retrace-artifacts \
--output-dir android-retrace-summary \
--title "MobileGL Android retrace overview" \
--group-label "Android Emulator" \
--html mobilegl-android-retrace-overview.html
- name: Upload Android retrace summary
uses: actions/upload-artifact@v7
with:
path: android-retrace-summary/mobilegl-android-retrace-overview.html
archive: false
if-no-files-found: error
remove-artifact-clutter:
name: remove artifact clutter
runs-on: ubuntu-latest
needs: retrace-summary
if: always()
permissions:
actions: write
steps:
- name: Delete intermediate Android retrace artifacts
env:
GH_TOKEN: ${{ github.token }}
run: |
declare -A failed_cases=()
while IFS= read -r job_name; do
case_name="${job_name#retrace (*, }"
case_name="${case_name%)}"
failed_cases["${case_name}"]=1
done < <(
gh api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/jobs?per_page=100" \
--jq '.jobs[] | select(.name | startswith("retrace (")) | select(.conclusion == "failure" or .conclusion == "cancelled" or .conclusion == "timed_out" or .conclusion == "action_required") | .name'
)
if ((${#failed_cases[@]})); then
echo "Retaining fixtures for failed retrace case(s):"
printf ' %s\n' "${!failed_cases[@]}"
else
echo "All retrace jobs succeeded; no fixtures need to be retained."
fi
deleted=0
retained=0
while IFS=$'\t' read -r artifact_id artifact_name; do
if [[ "${artifact_name}" == MobileGL-trace-fixture-* ]]; then
case_name="${artifact_name#MobileGL-trace-fixture-}"
if [[ -v "failed_cases[${case_name}]" ]]; then
echo "Retaining ${artifact_name} (${artifact_id}) for failed retrace."
((retained += 1))
continue
fi
fi
echo "Deleting ${artifact_name} (${artifact_id})"
gh api --method DELETE "repos/${GITHUB_REPOSITORY}/actions/artifacts/${artifact_id}"
((deleted += 1))
done < <(
gh api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/artifacts?per_page=100" \
--jq '.artifacts[] | select(.name | startswith("MobileGL-trace-fixture-") or startswith("MobileGL-android-retrace-result-") or startswith("trace-fixture-") or startswith("retrace-result-")) | [.id, .name] | @tsv'
)
echo "Deleted ${deleted} intermediate Android artifact(s); retained ${retained} failed-retrace fixture(s)."
-64
View File
@@ -1,64 +0,0 @@
name: Benchmark
on:
push:
branches:
- dev
- Feat/Backend-Direct-GLES
- Feat/Backend-Direct-Vulkan
jobs:
benchmark:
runs-on: ubuntu-latest
env:
# BENCH_ROOT: ${{github.workspace}}/MobileGL/MG_Benchmark
BENCH_ROOT: ${{github.workspace}}
steps:
- name: Set Swap Space
uses: pierotofy/set-swap-space@master
with:
swap-size-gb: 32
- name: Checkout repo
uses: actions/checkout@v4
with:
submodules: true
- name: Get CMake
uses: lukka/get-cmake@latest
- name: Prepare Vulkan SDK
uses: humbletim/setup-vulkan-sdk@v1.2.1
with:
vulkan-query-version: 1.4.304.1
vulkan-components: Vulkan-Headers, Vulkan-Loader
vulkan-use-cache: true
- name: Update glslang external sources
working-directory: ${{env.BENCH_ROOT}}/3rdparty/glslang
run: python update_glslang_sources.py
- name: Install clang-20
run: |
sudo apt-get update
sudo apt-get install -y clang-20 clang++-20 lld-20 libc++-20-dev libc++abi-20-dev libvulkan-dev
- name: Show installed toolchain
run: |
clang-20 --version
clang++-20 --version
ld.lld-20 --version || ld.lld --version || true
dpkg -l 'libc++*' || true
- name: Configure CMake
working-directory: ${{env.BENCH_ROOT}}
run: cmake -S . -B build-bench -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang-20 -DCMAKE_CXX_COMPILER=clang++-20 -DBENCHMARK_DOWNLOAD_DEPENDENCIES=ON -DBENCHMARK_ENABLE_TESTING=OFF -DMOBILEGL_BUILD_TEST=OFF -DMOBILEGL_BUILD_BENCHMARK=ON -DCMAKE_POLICY_VERSION_MINIMUM=3.5
- name: Build
working-directory: ${{env.BENCH_ROOT}}/build-bench
run: cmake --build .
- name: Benchmark
working-directory: ${{env.BENCH_ROOT}}/build-bench/MobileGL/MG_Benchmark
run: ctest -V -C Release
+518 -23
View File
@@ -6,27 +6,41 @@ on:
- dev
- Feat/Backend-Direct-GLES
- Feat/Backend-Direct-Vulkan
workflow_dispatch:
jobs:
test:
build-linux:
runs-on: ubuntu-latest
env:
# TEST_ROOT: ${{github.workspace}}/MobileGL/MG_Test
TEST_ROOT: ${{github.workspace}}
BUILD_DIR: build-linux
CCACHE_BASEDIR: ${{ github.workspace }}
CCACHE_COMPRESS: "true"
CCACHE_DIR: ${{ github.workspace }}/.ccache
CCACHE_MAXSIZE: 4G
CCACHE_NOHASHDIR: "true"
steps:
- name: Set Swap Space
uses: pierotofy/set-swap-space@master
uses: pierotofy/set-swap-space@v1.0
with:
swap-size-gb: 32
- name: Checkout repo
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
submodules: true
submodules: recursive
- name: Get CMake
uses: lukka/get-cmake@latest
uses: lukka/get-cmake@v4.3.3
- name: Restore ccache
uses: actions/cache@v5
with:
path: .ccache
key: ${{ runner.os }}-test-${{ github.job }}-ccache-${{ github.ref_name }}-${{ github.run_id }}
restore-keys: |
${{ runner.os }}-test-${{ github.job }}-ccache-${{ github.ref_name }}-
${{ runner.os }}-test-${{ github.job }}-ccache-
- name: Prepare Vulkan SDK
uses: humbletim/setup-vulkan-sdk@v1.2.1
@@ -36,39 +50,520 @@ jobs:
vulkan-use-cache: true
- name: Update glslang external sources
working-directory: ${{env.TEST_ROOT}}/3rdparty/glslang
working-directory: 3rdparty/glslang
run: python update_glslang_sources.py
- name: Install clang-20
- name: Install build dependencies
run: |
sudo apt-get update
sudo apt-get install -y clang-20 clang++-20 lld-20 libc++-20-dev libc++abi-20-dev libvulkan-dev
sudo apt-get install -y ccache clang-20 clang++-20 lld-20 libc++-20-dev libc++abi-20-dev libvulkan-dev libegl1-mesa-dev libgles2-mesa-dev libgl1-mesa-dri mesa-vulkan-drivers ninja-build
- name: Show installed toolchain
run: |
ccache --version
clang-20 --version
clang++-20 --version
ld.lld-20 --version || ld.lld --version || true
dpkg -l 'libc++*' || true
dpkg -l 'libc++*' 'libegl*' 'libgles*' 'mesa*' 'vulkan*' || true
- name: Configure CMake
working-directory: ${{env.TEST_ROOT}}
run: |
if [ "${{ secrets.ACTIONS_STEP_DEBUG }}" == "true" ]; then
cmake -S . -B build-test -G Ninja -DCMAKE_C_COMPILER=clang-20 -DCMAKE_CXX_COMPILER=clang++-20 -DCMAKE_BUILD_TYPE=Debug -DMOBILEGL_BUILD_TEST=ON -DMOBILEGL_BUILD_BENCHMARK=OFF -DCMAKE_POLICY_VERSION_MINIMUM=3.5
if [ "${{ secrets.ACTIONS_STEP_DEBUG }}" = "true" ]; then
BUILD_TYPE=Debug
else
cmake -S . -B build-test -G Ninja -DCMAKE_C_COMPILER=clang-20 -DCMAKE_CXX_COMPILER=clang++-20 -DCMAKE_BUILD_TYPE=Release -DMOBILEGL_BUILD_TEST=ON -DMOBILEGL_BUILD_BENCHMARK=OFF -DCMAKE_POLICY_VERSION_MINIMUM=3.5
BUILD_TYPE=Release
fi
cmake -S . -B "${BUILD_DIR}" -G Ninja \
-DCMAKE_C_COMPILER=clang-20 \
-DCMAKE_CXX_COMPILER=clang++-20 \
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
-DCMAKE_BUILD_TYPE="${BUILD_TYPE}" \
-DMOBILEGL_LOG_ACTIVE_LEVEL=MOBILEGL_LOG_LEVEL_INFO \
-DMOBILEGL_BUILD_TEST=ON \
-DMOBILEGL_BUILD_BENCHMARK=ON \
-DMOBILEGL_BUILD_TRACE_REPLAY=OFF \
-DBENCHMARK_DOWNLOAD_DEPENDENCIES=ON \
-DBENCHMARK_ENABLE_TESTING=OFF \
-DCMAKE_POLICY_VERSION_MINIMUM=3.5
- name: Build
working-directory: ${{env.TEST_ROOT}}/build-test
run: cmake --build .
run: cmake --build "${BUILD_DIR}" --parallel "$(nproc)"
- name: Show ccache stats
if: always()
run: ccache --show-stats
- name: Package Linux runtime
run: |
mkdir -p ci-artifacts
mapfile -t SHARED_LIBS < <(find "${BUILD_DIR}" -type f \( -name '*.so' -o -name '*.so.*' \) -print | sort)
tar \
--exclude='*/CMakeFiles' \
--exclude='*.o' \
--exclude='*.a' \
--exclude='*.ninja*' \
--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[@]}"
- name: Upload Linux runtime
uses: actions/upload-artifact@v7
with:
name: mobilegl-linux-runtime
path: ci-artifacts/mobilegl-linux-runtime.tgz
if-no-files-found: error
test:
runs-on: ubuntu-latest
needs: build-linux
steps:
- name: Checkout repo
uses: actions/checkout@v6
- name: Get CMake
uses: lukka/get-cmake@v4.3.3
- name: Install runtime dependencies
run: |
sudo apt-get update
sudo apt-get install -y libvulkan1 libegl1 libgles2 libgl1-mesa-dri mesa-vulkan-drivers
- name: Download Linux runtime
uses: actions/download-artifact@v8
with:
name: mobilegl-linux-runtime
path: .
- name: Unpack Linux runtime
run: tar -xzf mobilegl-linux-runtime.tgz
- name: Normalize CTest command paths
run: |
python - <<'PY'
from pathlib import Path
import re
for path in Path('build-linux').rglob('CTestTestfile.cmake'):
text = path.read_text()
text = re.sub(r'"[^"]*/cmake-[^"]*/bin/cmake"', '"cmake"', text)
path.write_text(text)
PY
- name: Test
working-directory: ${{env.TEST_ROOT}}/build-test/MobileGL/MG_Test
working-directory: build-linux
run: |
if [ "${{ secrets.ACTIONS_STEP_DEBUG }}" == "true" ]; then
ctest -V
if [ "${{ secrets.ACTIONS_STEP_DEBUG }}" = "true" ]; then
ctest -V -L unit --no-tests=error
else
ctest
ctest --output-on-failure -L unit --no-tests=error
fi
benchmark:
runs-on: ubuntu-latest
needs: build-linux
steps:
- name: Checkout repo
uses: actions/checkout@v6
- name: Get CMake
uses: lukka/get-cmake@v4.3.3
- name: Install runtime dependencies
run: |
sudo apt-get update
sudo apt-get install -y libvulkan1 libegl1 libgles2 libgl1-mesa-dri mesa-vulkan-drivers
- name: Download Linux runtime
uses: actions/download-artifact@v8
with:
name: mobilegl-linux-runtime
path: .
- name: Unpack Linux runtime
run: tar -xzf mobilegl-linux-runtime.tgz
- name: Normalize CTest command paths
run: |
python - <<'PY'
from pathlib import Path
import re
for path in Path('build-linux').rglob('CTestTestfile.cmake'):
text = path.read_text()
text = re.sub(r'"[^"]*/cmake-[^"]*/bin/cmake"', '"cmake"', text)
path.write_text(text)
PY
- name: Benchmark
working-directory: build-linux
run: ctest -V -C Release -L benchmark --no-tests=error
build-retrace:
runs-on: ubuntu-latest
needs:
- build-linux
- test
- benchmark
env:
BUILD_DIR: build-retrace
CCACHE_BASEDIR: ${{ github.workspace }}
CCACHE_COMPRESS: "true"
CCACHE_DIR: ${{ github.workspace }}/.ccache
CCACHE_MAXSIZE: 4G
CCACHE_NOHASHDIR: "true"
MOBILEGL_LIBRARY: ${{ github.workspace }}/build-linux/libMobileGL.so
steps:
- name: Set Swap Space
uses: pierotofy/set-swap-space@v1.0
with:
swap-size-gb: 32
- name: Checkout repo
uses: actions/checkout@v6
with:
submodules: recursive
- name: Get CMake
uses: lukka/get-cmake@v4.3.3
- name: Restore ccache
uses: actions/cache@v5
with:
path: .ccache
key: ${{ runner.os }}-test-${{ github.job }}-ccache-${{ github.ref_name }}-${{ github.run_id }}
restore-keys: |
${{ runner.os }}-test-${{ github.job }}-ccache-${{ github.ref_name }}-
${{ runner.os }}-test-${{ github.job }}-ccache-
- name: Prepare Vulkan SDK
uses: humbletim/setup-vulkan-sdk@v1.2.1
with:
vulkan-query-version: 1.4.304.1
vulkan-components: Vulkan-Headers, Vulkan-Loader
vulkan-use-cache: true
- name: Update glslang external sources
working-directory: 3rdparty/glslang
run: python update_glslang_sources.py
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y ccache clang-20 clang++-20 lld-20 libc++-20-dev libc++abi-20-dev libvulkan-dev libegl1-mesa-dev libgles2-mesa-dev libgl1-mesa-dri mesa-vulkan-drivers ninja-build
- name: Show installed toolchain
run: |
ccache --version
clang-20 --version
clang++-20 --version
ld.lld-20 --version || ld.lld --version || true
dpkg -l 'libc++*' 'libegl*' 'libgles*' 'mesa*' 'vulkan*' || true
- name: Download Linux runtime
uses: actions/download-artifact@v8
with:
name: mobilegl-linux-runtime
path: .
- name: Unpack Linux runtime
run: |
tar -xzf mobilegl-linux-runtime.tgz
test -f "${MOBILEGL_LIBRARY}"
- name: Configure CMake
run: |
if [ "${{ secrets.ACTIONS_STEP_DEBUG }}" = "true" ]; then
BUILD_TYPE=Debug
else
BUILD_TYPE=Release
fi
cmake -S . -B "${BUILD_DIR}" -G Ninja \
-DCMAKE_C_COMPILER=clang-20 \
-DCMAKE_CXX_COMPILER=clang++-20 \
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
-DCMAKE_BUILD_TYPE="${BUILD_TYPE}" \
-DMOBILEGL_LOG_ACTIVE_LEVEL=MOBILEGL_LOG_LEVEL_INFO \
-DMOBILEGL_BUILD_TEST=OFF \
-DMOBILEGL_BUILD_BENCHMARK=OFF \
-DMOBILEGL_BUILD_TRACE_REPLAY=ON \
-DMOBILEGL_TRACE_REPLAY_MOBILEGL_LIBRARY="${MOBILEGL_LIBRARY}" \
-DCMAKE_POLICY_VERSION_MINIMUM=3.5
- name: Build trace replay
run: cmake --build "${BUILD_DIR}" --target mobilegl_trace_replay --parallel "$(nproc)"
- name: Show ccache stats
if: always()
run: ccache --show-stats
- name: Normalize CTest command paths
run: |
python - <<'PY'
from pathlib import Path
import re
for path in Path('build-retrace').rglob('CTestTestfile.cmake'):
text = path.read_text()
text = re.sub(r'"[^"]*/cmake-[^"]*/bin/cmake"', '"cmake"', text)
path.write_text(text)
PY
- name: Package trace replay
run: |
mkdir -p ci-artifacts
tar -czf ci-artifacts/mobilegl-trace-replay.tgz \
build-retrace/tools/trace_replay/mobilegl_trace_replay \
build-retrace/tools/trace_replay/CTestTestfile.cmake
- name: Upload trace replay
uses: actions/upload-artifact@v7
with:
name: mobilegl-trace-replay
path: ci-artifacts/mobilegl-trace-replay.tgz
if-no-files-found: error
trace-cases:
name: trace case matrix
runs-on: ubuntu-latest
needs:
- test
- benchmark
outputs:
names: ${{ steps.trace-cases.outputs.names }}
steps:
- name: Checkout repo
uses: actions/checkout@v6
- name: Load trace cases
id: trace-cases
run: echo "names=$(python3 tools/trace_replay/trace_cases.py --ci --format names)" >> "$GITHUB_OUTPUT"
trace-fixtures:
name: trace fixture (${{ matrix.case }})
runs-on: ubuntu-latest
needs: trace-cases
strategy:
fail-fast: false
max-parallel: 4
matrix:
case: ${{ fromJSON(needs.trace-cases.outputs.names) }}
steps:
- name: Checkout repo
uses: actions/checkout@v6
- name: Fetch trace fixture
run: bash .github/scripts/fetch-trace-fixture-lfs.sh '${{ matrix.case }}'
- name: Stage trace fixture
run: |
safe_case="$(printf '%s' '${{ matrix.case }}' | sed 's/[^A-Za-z0-9._-]/_/g')"
stage_dir="trace-fixtures/${safe_case}"
mkdir -p "${stage_dir}"
python3 tools/trace_replay/trace_cases.py --format fixture-files --case '${{ matrix.case }}' |
while IFS= read -r file; do
cp "${file}" "${stage_dir}/"
done
- name: Upload trace fixture
uses: actions/upload-artifact@v7
with:
name: trace-fixture-${{ matrix.case }}
path: trace-fixtures/**
if-no-files-found: error
retrace:
name: retrace (${{ matrix.backend }}, ${{ matrix.case }})
runs-on: ubuntu-latest
needs:
- build-linux
- build-retrace
- trace-cases
- trace-fixtures
if: ${{ always() && needs.build-linux.result == 'success' && needs.build-retrace.result == 'success' && needs.trace-cases.result == 'success' }}
strategy:
fail-fast: false
max-parallel: 4
matrix:
backend:
- DirectGLES
- DirectVulkan
case: ${{ fromJSON(needs.trace-cases.outputs.names) }}
steps:
- name: Set Swap Space
uses: pierotofy/set-swap-space@v1.0
with:
swap-size-gb: 16
- name: Checkout repo
uses: actions/checkout@v6
- name: Download trace fixture
uses: actions/download-artifact@v8
with:
name: trace-fixture-${{ matrix.case }}
path: trace-fixture-download
- name: Install trace fixture
run: |
mkdir -p tools/trace_replay/fixtures
find trace-fixture-download -type f -exec cp {} tools/trace_replay/fixtures/ \;
- name: Get CMake
uses: lukka/get-cmake@v4.3.3
- name: Install runtime dependencies
run: |
sudo apt-get update
sudo apt-get install -y libvulkan1 libegl1-mesa-dev libgles2-mesa-dev libgl1-mesa-dri mesa-vulkan-drivers
test -e /usr/lib/x86_64-linux-gnu/libEGL.so
test -e /usr/lib/x86_64-linux-gnu/libGLESv2.so
- name: Download Linux runtime
uses: actions/download-artifact@v8
with:
name: mobilegl-linux-runtime
path: .
- name: Download trace replay
uses: actions/download-artifact@v8
with:
name: mobilegl-trace-replay
path: .
- name: Unpack retrace runtime
run: |
tar -xzf mobilegl-linux-runtime.tgz
tar -xzf mobilegl-trace-replay.tgz
test -f build-linux/libMobileGL.so
test -f build-retrace/tools/trace_replay/mobilegl_trace_replay
- name: Retrace and validate
working-directory: build-retrace/tools/trace_replay
run: |
if [ '${{ matrix.backend }}' = 'DirectVulkan' ]; then
export MOBILEGL_MAGMA_R11G11B10F_FALLBACK=1
fi
# The blended depth-write quirk auto-enables only on Qualcomm, which no CI
# runner has, so force it on for the OIT case it exists to fix. ForceOn
# bypasses only the vendor gate, so this exercises the real strip on
# lavapipe. The Android AVD lane deliberately leaves it off, keeping the
# unstripped path covered for the same trace.
if [ '${{ matrix.backend }}' = 'DirectVulkan' ] \
&& [ '${{ matrix.case }}' = 'improved-transparency-minecraft-26.3' ]; then
export MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE=1
fi
ctest -V --no-tests=error -R '^MobileGLTraceReplay\.${{ matrix.case }}\.${{ matrix.backend }}$'
- name: Upload actual image
if: always()
uses: actions/upload-artifact@v7
with:
name: retrace-result-${{ matrix.backend }}-${{ matrix.case }}
path: |
build-retrace/tools/trace_replay/${{ matrix.case }}/actual-images/**
build-retrace/tools/trace_replay/${{ matrix.case }}/${{ matrix.backend }}/output/**
if-no-files-found: warn
retrace-summary:
name: retrace summary
runs-on: ubuntu-latest
needs: retrace
if: ${{ always() && needs.retrace.result != 'skipped' }}
steps:
- name: Checkout repo
uses: actions/checkout@v6
- name: Set artifact metadata
run: |
echo "date_today=$(date +'%Y-%m-%d')" >> "$GITHUB_ENV"
- name: Set up Node.js
uses: actions/setup-node@v7
with:
node-version: '22'
- name: Download retrace results
uses: actions/download-artifact@v8
with:
pattern: retrace-result-*
path: retrace-artifacts
- name: Render retrace summary
run: |
node tools/trace_replay/render_retrace_summary.mjs \
--input retrace-artifacts \
--output-dir retrace-summary \
--title "MobileGL Linux retrace overview" \
--group-label "Linux" \
--html mobilegl-linux-retrace-overview.html
- name: Upload retrace summary
uses: actions/upload-artifact@v7
with:
path: retrace-summary/mobilegl-linux-retrace-overview.html
archive: false
if-no-files-found: error
remove-artifact-clutter:
name: remove artifact clutter
runs-on: ubuntu-latest
needs: retrace-summary
if: always()
permissions:
actions: write
steps:
- name: Delete intermediate Linux retrace artifacts
env:
GH_TOKEN: ${{ github.token }}
run: |
declare -A failed_cases=()
while IFS= read -r job_name; do
case_name="${job_name#retrace (*, }"
case_name="${case_name%)}"
failed_cases["${case_name}"]=1
done < <(
gh api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/jobs?per_page=100" \
--jq '.jobs[] | select(.name | startswith("retrace (")) | select(.conclusion == "failure" or .conclusion == "cancelled" or .conclusion == "timed_out" or .conclusion == "action_required") | .name'
)
if ((${#failed_cases[@]})); then
echo "Retaining fixtures for failed retrace case(s):"
printf ' %s\n' "${!failed_cases[@]}"
else
echo "All retrace jobs succeeded; no fixtures need to be retained."
fi
deleted=0
retained=0
while IFS=$'\t' read -r artifact_id artifact_name; do
if [[ "${artifact_name}" == trace-fixture-* ]]; then
case_name="${artifact_name#trace-fixture-}"
if [[ -v "failed_cases[${case_name}]" ]]; then
echo "Retaining ${artifact_name} (${artifact_id}) for failed retrace."
((retained += 1))
continue
fi
fi
echo "Deleting ${artifact_name} (${artifact_id})"
gh api --method DELETE "repos/${GITHUB_REPOSITORY}/actions/artifacts/${artifact_id}"
((deleted += 1))
done < <(
gh api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/artifacts?per_page=100" \
--jq '.artifacts[] | select(.name | startswith("trace-fixture-") or startswith("retrace-result-")) | [.id, .name] | @tsv'
)
echo "Deleted ${deleted} intermediate Linux artifact(s); retained ${retained} failed-retrace fixture(s)."
+7 -1
View File
@@ -18,4 +18,10 @@ MobileGL/MG_Test/build
/cmake-build*
.idea
MobileGL/MG*/build*
MobileGL/MG*/cmake-build*
MobileGL/MG*/cmake-build*
/android-plugin/.gradle
/android-plugin/build
/android-plugin/app/build
/android-plugin/app/src/trace/jniLibs
/android-plugin/local.properties
tools/trace_replay/work/
+3
View File
@@ -28,3 +28,6 @@
[submodule "3rdparty/SPIRV-Reflect"]
path = 3rdparty/SPIRV-Reflect
url = https://github.com/KhronosGroup/SPIRV-Reflect.git
[submodule "3rdparty/apitrace"]
path = 3rdparty/apitrace
url = https://github.com/MobileGL-Dev/apitrace.git
Vendored Submodule
+1
Submodule 3rdparty/apitrace added at 10935bb5e4
+136 -2
View File
@@ -6,6 +6,11 @@ option(MOBILEGL_BUILD_TEST "Build MobileGL tests"
option(MOBILEGL_BUILD_BENCHMARK "Build MobileGL benchmarks" ON )
option(MOBILEGL_FORCE_RELEASE_OPT "Enable Release optimization flags in Debug build" ON )
option(MOBILEGL_ENABLE_TRACY "Enable tracy for profiling" OFF)
option(MOBILEGL_BUILD_TRACE_REPLAY "Build desktop apitrace replay runner" OFF)
option(MOBILEGL_TRACE_ANGLE_VARIANTS "Enable signed trace-APK ANGLE variant loading" OFF)
option(MOBILEGL_IOS "Build MobileGL for iOS instead of macOS when APPLE is set" OFF)
set(MOBILEGL_LOG_ACTIVE_LEVEL "MOBILEGL_LOG_LEVEL_INFO" CACHE STRING "MobileGL active log level macro")
set(MOBILEGL_VULKAN_LIBRARY "" CACHE FILEPATH "Vulkan loader/MoltenVK library to link for iOS builds")
if (ANDROID)
set(MOBILEGL_BUILD_TEST OFF CACHE BOOL "Build MobileGL tests" FORCE)
@@ -179,11 +184,25 @@ set(SOURCE_FILES
MobileGL/MG_Util/ShaderTranspiler/SpvcSession.cpp
MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp
MobileGL/MG_Util/ShaderTranspiler/glslang/TMglGlslIoResolver.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenInterfaceStructPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EliminateFloatEqualsZeroPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RenameSamplerFunctionParameterPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecomposeWorkgroupVec3Pass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecoratePositionInvariantPass.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/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateNoPerspectivePass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FoldConstOffsetFor1DFetchPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerClipDistanceForEsslPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DefeatConstStructArrayLutPass.cpp
MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
MobileGL/MG_Util/SelfTest/DriverPost.cpp
MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp
MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp
@@ -211,6 +230,7 @@ set(SOURCE_FILES
MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp
MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp
MobileGL/MG_Impl/GLImpl/Sync/GL_Sync.cpp
MobileGL/MG_Impl/GLImpl/Query/GL_Query.cpp
MobileGL/MG_Impl/Init.cpp
MobileGL/MG_Impl/GetProcAddress.cpp
@@ -238,6 +258,7 @@ set(SOURCE_FILES
MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferObject.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/VkTimerQueryManager.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/VkClearManager.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp
@@ -270,6 +291,28 @@ set(SOURCE_FILES
MobileGL/MG_State/GLState/RenderbufferState/RenderbufferState.cpp
)
if (APPLE AND NOT MOBILEGL_IOS)
list(APPEND SOURCE_FILES
MobileGL/MG_Impl/CGLImpl/CGLImpl.cpp
MobileGL/MG_Impl/CGLImpl/Exporting/Definitions.cpp
MobileGL/MG_Impl/DyldInterpose/DyldInterpose.cpp
MobileGL/MG_Impl/NSOpenGLImpl/NSOpenGLImpl.cpp
)
endif()
if (ANDROID)
list(APPEND SOURCE_FILES
MobileGL/MG_Util/SelfTest/DriverPostJni.cpp
)
endif()
if (WIN32)
list(APPEND SOURCE_FILES
MobileGL/MG_Impl/WGLImpl/WGLImpl.cpp
MobileGL/MG_Impl/WGLImpl/Exporting/Definitions.cpp
)
endif()
set(MOBILEGL_LINK_LIBRARIES
glslang::glslang
spirv-cross-c
@@ -298,10 +341,18 @@ set(MOBILEGL_INCLUDE_DIR
${SPIRV-Headers_SOURCE_DIR}/include
)
add_library(${CMAKE_PROJECT_NAME} SHARED
add_library(${CMAKE_PROJECT_NAME} SHARED
${SOURCE_FILES}
)
if (WIN32)
# The wgl* entry points are exported via .def (see the comment in wgl.def);
# only the shared library links it.
target_sources(${CMAKE_PROJECT_NAME} PRIVATE
MobileGL/MG_Impl/WGLImpl/Exporting/wgl.def
)
endif()
if (CMAKE_BUILD_TYPE STREQUAL "Debug")
set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES
C_VISIBILITY_PRESET default
@@ -328,8 +379,34 @@ target_link_libraries(${CMAKE_PROJECT_NAME}
target_compile_definitions(${CMAKE_PROJECT_NAME}
PUBLIC
${MOBILEGL_COMPILE_DEF}
MOBILEGL_LOG_ACTIVE_LEVEL=${MOBILEGL_LOG_ACTIVE_LEVEL}
$<$<BOOL:${MOBILEGL_TRACE_ANGLE_VARIANTS}>:MOBILEGL_TRACE_ANGLE_VARIANTS=1>
)
if(UNIX AND NOT APPLE AND NOT ANDROID)
foreach(MOBILEGL_LOADER_ALIAS
libEGL.so libEGL.so.1)
add_custom_command(TARGET ${CMAKE_PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E create_symlink
"$<TARGET_FILE_NAME:${CMAKE_PROJECT_NAME}>"
"$<TARGET_FILE_DIR:${CMAKE_PROJECT_NAME}>/${MOBILEGL_LOADER_ALIAS}"
COMMENT "Creating ${MOBILEGL_LOADER_ALIAS} alias for Linux GL/EGL loaders"
)
endforeach()
endif()
if(WIN32)
# Drop-in for the classic GL loader path: a copy named opengl32.dll placed
# next to a host executable is what LoadLibrary("opengl32.dll") and gdi32's
# pixel-format forwarding will resolve.
add_custom_command(TARGET ${CMAKE_PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"$<TARGET_FILE:${CMAKE_PROJECT_NAME}>"
"$<TARGET_FILE_DIR:${CMAKE_PROJECT_NAME}>/opengl32.dll"
COMMENT "Creating opengl32.dll drop-in copy"
)
endif()
if(NOT ANDROID)
add_library(${CMAKE_PROJECT_NAME}_s STATIC
${SOURCE_FILES}
@@ -361,6 +438,7 @@ if(NOT ANDROID)
target_compile_definitions(${CMAKE_PROJECT_NAME}_s
PUBLIC
${MOBILEGL_COMPILE_DEF}
MOBILEGL_LOG_ACTIVE_LEVEL=${MOBILEGL_LOG_ACTIVE_LEVEL}
)
endif()
@@ -379,7 +457,48 @@ if (ANDROID)
)
endif()
if (NOT ANDROID)
if (APPLE AND NOT MOBILEGL_IOS)
target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC
"-framework Cocoa"
"-framework QuartzCore"
"-framework Foundation"
"-framework OpenGL"
objc)
if(TARGET ${CMAKE_PROJECT_NAME}_s)
target_link_libraries(${CMAKE_PROJECT_NAME}_s PUBLIC
"-framework Cocoa"
"-framework QuartzCore"
"-framework Foundation"
"-framework OpenGL"
objc)
endif()
endif()
if (APPLE AND MOBILEGL_IOS)
target_compile_definitions(${CMAKE_PROJECT_NAME} PUBLIC MOBILEGL_IOS=1 _LIBCPP_DISABLE_AVAILABILITY)
target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC
"-framework CoreGraphics"
"-framework Foundation"
"-framework QuartzCore"
objc)
if (MOBILEGL_VULKAN_LIBRARY)
target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC "${MOBILEGL_VULKAN_LIBRARY}")
endif()
if(TARGET ${CMAKE_PROJECT_NAME}_s)
target_compile_definitions(${CMAKE_PROJECT_NAME}_s PUBLIC MOBILEGL_IOS=1 _LIBCPP_DISABLE_AVAILABILITY)
target_link_libraries(${CMAKE_PROJECT_NAME}_s PUBLIC
"-framework CoreGraphics"
"-framework Foundation"
"-framework QuartzCore"
objc)
if (MOBILEGL_VULKAN_LIBRARY)
target_link_libraries(${CMAKE_PROJECT_NAME}_s PUBLIC "${MOBILEGL_VULKAN_LIBRARY}")
endif()
endif()
endif()
if (NOT ANDROID AND NOT MOBILEGL_IOS)
find_package(Vulkan)
if (Vulkan_FOUND)
target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC Vulkan::Vulkan Vulkan::Headers)
@@ -387,6 +506,17 @@ if (NOT ANDROID)
target_include_directories(${CMAKE_PROJECT_NAME} PUBLIC ${Vulkan_INCLUDE_DIR})
target_include_directories(${CMAKE_PROJECT_NAME}_s PUBLIC ${Vulkan_INCLUDE_DIR})
endif ()
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)
@@ -395,4 +525,8 @@ if (NOT ANDROID)
if (MOBILEGL_BUILD_BENCHMARK)
add_subdirectory(MobileGL/MG_Benchmark)
endif()
if (MOBILEGL_BUILD_TRACE_REPLAY)
add_subdirectory(tools/trace_replay)
endif()
endif()
+80 -1
View File
@@ -14,9 +14,88 @@ namespace MobileGL::MG_Config {
inline const String ProjectName = "MobileGL";
inline const String CoreName = "MobileGL Core";
inline const String CoreVendor = "MobileGL-Dev (BZLZHH, Swung0x48, Tungsten)";
inline const Version CoreVersion = {26, 3, 0, "-dev", VersionType::Development};
inline const Version CoreVersion = {26, 7, 0, "-dev", VersionType::Development};
inline const VersionStringFormatAttrib DefaultVersionStringFormatAttrib = {2, 2, 0, true, true};
inline const Uint64 CacheVersion = 0;
extern BackendType ActiveBackendType;
// Tri-state override for device-specific quirks: Auto lets the detected device decide,
// ForceOn/ForceOff bypass the detection in either direction. ForceOn only bypasses the
// device gate - each quirk keeps its structural safety checks.
enum class QuirkOverride : Uint8 {
Auto = 0,
ForceOn,
ForceOff,
};
// Feature toggles parsed once from environment variables in MG_ConfigLoader::Init()
// (ConfigLoader.cpp), before the accepted-env map is destroyed. All Bool fields share
// one truthy rule: the variable is set, non-empty, not "0", and not "false"
// (case-insensitive).
//
// Env variables intentionally NOT mirrored here (kept as live std::getenv at their
// call sites):
// - DISPLAY: X11 session variable, not MobileGL configuration.
// - MOBILEGL_LOG_FILE_PATH: log-file init runs before MG_ConfigLoader::Init
// (see MG_Util/Debug/Log.cpp).
struct FeaturesTable {
// MOBILEGL_DISABLE_TIMERQUERY: do not advertise or use GPU timer queries.
Bool DisableTimerQuery = false;
// MOBILEGL_USE_ANGLE: load ANGLE EGL/GLES libraries.
Bool UseAngle = false;
#if defined(MOBILEGL_TRACE_ANGLE_VARIANTS)
// MOBILEGL_TRACE_ANGLE_VARIANT: signed trace-APK ANGLE build short hash.
String TraceAngleVariant;
#endif
// MOBILEGL_DISABLE_SUBGROUP: force-disable Vulkan shader subgroup support.
Bool DisableSubgroup = false;
// MOBILEGL_MAGMA_R11G11B10F_FALLBACK: use fallback format for R11G11B10F on Vulkan.
Bool MagmaR11G11B10FFallback = false;
// MOBILEGL_MAGMA_FRAMESINFLIGHT: requested Magma frames in flight, defaulting to 3.
Uint32 MagmaFramesInFlight = 3;
// MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER: avoid mipmap min filters in samplers,
// resolves certain rendering bugs on ANGLE + llvmpipe.
Bool AvoidSamplerMipmapMinFilter = false;
// MOBILEGL_COHERENT_AS_FLUSH: app-compat for engines (e.g. Flywheel) that write
// GPU-read data through persistent GL_MAP_FLUSH_EXPLICIT_BIT maps they never
// flush. Persistent FLUSH_EXPLICIT map requests are rewritten to coherent
// semantics: writes reach the backend without glFlushMappedBufferRange, and
// flush calls on rewritten maps become error-free no-ops. Non-persistent maps
// keep spec FLUSH_EXPLICIT behavior.
Bool CoherentAsFlush = false;
// MOBILEGL_TRACE_SKIP_AUTODESTROY: skip teardown in the ELF destructor (Init.cpp).
Bool TraceSkipAutodestroy = false;
// MOBILEGL_DISABLE_UBO_RING: force the DirectGLES global-UBO upload back to the
// 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;
// MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN: overrides the shader-source quirk that
// rewrites the recognized workgroup prefix-scan template on Qualcomm devices with
// subgroups wider than 32 lanes (see ShaderSourceProcessor's quirk registry).
QuirkOverride SubgroupPrefixScanQuirk = QuirkOverride::Auto;
// MOBILEGL_QUIRK_CLIP_DISTANCE: overrides the DirectGLES quirk that lowers
// gl_ClipDistance for Adreno's ESSL compiler (shadow Private arrays with
// constant-index builtin flushes, dynamic-index gl_in copy loop, redeclaration
// strip, and const struct-array LUT splitting). Auto detects Qualcomm.
QuirkOverride ClipDistanceQuirk = QuirkOverride::Auto;
// MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE: overrides the DirectVulkan quirk that
// strips depth writes from accumulation-blended pipelines (MIN/MAX or additive
// ONE+ONE - the multi-pass depth-equality signature) on drivers without
// cross-pipeline vertex position invariance. Sorted-transparency "over" blends,
// gl_FragDepth writers, and fully color-masked attachments are exempt (see
// PipelineFactory::ShouldSuppressDepthWrite). Auto detects Qualcomm.
QuirkOverride MagmaDisableBlendedDepthWriteQuirk = QuirkOverride::Auto;
// MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS: leave the Vulkan robustBufferAccess device
// feature off. It is enabled by default to match GL's defined out-of-range fetch
// behavior; this escape hatch exists to measure or dodge its GPU cost on a device.
Bool DisableRobustBufferAccess = false;
};
extern FeaturesTable Features;
} // namespace MobileGL::MG_Config
+86
View File
@@ -8,6 +8,19 @@
#include "Config.h"
#include <cerrno>
#include <cstdlib>
#ifndef _WIN32
extern char** environ;
#endif
namespace MobileGL::MG_Config {
// Zero/default-initialized at static-init time (all fields have constexpr-friendly
// defaults), so it is safe to read even if MG_ConfigLoader::Init has not run yet.
FeaturesTable Features;
} // namespace MobileGL::MG_Config
namespace MobileGL::MG_ConfigLoader {
static UniquePtr<UnorderedMap<String, String>> acceptedEnvVariablesMap;
@@ -56,6 +69,78 @@ namespace MobileGL::MG_ConfigLoader {
}
}
// Unified truthy rule for boolean feature env variables: set, non-empty, not "0",
// and not "false" (case-insensitive).
static Bool IsTruthyValue(const String& value) {
if (value.empty() || value == "0") {
return false;
}
String lowered = value;
std::transform(lowered.begin(), lowered.end(), lowered.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return lowered != "false";
}
inline Bool QueryEnvFlag(const String& key) {
auto it = acceptedEnvVariablesMap->find(key);
return it != acceptedEnvVariablesMap->end() && IsTruthyValue(it->second);
}
// Quirk overrides are tri-state: an unset variable keeps device auto-detection, a truthy
// value forces the quirk on, anything else set ("0", "false", "") forces it off.
inline MG_Config::QuirkOverride QueryEnvQuirkOverride(const String& key) {
auto it = acceptedEnvVariablesMap->find(key);
if (it == acceptedEnvVariablesMap->end()) {
return MG_Config::QuirkOverride::Auto;
}
return IsTruthyValue(it->second) ? MG_Config::QuirkOverride::ForceOn
: MG_Config::QuirkOverride::ForceOff;
}
inline Uint32 QueryEnvUint32(const String& key, Uint32 defaultValue, Uint32 minValue, Uint32 maxValue) {
auto it = acceptedEnvVariablesMap->find(key);
if (it == acceptedEnvVariablesMap->end()) {
return defaultValue;
}
const String& value = it->second;
char* parseEnd = nullptr;
errno = 0;
const unsigned long parsedValue = std::strtoul(value.c_str(), &parseEnd, 10);
if (parseEnd == value.c_str() || *parseEnd != '\0' || errno == ERANGE || parsedValue < minValue ||
parsedValue > maxValue) {
MGLOG_W("Config: Ignoring invalid env variable %s='%s'; expected an integer in range [%u, %u], "
"using default %u",
key.c_str(), value.c_str(), minValue, maxValue, defaultValue);
return defaultValue;
}
return static_cast<Uint32>(parsedValue);
}
inline void InitFeatures() {
auto& features = MG_Config::Features;
features.DisableTimerQuery = QueryEnvFlag("MOBILEGL_DISABLE_TIMERQUERY");
features.UseAngle = QueryEnvFlag("MOBILEGL_USE_ANGLE");
#if defined(MOBILEGL_TRACE_ANGLE_VARIANTS)
QueryEnvVariable("MOBILEGL_TRACE_ANGLE_VARIANT", features.TraceAngleVariant, "");
#endif
features.DisableSubgroup = QueryEnvFlag("MOBILEGL_DISABLE_SUBGROUP");
features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK");
features.MagmaFramesInFlight = QueryEnvUint32("MOBILEGL_MAGMA_FRAMESINFLIGHT", 3, 1, 64);
features.AvoidSamplerMipmapMinFilter =
QueryEnvFlag("MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER");
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");
features.SubgroupPrefixScanQuirk = QueryEnvQuirkOverride("MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN");
features.ClipDistanceQuirk = QueryEnvQuirkOverride("MOBILEGL_QUIRK_CLIP_DISTANCE");
features.MagmaDisableBlendedDepthWriteQuirk =
QueryEnvQuirkOverride("MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE");
features.DisableRobustBufferAccess = QueryEnvFlag("MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS");
}
inline void InitBackendType() {
String backendTypeStr;
QueryEnvVariable("MOBILEGL_BACKEND_TYPE", backendTypeStr, "DirectGLES");
@@ -77,6 +162,7 @@ namespace MobileGL::MG_ConfigLoader {
InitializeAcceptedEnvVariables();
InitBackendType();
InitFeatures();
// Destroy the map since we won't need it anymore
acceptedEnvVariablesMap.reset();
+18 -9
View File
@@ -32,9 +32,14 @@
#define MOBILEGL_GLX_API MOBILEGL_API
#define MOBILEGL_GL_API MOBILEGL_API
#define MOBILEGL_EGL_API MOBILEGL_API
#define MOBILEGL_CGL_API MOBILEGL_API
#define MOBILEGL_NSOPENGL_API MOBILEGL_API
#define MOBILEGL_WGL_API MOBILEGL_API
// ====================== MobileGL configurations ======================= //
#define MOBILEGL_LOG_ACTIVE_LEVEL MOBILEGL_LOG_LEVEL_DEBUG
#ifndef MOBILEGL_LOG_ACTIVE_LEVEL
#define MOBILEGL_LOG_ACTIVE_LEVEL MOBILEGL_LOG_LEVEL_INFO
#endif
#define MOBILEGL_LOG_ENABLE_CONSOLE 0
#define MOBILEGL_LOG_ENABLE_FILE 1
@@ -63,11 +68,15 @@
#endif
// =============================== Utils ================================ //
#define MOBILEGL_ASSERT(condition, ...) \
do { \
if (!(condition)) { \
MGLOG_F("Assertion failed" __VA_OPT__(": ") __VA_ARGS__); \
MGLOG_F(" at %s:%d (%s)", __FILE__, __LINE__, __func__); \
TRAP; \
} \
} while (0)
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
#define MOBILEGL_ASSERT(condition, ...) \
do { \
if (!(condition)) { \
MGLOG_F("Assertion failed" __VA_OPT__(": ") __VA_ARGS__); \
MGLOG_F(" at %s:%d (%s)", __FILE__, __LINE__, __func__); \
TRAP; \
} \
} while (0)
#else
#define MOBILEGL_ASSERT(condition, ...)
#endif
+7 -1
View File
@@ -14,7 +14,13 @@ namespace MobileGL {
} // namespace MG_Config
namespace MG_Backend {
UniquePtr<BackendObject> pActiveBackendObject;
// Leak-at-exit storage: the UniquePtr itself lives on the heap and is
// never destroyed by the runtime, so process exit runs no backend
// destructors (static destruction order across TUs is undefined).
// Deterministic teardown happens inside the EGL lifecycle instead:
// the last eglTerminate calls MobileGL::Destroy(), which .reset()s
// these singletons while the process is still healthy.
UniquePtr<BackendObject>& pActiveBackendObject = *new UniquePtr<BackendObject>();
GlobalBackendFunctionsTable gBackendFunctionsTable;
} // namespace MG_Backend
} // namespace MobileGL
+24
View File
@@ -32,6 +32,8 @@
#include <thread>
#include <vector>
#include <cassert>
#include <climits>
#include <cstdlib>
#include <cstdarg>
#include <cstring>
#include <numeric>
@@ -108,10 +110,32 @@
#define VK_USE_PLATFORM_WIN32_KHR
#elif defined(__APPLE__)
#define VK_USE_PLATFORM_METAL_EXT
#elif defined(__linux__)
#define VK_USE_PLATFORM_XLIB_KHR
typedef struct _XDisplay Display;
typedef unsigned long XID;
typedef XID Window;
typedef unsigned long VisualID;
#else
#warning "VK_USE_PLATFORM_*_KHR not defined for this platform!"
#endif
#if defined(VK_USE_PLATFORM_XLIB_KHR)
#pragma push_macro("Bool")
#pragma push_macro("None")
#pragma push_macro("Always")
#pragma push_macro("Status")
#pragma push_macro("LSBFirst")
#pragma push_macro("DestroyAll")
#endif
#include <vulkan/vulkan.h>
#if defined(VK_USE_PLATFORM_XLIB_KHR)
#pragma pop_macro("DestroyAll")
#pragma pop_macro("LSBFirst")
#pragma pop_macro("Status")
#pragma pop_macro("Always")
#pragma pop_macro("None")
#pragma pop_macro("Bool")
#endif
#ifdef TRACY_ENABLE
#include <tracy/Tracy.hpp>
+72 -34
View File
@@ -9,13 +9,55 @@
#include "Init.h"
#include "Config.h"
#include <MG_Backend/BackendObjects.h>
#include <MG_Backend/DirectVulkan/DirectVulkan.h>
#include <MG_State/GLState/Core.h>
#include <MG_State/EGLState/Core.h>
#include <MG_Impl/GLImpl/Texture/ProxyTexture.h>
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
#include <atomic>
#include <mutex>
namespace MobileGL {
namespace {
std::atomic<Bool> g_isInitialized = false;
thread_local Bool tl_initializing = false;
std::mutex& InitMutex() {
static std::mutex mutex;
return mutex;
}
void DestroyImpl(Bool logLifecycle) {
if (!g_isInitialized) {
return;
}
if (logLifecycle) {
MGLOG_I("MobileGL closing...");
}
glslang::FinalizeProcess();
MG_Backend::pActiveBackendObject.reset();
MG_State::pGLContext.reset();
MG_State::pEGLContext.reset();
MG_Impl::GLImpl::TextureImpl::pProxyTextureManager.reset();
MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo.reset();
MG_Backend::gBackendFunctionsTable = {};
g_isInitialized = false;
if (logLifecycle) {
MG_Util::Debug::Close();
}
// TODO: add and use Destroy functions for other subsystems
}
}
void Initialize() {
if (g_isInitialized) {
MGLOG_D("MobileGL already initialized; skipping duplicate Initialize()");
return;
}
MG_Util::Debug::InitFile();
MGLOG_I("Initializing MobileGL...");
MG_ConfigLoader::Init();
@@ -28,43 +70,39 @@ namespace MobileGL {
MGLOG_D("MG_Impl initialized");
glslang::InitializeProcess();
MGLOG_D("glslang initialized");
g_isInitialized = true;
MGLOG_I("MobileGL initialized");
}
void Destroy() {
MGLOG_I("MobileGL closing...");
glslang::FinalizeProcess();
MG_State::pGLContext.reset();
MG_State::pEGLContext.reset();
MG_Impl::GLImpl::TextureImpl::pProxyTextureManager.reset();
MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo.reset();
MG_Util::Debug::Close();
// TODO: add and use Destroy functions for other subsystems
}
#if defined(__linux__) || defined(__APPLE__)
__attribute__((constructor)) static void AutoInit() {
Initialize();
}
__attribute__((destructor)) static void AutoDestroy() {
Destroy();
}
#endif
#ifdef _WIN32
BOOL WINAPI DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
switch (ul_reason_for_call) {
case DLL_PROCESS_ATTACH:
Initialize();
break;
case DLL_PROCESS_DETACH:
Destroy();
break;
void EnsureInitialized() {
if (g_isInitialized.load(std::memory_order_acquire)) {
return;
}
return TRUE;
// Re-entrant call while this thread is already inside Initialize()
// (e.g. an init step routing back through a public entry point).
if (tl_initializing) {
return;
}
const std::lock_guard<std::mutex> lock(InitMutex());
if (g_isInitialized.load(std::memory_order_acquire)) {
return;
}
tl_initializing = true;
Initialize();
tl_initializing = false;
}
#endif
void Destroy() {
DestroyImpl(true);
}
// MobileGL's lifecycle is owned entirely by the host-API layers
// (EGL/WGL/CGL): initialization happens lazily on the first entry point
// via EnsureInitialized(), and full teardown happens deterministically
// when the last EGL display is terminated with nothing current (EGLImpl
// calls Destroy()). There is intentionally no static constructor, no
// static destructor, and no DllMain: the global singletons use
// leak-at-exit storage (see GlobalObjects.cpp), so a process that exits
// without eglTerminate simply leaks them to the OS instead of running
// backend destructors during static teardown.
} // namespace MobileGL
+6
View File
@@ -11,6 +11,12 @@
namespace MobileGL {
void Initialize();
// Thread-safe, idempotent, and re-entrant wrapper around Initialize().
// Host layers (EGL/WGL/CGL entry points) call this lazily on first use so
// MobileGL's lifecycle never depends on ELF/DLL static constructors, and
// so a fresh init can follow a full Destroy() (e.g. after the last
// eglTerminate).
void EnsureInitialized();
void Destroy();
namespace MG_Util::Debug {
+370 -16
View File
@@ -7,18 +7,164 @@
// End of Source File Header
#include "BackendObject.h"
#include "MG_Util/Converters/MGToStr/TextureEnumConverter.h"
#include <algorithm>
#include <cstring>
#include <iomanip>
#include <sstream>
namespace MobileGL::MG_Backend {
namespace {
Bool IsReleaseCurrentRequest(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) {
return dpy == EGL_NO_DISPLAY && draw == EGL_NO_SURFACE && read == EGL_NO_SURFACE && ctx == EGL_NO_CONTEXT;
(void)dpy;
return draw == EGL_NO_SURFACE && read == EGL_NO_SURFACE && ctx == EGL_NO_CONTEXT;
}
std::thread::id CurrentThreadKey() {
return std::this_thread::get_id();
}
const char* GetFormatCapabilitySupportString(const FormatCapabilityCache& cache,
SizeT targetIndex,
SizeT formatIndex,
FormatCapability capability) {
if (HasFormatCapability(cache.FullCaps[targetIndex][formatIndex], capability)) return "Full";
if (HasFormatCapability(cache.CaveatCaps[targetIndex][formatIndex], capability)) return "Caveat";
return "None";
}
SizeT GetPrintedFormatNameWidth() {
SizeT width = 0;
for (SizeT formatIndex = 0; formatIndex < kFormatCapabilityFormatCount; ++formatIndex) {
const auto format = static_cast<TextureInternalFormat>(formatIndex);
width = std::max(width, MG_Util::ConvertTextureInternalFormatToString(format).size());
}
return width;
}
SizeT GetCapabilityColumnWidth(FormatCapability capability) {
SizeT width = std::strlen(GetFormatCapabilityName(capability));
width = std::max<SizeT>(width, std::strlen("Caveat"));
return width;
}
String BuildFormatCapabilityHeader(SizeT formatNameWidth) {
std::ostringstream line;
line << std::left << std::setw(static_cast<Int>(formatNameWidth)) << "";
for (FormatCapability capability : kReportedFormatCapabilities) {
line << " | " << std::left << std::setw(static_cast<Int>(GetCapabilityColumnWidth(capability)))
<< GetFormatCapabilityName(capability);
}
return line.str();
}
String BuildFormatCapabilityRow(const FormatCapabilityCache& cache,
SizeT targetIndex,
SizeT formatIndex,
SizeT formatNameWidth) {
const auto format = static_cast<TextureInternalFormat>(formatIndex);
std::ostringstream line;
line << std::left << std::setw(static_cast<Int>(formatNameWidth))
<< MG_Util::ConvertTextureInternalFormatToString(format);
for (FormatCapability capability : kReportedFormatCapabilities) {
line << " | " << std::left << std::setw(static_cast<Int>(GetCapabilityColumnWidth(capability)))
<< GetFormatCapabilitySupportString(cache, targetIndex, formatIndex, capability);
}
return line.str();
}
} // namespace
void FormatCapabilityCache::Clear() {
for (auto& row : FullCaps) {
row.fill(FormatCapabilityFlags{});
}
for (auto& row : CaveatCaps) {
row.fill(FormatCapabilityFlags{});
}
for (auto& row : SampleCounts) {
for (auto& counts : row) {
counts.clear();
}
}
}
Bool HasFormatCapability(FormatCapabilityFlags caps, FormatCapability capability) {
return static_cast<Bool>(caps & capability);
}
SizeT GetFormatCapabilityTargetIndex(TextureTarget target) {
if (target == TextureTarget::Unknown || static_cast<Int>(target) < 0 ||
static_cast<SizeT>(target) >= kFormatCapabilityTextureTargetCount) {
return kFormatCapabilityTargetCount;
}
return static_cast<SizeT>(target);
}
SizeT GetRenderbufferFormatCapabilityTargetIndex() {
return kFormatCapabilityRenderbufferTargetIndex;
}
const char* GetFormatCapabilityName(FormatCapability capability) {
switch (capability) {
case FormatCapability::Creatable:
return "Creatable";
case FormatCapability::Sampled:
return "Sampled";
case FormatCapability::LinearFilter:
return "LinearFilter";
case FormatCapability::GenerateMipmap:
return "GenerateMipmap";
case FormatCapability::TextureGather:
return "TextureGather";
case FormatCapability::TextureShadow:
return "TextureShadow";
case FormatCapability::FramebufferRenderable:
return "FramebufferRenderable";
case FormatCapability::FramebufferLayered:
return "FramebufferLayered";
case FormatCapability::MultisampleTexture:
return "MultisampleTexture";
case FormatCapability::MultisampleRenderbuffer:
return "MultisampleRenderbuffer";
case FormatCapability::ColorAttachment:
return "ColorAttachment";
case FormatCapability::DepthAttachment:
return "DepthAttachment";
case FormatCapability::StencilAttachment:
return "StencilAttachment";
case FormatCapability::TextureBuffer:
return "TextureBuffer";
}
return "Unknown";
}
String GetFormatCapabilityTargetName(SizeT targetIndex) {
if (targetIndex == kFormatCapabilityRenderbufferTargetIndex) {
return "Renderbuffer";
}
if (targetIndex >= kFormatCapabilityTextureTargetCount) {
return "Unknown";
}
return MG_Util::ConvertTextureTargetToString(static_cast<TextureTarget>(targetIndex));
}
void PrintFormatCapabilities(const FormatCapabilityCache& cache) {
const SizeT formatNameWidth = GetPrintedFormatNameWidth();
MGLOG_D("Backend format capabilities:");
for (SizeT targetIndex = 0; targetIndex < kFormatCapabilityTargetCount; ++targetIndex) {
MGLOG_D("");
const String targetName = GetFormatCapabilityTargetName(targetIndex);
MGLOG_D("- %s", targetName.c_str());
const String header = BuildFormatCapabilityHeader(formatNameWidth);
MGLOG_D("%s", header.c_str());
for (SizeT formatIndex = 0; formatIndex < kFormatCapabilityFormatCount; ++formatIndex) {
const String row = BuildFormatCapabilityRow(cache, targetIndex, formatIndex, formatNameWidth);
MGLOG_D("%s", row.c_str());
}
}
}
Bool BackendObject::InitializeEGLDisplay(EGLDisplay dpy, EGLint* major, EGLint* minor) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
if (dpy == EGL_NO_DISPLAY) {
@@ -42,28 +188,116 @@ namespace MobileGL::MG_Backend {
return true;
}
Bool BackendObject::CreateEGLWindowSurface(const WindowHandle& handle) {
Bool BackendObject::CreateEGLWindowSurface(EGLSurface surface, const WindowHandle& handle) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
return RegisterEGLWindowSurface(surface, handle) && ActivateEGLSurface(surface);
}
Bool BackendObject::ResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
auto surfaceIt = m_eglSurfaces.find(surface);
if (surfaceIt == m_eglSurfaces.end() || surfaceIt->second.Kind != SurfaceKind::Window) {
MGLOG_E("ResizeEGLWindowSurface failed: no window surface is initialized");
return false;
}
surfaceIt->second.Window.Width = width;
surfaceIt->second.Window.Height = height;
if (m_eglSurface == surface) {
m_windowHandle.Width = width;
m_windowHandle.Height = height;
}
return true;
}
Bool BackendObject::CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
return RegisterEGLPbufferSurface(surface, width, height) && ActivateEGLSurface(surface);
}
Bool BackendObject::RegisterEGLWindowSurface(EGLSurface surface, const WindowHandle& handle) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
if (!m_eglDisplayInitialized) {
MGLOG_E("CreateEGLWindowSurface failed: EGL display is not initialized");
MGLOG_E("RegisterEGLWindowSurface failed: EGL display is not initialized");
return false;
}
if (surface == EGL_NO_SURFACE) {
MGLOG_E("RegisterEGLWindowSurface failed: invalid EGLSurface");
return false;
}
if (handle.Backend == WindowBackend::Unknown || !handle.Handle) {
MGLOG_E("CreateEGLWindowSurface failed: invalid native window handle");
MGLOG_E("RegisterEGLWindowSurface failed: invalid native window handle");
return false;
}
if (m_eglWindowSurfaceInitialized && m_windowHandle.Backend == handle.Backend && m_windowHandle.Handle == handle.Handle) {
auto& state = m_eglSurfaces[surface];
state = EGLSurfaceState{
.Kind = SurfaceKind::Window,
.Window = handle,
.Width = static_cast<EGLint>(std::max<Uint32>(handle.Width, 1)),
.Height = static_cast<EGLint>(std::max<Uint32>(handle.Height, 1)),
};
return true;
}
Bool BackendObject::RegisterEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
if (!m_eglDisplayInitialized) {
MGLOG_E("RegisterEGLPbufferSurface failed: EGL display is not initialized");
return false;
}
if (surface == EGL_NO_SURFACE) {
MGLOG_E("RegisterEGLPbufferSurface failed: invalid EGLSurface");
return false;
}
if (width <= 0 || height <= 0) {
MGLOG_E("RegisterEGLPbufferSurface failed: invalid size %dx%d", width, height);
return false;
}
m_eglSurfaces[surface] = EGLSurfaceState{
.Kind = SurfaceKind::Pbuffer,
.Width = width,
.Height = height,
};
return true;
}
const BackendObject::EGLSurfaceState* BackendObject::GetRegisteredEGLSurface(EGLSurface surface) const {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
auto surfaceIt = m_eglSurfaces.find(surface);
return surfaceIt == m_eglSurfaces.end() ? nullptr : &surfaceIt->second;
}
Bool BackendObject::ActivateEGLSurface(EGLSurface surface) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
const auto* surfaceState = GetRegisteredEGLSurface(surface);
if (!surfaceState) {
MGLOG_E("ActivateEGLSurface failed: EGL surface is not registered");
return false;
}
if (m_eglSurfaceInitialized && m_eglSurface == surface) {
return true;
}
SetWindowHandle(handle);
if (!InitWindowSurface()) {
MGLOG_E("CreateEGLWindowSurface failed: backend InitWindowSurface failed");
if (surfaceState->Kind == SurfaceKind::Window) {
SetWindowHandle(surfaceState->Window);
if (!InitWindowSurface()) {
MGLOG_E("ActivateEGLSurface failed: backend InitWindowSurface failed");
return false;
}
} else if (surfaceState->Kind == SurfaceKind::Pbuffer) {
if (!InitPbufferSurface(surfaceState->Width, surfaceState->Height)) {
MGLOG_E("ActivateEGLSurface failed: backend InitPbufferSurface failed");
return false;
}
} else {
MGLOG_E("ActivateEGLSurface failed: unsupported surface kind");
return false;
}
m_eglWindowSurfaceInitialized = true;
m_eglSurface = surface;
m_eglSurfaceInitialized = true;
m_eglSurfaceKind = surfaceState->Kind;
m_eglCurrentThreads.clear();
m_backendCapabilitiesInitialized = false;
return true;
@@ -73,7 +307,7 @@ namespace MobileGL::MG_Backend {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
const auto threadKey = CurrentThreadKey();
if (IsReleaseCurrentRequest(dpy, draw, read, ctx)) {
m_eglCurrentThreads.erase(threadKey);
ReleaseEGLCurrentThread(threadKey);
return true;
}
@@ -81,8 +315,22 @@ namespace MobileGL::MG_Backend {
MGLOG_E("MakeEGLCurrent failed: EGL display mismatch or not initialized");
return false;
}
if (!m_eglWindowSurfaceInitialized) {
MGLOG_E("MakeEGLCurrent failed: EGL window surface is not initialized");
if (!m_eglSurfaceInitialized) {
if (draw != read || !ActivateEGLSurface(draw)) {
MGLOG_E("MakeEGLCurrent failed: EGL surface is not initialized");
return false;
}
}
if (!GetRegisteredEGLSurface(draw) || !GetRegisteredEGLSurface(read)) {
MGLOG_E("MakeEGLCurrent failed: EGL surface is not registered");
return false;
}
if (draw != read) {
MGLOG_E("MakeEGLCurrent failed: separate draw/read surfaces are not supported");
return false;
}
if (draw != m_eglSurface && !ActivateEGLSurface(draw)) {
MGLOG_E("MakeEGLCurrent failed: EGL surface is not backed by this backend");
return false;
}
if (draw == EGL_NO_SURFACE || read == EGL_NO_SURFACE || ctx == EGL_NO_CONTEXT) {
@@ -98,14 +346,23 @@ namespace MobileGL::MG_Backend {
m_backendCapabilitiesInitialized = true;
}
m_eglCurrentThreads[threadKey] = true;
ReleaseEGLCurrentThread(threadKey);
m_eglCurrentThreads[threadKey] = EGLCurrentState{
.Display = dpy,
.DrawSurface = draw,
.ReadSurface = read,
.Context = ctx,
};
return true;
}
void BackendObject::ResetEGLRuntimeState() {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
m_eglWindowSurfaceInitialized = false;
m_eglSurfaceInitialized = false;
m_backendCapabilitiesInitialized = false;
m_eglSurfaceKind = SurfaceKind::None;
m_eglSurface = EGL_NO_SURFACE;
m_windowHandle = {};
m_eglCurrentThreads.clear();
}
@@ -115,11 +372,17 @@ namespace MobileGL::MG_Backend {
MGLOG_E("SwapEGLBuffers failed: EGL display mismatch or not initialized");
return false;
}
if (m_eglCurrentThreads.find(CurrentThreadKey()) == m_eglCurrentThreads.end()) {
const auto currentIt = m_eglCurrentThreads.find(CurrentThreadKey());
if (currentIt == m_eglCurrentThreads.end()) {
MGLOG_E("SwapEGLBuffers failed: no current context attached");
return false;
}
if (!m_eglWindowSurfaceInitialized || draw == EGL_NO_SURFACE) {
if (currentIt->second.Display != dpy || currentIt->second.DrawSurface != draw ||
currentIt->second.Context == EGL_NO_CONTEXT) {
MGLOG_E("SwapEGLBuffers failed: draw surface is not current on this thread");
return false;
}
if (!m_eglSurfaceInitialized || draw == EGL_NO_SURFACE || draw != m_eglSurface) {
MGLOG_E("SwapEGLBuffers failed: invalid draw surface");
return false;
}
@@ -134,8 +397,99 @@ namespace MobileGL::MG_Backend {
return true;
}
void BackendObject::SetEGLSwapInterval(Int interval) {
const auto& backendFunctions = GetBackendFunctions();
if (backendFunctions.SetSwapInterval) {
backendFunctions.SetSwapInterval(interval);
}
}
Bool BackendObject::IsEGLSurfaceCurrent(EGLSurface surface) const {
if (surface == EGL_NO_SURFACE) {
return false;
}
for (const auto& current : m_eglCurrentThreads) {
if (current.second.DrawSurface == surface || current.second.ReadSurface == surface) {
return true;
}
}
return false;
}
void BackendObject::DestroyPendingEGLSurfaceIfUnused(EGLSurface surface) {
auto surfaceIt = m_eglSurfaces.find(surface);
if (surfaceIt == m_eglSurfaces.end() || !surfaceIt->second.DestroyPending ||
IsEGLSurfaceCurrent(surface)) {
return;
}
m_eglSurfaces.erase(surfaceIt);
if (m_eglSurface == surface) {
OnEGLSurfaceReleased(surface);
ResetEGLRuntimeState();
}
}
void BackendObject::ReleaseEGLCurrentThread(const std::thread::id& threadKey) {
auto currentIt = m_eglCurrentThreads.find(threadKey);
if (currentIt == m_eglCurrentThreads.end()) {
return;
}
const EGLSurface drawSurface = currentIt->second.DrawSurface;
const EGLSurface readSurface = currentIt->second.ReadSurface;
m_eglCurrentThreads.erase(currentIt);
DestroyPendingEGLSurfaceIfUnused(drawSurface);
DestroyPendingEGLSurfaceIfUnused(readSurface);
}
void BackendObject::ReleaseEGLSurface(EGLSurface surface) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
auto surfaceIt = m_eglSurfaces.find(surface);
if (surfaceIt == m_eglSurfaces.end()) {
return;
}
if (IsEGLSurfaceCurrent(surface)) {
surfaceIt->second.DestroyPending = true;
return;
}
m_eglSurfaces.erase(surfaceIt);
if (m_eglSurface == surface) {
OnEGLSurfaceReleased(surface);
ResetEGLRuntimeState();
}
}
void BackendObject::ReleaseEGLResources() {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
ResetEGLRuntimeState();
m_eglSurfaces.clear();
m_eglDisplay = EGL_NO_DISPLAY;
m_eglDisplayInitialized = false;
}
void BackendObject::SetWindowHandle(const WindowHandle& handle) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
m_windowHandle = handle;
}
const FormatCapabilityCache& BackendObject::GetFormatCapabilities() const {
return m_formatCapabilities;
}
FormatCapabilityCache& BackendObject::MutableFormatCapabilities() {
return m_formatCapabilities;
}
Bool BackendObject::InitPbufferSurface(EGLint width, EGLint height) {
(void)width;
(void)height;
return false;
}
void BackendObject::OnEGLSurfaceReleased(EGLSurface surface) {
(void)surface;
}
} // namespace MobileGL::MG_Backend
+289 -4
View File
@@ -8,8 +8,14 @@
#pragma once
#include <Includes.h>
#include "MG_State/GLState/TextureState/TextureEnum.h"
namespace MobileGL {
namespace MG_State::GLState {
class FramebufferObject;
class ITextureObject;
}
enum class BackendType {
DirectGLES,
DirectVulkan,
@@ -18,11 +24,88 @@ namespace MobileGL {
};
namespace MG_Backend {
enum class FormatCapability : Uint64 {
Creatable = 1ull << 0,
Sampled = 1ull << 1,
LinearFilter = 1ull << 2,
GenerateMipmap = 1ull << 3,
TextureGather = 1ull << 4,
TextureShadow = 1ull << 5,
FramebufferRenderable = 1ull << 6,
FramebufferLayered = 1ull << 7,
MultisampleTexture = 1ull << 8,
MultisampleRenderbuffer = 1ull << 9,
ColorAttachment = 1ull << 10,
DepthAttachment = 1ull << 11,
StencilAttachment = 1ull << 12,
TextureBuffer = 1ull << 13
};
using FormatCapabilityFlags = Flags<FormatCapability>;
inline constexpr Array<FormatCapability, 14> kReportedFormatCapabilities = {
FormatCapability::Creatable,
FormatCapability::Sampled,
FormatCapability::LinearFilter,
FormatCapability::GenerateMipmap,
FormatCapability::TextureGather,
FormatCapability::TextureShadow,
FormatCapability::FramebufferRenderable,
FormatCapability::FramebufferLayered,
FormatCapability::MultisampleTexture,
FormatCapability::MultisampleRenderbuffer,
FormatCapability::ColorAttachment,
FormatCapability::DepthAttachment,
FormatCapability::StencilAttachment,
FormatCapability::TextureBuffer,
};
inline constexpr SizeT kFormatCapabilityTextureTargetCount =
static_cast<SizeT>(TextureTarget::TextureTargetCount);
inline constexpr SizeT kFormatCapabilityRenderbufferTargetIndex = kFormatCapabilityTextureTargetCount;
inline constexpr SizeT kFormatCapabilityTargetCount = kFormatCapabilityTextureTargetCount + 1;
inline constexpr SizeT kFormatCapabilityFormatCount =
static_cast<SizeT>(TextureInternalFormat::TextureInternalFormatCount);
using FormatCapabilityTable =
Array<Array<FormatCapabilityFlags, kFormatCapabilityFormatCount>, kFormatCapabilityTargetCount>;
using FormatSampleCountTable =
Array<Array<Vector<Int>, kFormatCapabilityFormatCount>, kFormatCapabilityTargetCount>;
struct FormatCapabilityCache {
FormatCapabilityTable FullCaps{};
FormatCapabilityTable CaveatCaps{};
FormatSampleCountTable SampleCounts{};
void Clear();
};
Bool HasFormatCapability(FormatCapabilityFlags caps, FormatCapability capability);
SizeT GetFormatCapabilityTargetIndex(TextureTarget target);
SizeT GetRenderbufferFormatCapabilityTargetIndex();
const char* GetFormatCapabilityName(FormatCapability capability);
String GetFormatCapabilityTargetName(SizeT targetIndex);
void PrintFormatCapabilities(const FormatCapabilityCache& cache);
// Opaque backend fence-sync handle, created by GLFunctionsTable::FenceSync
// and released by GLFunctionsTable::DeleteSync.
using BackendSyncHandle = void*;
// Opaque backend timer-query handle, created by
// GLFunctionsTable::BeginTimeElapsedQuery / QueryCounterTimestamp and
// released by GLFunctionsTable::DeleteBackendQuery.
using BackendQueryHandle = void*;
struct GLFunctionsTable {
void (*DrawArrays)(GLenum mode, GLint first, GLsizei count);
void (*DrawElements)(GLenum mode, GLsizei count, GLenum type, const void* indices);
void (*DrawElementsBaseVertex)(GLenum mode, GLsizei count, GLenum type, const void* indices,
GLint basevertex);
void (*MultiDrawArrays)(GLenum mode, const GLint* first, const GLsizei* count, GLsizei drawcount);
void (*MultiDrawElements)(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount);
void (*MultiDrawElementsBaseVertex)(GLenum mode, const GLsizei* count, GLenum type,
@@ -31,6 +114,10 @@ namespace MobileGL {
void (*MultiDrawElementsIndirect)(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount,
GLsizei stride);
void (*MultiDrawArraysIndirect)(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride);
void (*MultiDrawElementsIndirectCount)(GLenum mode, GLenum type, const void* indirect,
GLintptr drawcount, GLsizei maxdrawcount, GLsizei stride);
void (*MultiDrawArraysIndirectCount)(GLenum mode, const void* indirect, GLintptr drawcount,
GLsizei maxdrawcount, GLsizei stride);
void (*DrawRangeElementsBaseVertex)(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type,
const void* indices, GLint basevertex);
void (*DrawRangeElements)(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type,
@@ -54,29 +141,180 @@ namespace MobileGL {
void (*ClearBufferfv)(GLenum buffer, GLint drawbuffer, const GLfloat* value);
void (*ClearBufferuiv)(GLenum buffer, GLint drawbuffer, const GLuint* value);
void (*ClearBufferiv)(GLenum buffer, GLint drawbuffer, const GLint* value);
void (*ClearNamedFramebufferfv)(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
GLenum buffer, GLint drawbuffer, const GLfloat* value);
void (*ClearNamedFramebufferfi)(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
void (*BlitFramebuffer)(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0,
GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter);
void (*BlitNamedFramebuffer)(const SharedPtr<MG_State::GLState::FramebufferObject>& readFramebuffer,
const SharedPtr<MG_State::GLState::FramebufferObject>& drawFramebuffer,
GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
GLbitfield mask, GLenum filter);
void (*CopyTexImage2D)(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width,
GLsizei height, GLint border);
void (*CopyTexSubImage2D)(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y,
GLsizei width, GLsizei height);
void (*CopyImageSubData)(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
void (*GenerateMipmap)(GLenum target);
void (*ReadPixels)(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type,
void* pixels);
void (*GetTexImage)(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels);
void (*GetTextureImage)(const SharedPtr<MG_State::GLState::ITextureObject>& texture,
TextureUploadTarget uploadTarget, GLint level, GLenum format, GLenum type,
GLsizei bufSize, GLvoid* pixels);
void (*DispatchCompute)(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ);
void (*DispatchComputeIndirect)(GLintptr indirect);
void (*MemoryBarrier)(GLbitfield barriers);
void (*MemoryBarrierByRegion)(GLbitfield barriers);
void (*BindImageTexture)(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer,
GLenum access, GLenum format);
void (*GetIntegeri_v)(GLenum target, GLuint index, GLint* data);
void (*GetInteger64i_v)(GLenum target, GLuint index, GLint64* data);
void (*GetProgramiv)(GLuint program, GLenum pname, GLint* params);
void (*GetProgramInterfaceiv)(GLuint program, GLenum programInterface, GLenum pname, GLint* params);
GLuint (*GetProgramResourceIndex)(GLuint program, GLenum programInterface, const GLchar* name);
void (*GetProgramResourceName)(GLuint program, GLenum programInterface, GLuint index, GLsizei bufSize,
GLsizei* length, GLchar* name);
void (*GetProgramResourceiv)(GLuint program, GLenum programInterface, GLuint index, GLsizei propCount,
const GLenum* props, GLsizei bufSize, GLsizei* length, GLint* params);
GLint (*GetProgramResourceLocation)(GLuint program, GLenum programInterface, const GLchar* name);
GLint (*GetProgramResourceLocationIndex)(GLuint program, GLenum programInterface, const GLchar* name);
void (*ShaderStorageBlockBinding)(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding);
// GL fence sync objects. All entries are optional (may be null); the
// frontend then falls back to always-signaled sync semantics.
// FenceSync may itself return null when the backend cannot create a
// fence right now (e.g. the calling thread does not own the backend
// context); the frontend treats such a sync as always signaled.
BackendSyncHandle (*FenceSync)();
GLenum (*ClientWaitSync)(BackendSyncHandle sync, GLbitfield flags, GLuint64 timeout);
void (*WaitSync)(BackendSyncHandle sync, GLbitfield flags, GLuint64 timeout);
void (*DeleteSync)(BackendSyncHandle sync);
Bool (*GetSyncStatus)(BackendSyncHandle sync); // true = signaled
// GL timer-query objects (GL_ARB_timer_query). All entries are
// optional (may be null); the frontend then falls back to zero
// results and reports GL_QUERY_COUNTER_BITS == 0.
// BeginTimeElapsedQuery / QueryCounterTimestamp may themselves
// return null when the backend cannot create a query right now;
// the frontend treats such a query as immediately available with
// a zero result.
// Dynamic support check: true only when the live backend can
// actually time at the moment of the call (extension / entry
// points / timestamp valid bits are known then, not at table
// init). Gates the advertised GL_QUERY_COUNTER_BITS.
Bool (*IsTimerQuerySupported)();
BackendQueryHandle (*BeginTimeElapsedQuery)(); // starts a TIME_ELAPSED span
void (*EndTimeElapsedQuery)(BackendQueryHandle query); // ends the span
BackendQueryHandle (*QueryCounterTimestamp)(); // glQueryCounter(GL_TIMESTAMP) one-shot
Bool (*IsQueryResultAvailable)(BackendQueryHandle query); // non-blocking
// Returns true when a final value was produced (*outNanoseconds
// written; the frontend may cache it and release the handle).
// Returns false when the result could not be obtained YET - e.g.
// a Vulkan wait that refuses to block on a not-yet-submitted
// frame serial - in which case the frontend must keep the handle
// and leave the query readable later.
Bool (*GetQueryResult64)(BackendQueryHandle query, Bool wait, Uint64* outNanoseconds);
void (*DeleteBackendQuery)(BackendQueryHandle query);
Int64 (*GetGpuTimestampNs)(); // glGetInteger64v(GL_TIMESTAMP); 0 if unsupported
};
struct GlobalBackendFunctionsTable {
GLFunctionsTable GL;
void (*Present)();
// Optional: applies the app-requested eglSwapInterval to the native
// presentation path (null = backend keeps its own pacing policy).
void (*SetSwapInterval)(Int interval);
};
// Coarse GPU vendor identity for gating device-specific quirks. Detected from the
// Vulkan physical-device vendorID or the GLES GL_VENDOR/GL_RENDERER strings; stays
// Unknown when detection is inconclusive, in which case auto-gated quirks stay off.
enum class GpuVendorKind : Uint8 {
Unknown = 0,
Qualcomm,
Arm,
Nvidia,
Amd,
Intel,
ImgTec,
// Software rasterizers (llvmpipe/lavapipe, SwiftShader).
Software,
};
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;
Float SmoothLineWidthRangeMax = 1.0f;
Float SmoothLineWidthGranularity = 1.0f;
Float PointSizeRangeMin = 1.0f;
Float PointSizeRangeMax = 1.0f;
Float PointSizeGranularity = 1.0f;
Int Max3DTextureSize = 16384;
Int MaxArrayTextureLayers = 2048;
Int MaxCubeMapTextureSize = 16384;
Int MaxFramebufferWidth = 16384;
Int MaxFramebufferHeight = 16384;
Int MaxFramebufferLayers = 2048;
Int MaxRenderbufferSize = 16384;
Int MaxTextureSize = 16384;
Int MaxColorTextureSamples = 1;
Int MaxDepthTextureSamples = 1;
Int MaxFramebufferSamples = 1;
Int MaxIntegerSamples = 1;
Int MaxSamples = 1;
Int MaxSampleMaskWords = 1;
Int MaxTextureImageUnits = 32;
Int MaxVertexTextureImageUnits = 32;
Int MaxComputeTextureImageUnits = 32;
Int MaxCombinedTextureImageUnits = 192;
Int MaxVertexAttribs = 16;
Int MaxComputeShaderStorageBlocks = 8;
Int MaxCombinedShaderStorageBlocks = 32;
Int MaxComputeUniformBlocks = 12;
Int MaxComputeWorkGroupInvocations = 128;
Int MaxShaderStorageBufferBindings = 8;
Int MaxTextureBufferSize = 65536;
Int MaxUniformBufferBindings = 24;
Int MaxUniformBlockSize = 16384;
Int MaxImageUnits = 8;
Int MaxCombinedImageUniforms = 8;
Int MaxVertexImageUniforms = 0;
Int MaxGeometryImageUniforms = 0;
Int MaxFragmentImageUniforms = 8;
Int MaxComputeImageUniforms = 8;
Int MaxDrawBuffers = 8;
Int MaxColorAttachments = 8;
Int MaxClipDistances = 8;
Int MaxViewports = 16;
Int MaxViewportWidth = 16384;
Int MaxViewportHeight = 16384;
Float ViewportBoundsRangeMin = 0.0f;
Float ViewportBoundsRangeMax = 0.0f;
Int ViewportSubpixelBits = 0;
Bool SupportsWideLines = false;
SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024;
Uint32 SubgroupSize = 0;
Uint32 SubgroupSupportedStages = 0;
Uint32 SubgroupSupportedFeatures = 0;
Bool SubgroupQuadOperationsInAllStages = false;
GpuVendorKind GpuVendor = GpuVendorKind::Unknown;
};
enum class WindowBackend {
Android,
// TODO: X11, Wayland, Windows, macOS, etc.
X11,
MetalLayer,
Win32, // Handle is an HWND
// TODO: Wayland, etc.
WindowBackendCount,
Unknown = -1
};
@@ -84,6 +322,8 @@ namespace MobileGL {
struct WindowHandle {
WindowBackend Backend = WindowBackend::Unknown;
void* Handle = nullptr;
Uint32 Width = 0;
Uint32 Height = 0;
};
class BackendObject {
@@ -95,9 +335,16 @@ namespace MobileGL {
virtual Bool InitWindowSurface() = 0;
virtual Bool InitializeEGLDisplay(EGLDisplay dpy, EGLint* major, EGLint* minor);
virtual Bool CreateEGLWindowSurface(const WindowHandle& handle);
virtual Bool CreateEGLWindowSurface(EGLSurface surface, const WindowHandle& handle);
virtual Bool ResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height);
virtual Bool CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height);
virtual Bool MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx);
virtual Bool SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw);
// Forwards the app-requested eglSwapInterval to the backend's native
// presentation path (no-op for backends without a SetSwapInterval hook).
virtual void SetEGLSwapInterval(Int interval);
virtual void ReleaseEGLSurface(EGLSurface surface);
virtual void ReleaseEGLResources();
void SetWindowHandle(const WindowHandle& handle);
@@ -105,18 +352,56 @@ namespace MobileGL {
virtual String GetBackendAPIVersionString() const = 0;
virtual const GlobalBackendFunctionsTable& GetBackendFunctions() const = 0;
virtual const DynamicBackendParameters& GetDynamicParameters() const = 0;
const FormatCapabilityCache& GetFormatCapabilities() const;
virtual BackendType GetBackendType() const = 0;
protected:
enum class SurfaceKind {
None,
Window,
Pbuffer
};
struct EGLCurrentState {
EGLDisplay Display = EGL_NO_DISPLAY;
EGLSurface DrawSurface = EGL_NO_SURFACE;
EGLSurface ReadSurface = EGL_NO_SURFACE;
EGLContext Context = EGL_NO_CONTEXT;
};
struct EGLSurfaceState {
SurfaceKind Kind = SurfaceKind::None;
Bool DestroyPending = false;
WindowHandle Window;
EGLint Width = 1;
EGLint Height = 1;
};
void ResetEGLRuntimeState();
Bool RegisterEGLWindowSurface(EGLSurface surface, const WindowHandle& handle);
Bool RegisterEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height);
const EGLSurfaceState* GetRegisteredEGLSurface(EGLSurface surface) const;
Bool ActivateEGLSurface(EGLSurface surface);
virtual Bool InitPbufferSurface(EGLint width, EGLint height);
virtual void OnEGLSurfaceReleased(EGLSurface surface);
FormatCapabilityCache& MutableFormatCapabilities();
mutable std::recursive_mutex m_eglStateMutex;
FormatCapabilityCache m_formatCapabilities;
WindowHandle m_windowHandle;
EGLDisplay m_eglDisplay = EGL_NO_DISPLAY;
EGLSurface m_eglSurface = EGL_NO_SURFACE;
Bool m_eglDisplayInitialized = false;
Bool m_eglWindowSurfaceInitialized = false;
Bool m_eglSurfaceInitialized = false;
Bool m_backendCapabilitiesInitialized = false;
UnorderedMap<std::thread::id, Bool> m_eglCurrentThreads;
SurfaceKind m_eglSurfaceKind = SurfaceKind::None;
UnorderedMap<std::thread::id, EGLCurrentState> m_eglCurrentThreads;
UnorderedMap<EGLSurface, EGLSurfaceState> m_eglSurfaces;
private:
Bool IsEGLSurfaceCurrent(EGLSurface surface) const;
void DestroyPendingEGLSurfaceIfUnused(EGLSurface surface);
void ReleaseEGLCurrentThread(const std::thread::id& threadKey);
};
} // namespace MG_Backend
} // namespace MobileGL
+1 -1
View File
@@ -13,6 +13,6 @@
#include "DirectVulkan/BackendObject_DirectVulkan.h"
namespace MobileGL::MG_Backend {
extern UniquePtr<BackendObject> pActiveBackendObject;
extern UniquePtr<BackendObject>& pActiveBackendObject;
extern GlobalBackendFunctionsTable gBackendFunctionsTable;
} // namespace MobileGL::MG_Backend
File diff suppressed because it is too large Load Diff
@@ -12,6 +12,12 @@
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
namespace MobileGL::MG_Backend::DirectGLES {
// Populates the same format-capability cache used by backend startup. The caller
// must keep the supplied GLES context current for the duration of this call.
void PopulateFormatCapabilities(const MG_External::GLESFunctionsTable& gl,
const MG_External::GLESCapabilities& capabilities,
FormatCapabilityCache& cache);
class BackendObject_DirectGLES : public BackendObject {
public:
~BackendObject_DirectGLES() override;
@@ -20,9 +26,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
Bool InitCapabilities() override;
Bool InitWindowSurface() override;
Bool InitializeEGLDisplay(EGLDisplay dpy, EGLint* major, EGLint* minor) override;
Bool CreateEGLWindowSurface(const WindowHandle& handle) override;
Bool CreateEGLWindowSurface(EGLSurface surface, const WindowHandle& handle) override;
Bool CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height) override;
Bool MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) override;
Bool SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) override;
void ReleaseEGLSurface(EGLSurface surface) override;
void ReleaseEGLResources() override;
const RendererInfo& GetRendererInfo() const override;
String GetBackendAPIVersionString() const override;
@@ -32,9 +41,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
const MG_External::GLESFunctionsTable& GetGLESFunctions() const;
const MG_External::EGLFunctionsTable& GetEGLFunctions() const;
void ApplyGLESCapabilitiesForTesting(const MG_External::GLESCapabilities& capabilities);
private:
void UpdateDynamicBackendParameters();
Bool InitPbufferSurface(EGLint width, EGLint height) override;
void OnEGLSurfaceReleased(EGLSurface surface) override;
Bool m_initialized = false;
MG_External::EGLFunctionsTable m_EGLFunctions;
@@ -42,4 +54,25 @@ namespace MobileGL::MG_Backend::DirectGLES {
MG_External::GLESCapabilities m_GLESCapabilities;
DynamicBackendParameters m_dynamicParameters;
};
// Single-source-of-truth helpers shared with the driver POST
// (MG_Util/SelfTest/DriverPost.cpp), so the identity strings and extension list
// MobileGL reports to applications on this backend cannot drift from what the
// POST screen shows.
// Static identity of the Espryt renderer (renderer/backend names, target GL/GLSL
// versions, ExtraVendor). The Extensions vector inside is live backend state that
// is reconciled after capability init; callers that need the advertised list for
// a known capability set must use BuildAdvertisedExtensions instead.
const RendererInfo& GetRendererIdentity();
// The full OpenGL extension list Espryt advertises (glGetString(GL_EXTENSIONS))
// 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
// inside the application-visible GL_RENDERER string).
String FormatBackendAPIVersionString(const String& glesRendererString, Int glesMajor, Int glesMinor);
} // namespace MobileGL::MG_Backend::DirectGLES
File diff suppressed because it is too large Load Diff
@@ -8,6 +8,8 @@
#pragma once
#include <Includes.h>
#include <MG_Backend/BackendObject.h>
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
#include <MG_State/GLState/TextureState/TextureState.h>
#include <MG_State/GLState/SamplerState/SamplerObject.h>
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
@@ -17,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);
@@ -25,11 +31,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices);
void DrawArrays(GLenum mode, GLint first, GLsizei count);
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const GLvoid* indices, GLint basevertex);
void MultiDrawArrays(GLenum mode, const GLint* first, const GLsizei* count, GLsizei drawcount);
void MultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount);
void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount, const GLint* basevertex);
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride);
void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount,
GLsizei maxdrawcount, GLsizei stride);
void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride);
void DrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type,
const void* indices, GLint basevertex);
@@ -46,18 +55,100 @@ namespace MobileGL::MG_Backend::DirectGLES {
GLuint baseinstance);
void DrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei instancecount);
void DrawArraysIndirect(GLenum mode, const void* indirect);
void ClearNamedFramebufferfv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
GLenum buffer, GLint drawbuffer, const GLfloat* value);
void ClearNamedFramebufferfi(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1,
GLint dstY1, GLbitfield mask, GLenum filter);
void BlitNamedFramebuffer(const SharedPtr<MG_State::GLState::FramebufferObject>& readFramebuffer,
const SharedPtr<MG_State::GLState::FramebufferObject>& drawFramebuffer,
GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
GLbitfield mask, GLenum filter);
void CopyTexImage2D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width,
GLsizei height, GLint border);
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
GLsizei height);
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
void GenerateMipmap(GLenum target);
const GLubyte* GetString(GLenum name);
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels);
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels);
void DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ);
void DispatchComputeIndirect(GLintptr indirect);
void MemoryBarrier(GLbitfield barriers);
void MemoryBarrierByRegion(GLbitfield barriers);
void BindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access,
GLenum format);
void GetIntegeri_v(GLenum target, GLuint index, GLint* data);
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data);
void GetProgramiv(GLuint program, GLenum pname, GLint* params);
void GetProgramInterfaceiv(GLuint program, GLenum programInterface, GLenum pname, GLint* params);
GLuint GetProgramResourceIndex(GLuint program, GLenum programInterface, const GLchar* name);
void GetProgramResourceName(GLuint program, GLenum programInterface, GLuint index, GLsizei bufSize, GLsizei* length,
GLchar* name);
void GetProgramResourceiv(GLuint program, GLenum programInterface, GLuint index, GLsizei propCount,
const GLenum* props, GLsizei bufSize, GLsizei* length, GLint* params);
GLint GetProgramResourceLocation(GLuint program, GLenum programInterface, const GLchar* name);
GLint GetProgramResourceLocationIndex(GLuint program, GLenum programInterface, const GLchar* name);
void ShaderStorageBlockBinding(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding);
Bool InitWindowSurface(NativeWindowType window);
Bool InitPbufferSurface(EGLint width, EGLint height);
Bool MakeCurrent();
Bool ReleaseCurrent();
// True when the backend ES context is current on the calling thread, i.e.
// immediate buffer ops may issue GL calls right now.
Bool IsBackendContextCurrentOnThisThread();
// GL fence sync objects, backed by native ES fences. FenceSync returns null
// (the frontend then falls back to an always-signaled sync) when the calling
// thread does not own the ES context. Waits/queries degrade to "signaled" in
// the same situation, and handles created under a since-destroyed ES context
// are always treated as signaled.
BackendSyncHandle FenceSync();
GLenum ClientWaitSync(BackendSyncHandle sync, GLbitfield flags, GLuint64 timeout);
void WaitSync(BackendSyncHandle sync, GLbitfield flags, GLuint64 timeout);
void DeleteSync(BackendSyncHandle sync);
Bool GetSyncStatus(BackendSyncHandle sync);
// True when GL_EXT_disjoint_timer_query and every entry point the timer
// hooks below need are present. Also gates the E_GL_ARB_timer_query
// advertisement in BackendObject_DirectGLES::InitCapabilities, and is
// registered as the GLFunctionsTable::IsTimerQuerySupported hook: a pure
// capability read needs no current ES context, and it stays false until
// the ES capabilities have been filled in.
Bool AreTimerQueriesSupported();
// GL timer-query objects, backed by GL_EXT_disjoint_timer_query. The
// creators return null (the frontend then falls back to an immediately
// available zero result) when the calling thread does not own the ES
// context or the extension/entry points are missing, and handles created
// under a since-destroyed ES context are always treated as complete with
// a zero result (mirrors the fence-sync handles above).
BackendQueryHandle BeginTimeElapsedQuery();
void EndTimeElapsedQuery(BackendQueryHandle query);
BackendQueryHandle QueryCounterTimestamp();
Bool IsQueryResultAvailable(BackendQueryHandle query);
// Returns true when a final value landed in *outNanoseconds (a zero for
// null or stale-generation handles IS final: the frontend may cache it
// and release the handle). Returns false only when the calling thread
// does not own the ES context, so the value is genuinely unobtainable
// right now; the handle stays alive and readable later.
Bool GetQueryResult64(BackendQueryHandle query, Bool wait, Uint64* outNanoseconds);
void DeleteBackendQuery(BackendQueryHandle query);
Int64 GetGpuTimestampNs();
void Present();
// Frame-completion watermarks for the buffer-storage pool: CurrentFrameSerial()
// is bumped once per Present(); CompletedFrameSerial() is the newest frame whose
// GPU work has provably finished (advanced by polling a one-fence-per-frame ring).
// A buffer retired during frame N is safe to recycle once CompletedFrameSerial() >= N.
Uint64 CurrentFrameSerial();
Uint64 CompletedFrameSerial();
// Applies (or defers until the window surface exists) the app-requested
// eglSwapInterval on the native EGL surface.
void SetSwapInterval(Int interval);
void SetEGLFuncsTable(const MG_External::EGLFunctionsTable& eglFuncs);
void SetGLESFuncsTable(const MG_External::GLESFunctionsTable& glesFuncs);
void SetGLESCapabilities(const MG_External::GLESCapabilities& capabilities);
File diff suppressed because it is too large Load Diff
+393 -22
View File
@@ -8,13 +8,19 @@
#pragma once
#include <Includes.h>
#include <atomic>
#include <mutex>
#include "DirectGLES.h"
#include "MG_State/GLState/SamplerState/SamplerObject.h"
#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);
String PromoteDrawParameterGlobalsToUniforms(String source, GLenum shaderType);
template <typename StateObject, typename BackendObject>
class StateBackendObjectRegistry {
public:
@@ -50,6 +56,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
return const_cast<StateBackendObjectRegistry*>(this)->find(stateObj);
}
iterator begin() { return m_backendObjects.begin(); }
const_iterator begin() const { return m_backendObjects.begin(); }
iterator end() { return m_backendObjects.end(); }
const_iterator end() const { return m_backendObjects.end(); }
@@ -111,38 +119,150 @@ namespace MobileGL::MG_Backend::DirectGLES {
namespace BufferImpl {
const GLenum TempBufferTarget = GL_ARRAY_BUFFER;
class BackendBufferObject {
// The DirectGLES storage behind one frontend buffer. Owned (refcounted) by
// the frontend BufferObject; immediate BufferBackendOps keep it current, so
// draw-time "sync" reduces to ensuring the storage exists.
class GLESBufferResource : public MG_State::GLState::BackendBufferResource {
public:
BackendBufferObject();
void SyncToBackend(const SharedPtr<MG_State::GLState::BufferObject>& stateBufferObject);
Uint GetBackendBufferId() const { return m_backendBufferId; }
void Bind(GLenum target = TempBufferTarget);
~GLESBufferResource() override = default;
private:
void SyncToBackend_glBufferData(const SharedPtr<MG_State::GLState::BufferObject>& stateBufferObject);
void SyncToBackend_glBufferSubData(const SharedPtr<MG_State::GLState::BufferObject>& stateBufferObject);
void SyncToBackend_glMapBufferRange(const SharedPtr<MG_State::GLState::BufferObject>& stateBufferObject,
Bool invalidate = true, Bool unsynchronized = true);
Uint m_backendBufferId = 0;
SizeT m_prevBufferSize = 0;
Bool m_isInitialized = false;
Uint id = 0;
SizeT storageSize = 0;
Bool storageInitialized = false;
// ES context generation this resource's id belongs to; ids from a
// destroyed context are invalid and must not be deleted or reused.
Uint contextGeneration = 0;
// Frontend change serial the backend storage reflects. When immediate
// ops cannot run (ops unregistered, no current context), this lags and
// EnsureBufferResource falls back to a full re-upload. Atomic: read on
// the context-owning thread while ops on other threads may update it.
std::atomic<Uint64> syncedChangeSerial{0};
// Ops that arrived while no ES context was current on the calling thread
// (or before storage existed); replayed by EnsureBufferResource. The ES
// context migrates between app threads, so deferring ops can race with
// the owning thread replaying them: guard both fields with pendingMutex.
Bool pendingRespecify = false;
VecRange1D pendingRanges;
std::mutex pendingMutex;
// Zero-copy coherent persistent map (EXT_buffer_storage): the GL store is
// immutable, persistently+coherently mapped, and persistentPtr is what the app
// (and the frontend PipeResource) write into directly. While set, draw-time
// sync is a no-op and no per-draw glBufferSubData is issued. Cleared on ES
// context loss.
Bool persistentMapped = false;
void* persistentPtr = nullptr;
};
extern BackendBufferObject* g_boundVertexBufferObject;
extern StateBackendObjectRegistry<MG_State::GLState::BufferObject, BackendBufferObject> g_backendBufferObjects;
// Registered as the frontend's BufferBackendOps at backend init and on
// every MakeCurrent (the ES context can be destroyed and recreated, e.g.
// by the trace replayer's probe context).
void RegisterBufferBackendOps();
void UnregisterBufferBackendOps();
// The ES context died: unregister ops, invalidate all outstanding GL ids
// (they belonged to the dead context) and drop deferred deletes.
void OnBackendContextDestroyed();
// Get-or-create the backend resource and bring its storage up to date
// (creates the GL buffer, replays pending ops, pushes persistent-mapped
// ranges). Requires the ES context to be current. Returns nullptr only
// for null input.
GLESBufferResource* EnsureBufferResource(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject);
// Existing resource or nullptr; performs no GL calls.
GLESBufferResource* GetBufferResource(MG_State::GLState::BufferObject* bufferObject);
// Deletes GL buffers whose owning frontend objects died (possibly on a
// thread without a current ES context). Called from draw-time sync.
void ProcessDeferredBufferReleases();
// glBindBuffer with a redundant-bind cache for GL_ARRAY_BUFFER.
void BindBufferId(GLenum target, Uint id);
void InvalidateArrayBufferBindingCache();
// Redundant-bind caches for the driver-level GL_PIXEL_PACK/UNPACK_BUFFER
// bindings. Every backend readback (glReadPixels / pack-PBO map) and pixel
// upload site routes its binding through these so the shadow always matches
// the driver; the resting state between operations is 0, which keeps any
// path that implicitly assumes "no PBO bound" correct. Scrubbed when a
// buffer id is deleted/pooled (GL resets a deleted buffer's bindings to 0,
// and a recycled name matching the shadow would false-skip the rebind) and
// invalidated on MakeCurrent (context may reset).
void BindPixelPackBufferId(Uint id);
void BindPixelUnpackBufferId(Uint id);
void InvalidatePixelBufferBindingCaches();
// A GL buffer id is being deleted by code outside BufferImpl (e.g. the VAO
// client-attribute staging buffers): scrub every buffer-binding shadow that
// could false-skip when the name is recycled.
void NoteBufferIdDeleted(Uint id);
// Redundant-bind cache for INDEXED buffer bindings (glBindBufferBase/Range on
// GL_UNIFORM_BUFFER / GL_SHADER_STORAGE_BUFFER): skips the GL call when the
// (id, range) already at that index matches, like the array-buffer/texture/
// sampler caches already do. Invalidated on MakeCurrent (context may reset).
void BindBufferBaseCached(GLenum glTarget, Uint index, Uint id);
void BindBufferRangeCached(GLenum glTarget, Uint index, Uint id, GLintptr offset, GLsizeiptr size);
void InvalidateIndexedBufferBindingCache();
// Buffer-storage pool maintenance. TrimBufferPool evicts over-budget entries
// (called once per frame from Present); ClearBufferPool drops all pooled ids
// without glDeleteBuffers (called when the ES context is going away).
void TrimBufferPool();
void ClearBufferPool();
// --- Global-UBO ring ------------------------------------------------------
// One persistently+coherently mapped buffer (EXT_buffer_storage) shared by
// every program's lowered default-uniform block. Each content change is
// bump-allocated into a fresh slot and bound with glBindBufferRange, so the
// CPU never rewrites bytes the GPU may still be reading — the per-draw
// glBufferSubData into one static UBO forced Adreno to resolve that
// write-after-read hazard on every uniform-dirtying draw (MC dirties
// uniforms every draw). Reclamation rides the Present() frame-fence
// watermark; no ring bytes are recycled before their frame's GPU work
// completed.
//
// A program's cached slot, reusable within one frame while the frontend UBO
// content version is unchanged. Cross-frame reuse is intentionally not
// attempted: later same-frame allocations may recycle bytes of completed
// frames, so re-referencing them would need per-bind pinning — rewriting
// GetUBOSize() bytes once per program per frame is far cheaper.
struct UboRingAllocation {
Uint32 contentVersion = ~0u; // frontend UBO content version held at `offset`
Uint32 ringGeneration = 0; // ring identity the slot lives in (0 = never valid)
Uint64 frameSerial = ~Uint64{0}; // frame the slot was written in
SizeT offset = 0;
};
// False when the feature is disabled, EXT_buffer_storage / fences are
// missing, the ES context is not current, or ring creation already failed
// under this context (callers then take the legacy glBufferSubData path).
Bool UboRingAvailable();
// Bump-allocate `size` bytes aligned to GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT.
// Grows the ring (new GL store, generation bump) when the in-flight span
// would be overrun. Returns false when storage (re)creation fails.
Bool UboRingAllocate(SizeT size, SizeT& outOffset);
void* UboRingMappedPtr();
Uint UboRingBufferId();
Uint32 UboRingGeneration();
// Present()-time upkeep: records the frame's high-water mark for reclamation
// and deletes grown-away ring stores once the GPU is done with them.
void UboRingOnPresent();
} // namespace BufferImpl
namespace VertexArrayImpl {
class BackendVertexArrayObject {
public:
BackendVertexArrayObject();
~BackendVertexArrayObject();
void SyncToBackend(const SharedPtr<MG_State::GLState::VertexArrayObject>& stateVAOObject);
void SyncClientSideAttributesForDrawArrays(
const SharedPtr<MG_State::GLState::VertexArrayObject>& stateVAOObject, GLint first, GLsizei count);
Uint GetBackendVertexArrayId() const { return m_backendVAOId; }
void Bind() const;
private:
Uint m_backendVAOId = 0;
Array<Uint, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS> m_clientAttributeBufferIds;
// Attribs the frontend has Enabled but that have no source at all (no buffer object
// and NULL client pointer). GL keeps such attribs latently enabled, but Adreno's ES
// driver treats them as client arrays and memcpys from address 0 at draw time
// (SIGSEGV), so they are kept disabled on the backend VAO until they gain a source.
Uint32 m_forceDisabledAttribsMask = 0;
Bool m_isInitialized = false;
Uint16 m_syncedIndexBufferVersion = 0;
Array<MG_State::GLState::VertexAttributeVersion, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS>
@@ -155,11 +275,57 @@ namespace MobileGL::MG_Backend::DirectGLES {
namespace TextureImpl {
inline Bool IsSupportedTextureTarget(TextureTarget target) {
if (target == TextureTarget::Texture1D || target == TextureTarget::TextureRectangle ||
target == TextureTarget::Texture2DMultisampleArray || target == TextureTarget::Texture1DArray ||
target == TextureTarget::Texture2DMultisample || 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) {
return target == TextureTarget::Texture2DMultisample ||
target == TextureTarget::Texture2DMultisampleArray;
}
inline Bool SupportsWrapR(TextureTarget target) {
return target == TextureTarget::Texture3D || target == TextureTarget::TextureCubeMap;
}
struct StateTextureBasicInfo { // Used for tracking texture state changes
@@ -169,11 +335,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
SizeT depth = 0;
SizeT mipmapLevels = 0;
Uint bufferExternalIndex = 0;
Int samples = 0;
Bool fixedSampleLocations = true;
bool operator==(const StateTextureBasicInfo& other) const {
return internalFormat == other.internalFormat && width == other.width && height == other.height &&
depth == other.depth && mipmapLevels == other.mipmapLevels &&
bufferExternalIndex == other.bufferExternalIndex;
bufferExternalIndex == other.bufferExternalIndex && samples == other.samples &&
fixedSampleLocations == other.fixedSampleLocations;
}
bool operator!=(const StateTextureBasicInfo& other) const { return !(*this == other); }
@@ -183,15 +352,29 @@ namespace MobileGL::MG_Backend::DirectGLES {
class BackendTextureObject {
public:
BackendTextureObject();
// Deletes the GL texture (frontend glDeleteTextures used to leak every
// backend id for the context lifetime) and scrubs the binding/scratch-FBO
// shadows so a recycled name or heap address cannot false-skip a rebind.
~BackendTextureObject();
BackendTextureObject(const BackendTextureObject&) = delete;
BackendTextureObject& operator=(const BackendTextureObject&) = delete;
void SyncMipmapsToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
void SyncBuiltinSamplerToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
void SyncTextureParamsToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
void RequireImageBindableStorage();
void Bind(GLenum target, Uint unit = TempTextureUnit);
Uint GetBackendTextureId() const;
private:
void RecreateBackendTexture();
Uint m_backendTextureId = 0;
// ES context generation the id was created under; a dtor running after
// that context died must not delete a foreign (recycled) name.
Uint m_contextGeneration = 0;
Bool m_isInitialized = false;
Bool m_imageBindableStorageRequired = false;
Bool m_backendStorageImmutable = false;
StateTextureBasicInfo m_prevTextureInfo;
SamplerParameters m_cacheSamplerParameters;
UintVec2 m_cacheLodRange = {0, 1000};
@@ -206,10 +389,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
void UnbindTexture(Uint unit, GLenum target);
extern StateBackendObjectRegistry<MG_State::GLState::ITextureObject, BackendTextureObject>
g_backendTextureObjects;
SharedPtr<BackendTextureObject>& SyncTextureObjectToBackend(
const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
Bool imageBindableStorageRequired = false);
extern Array<Array<BackendTextureObject*, (SizeT)TextureTarget::TextureTargetCount>,
MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS>
g_boundTexturesCache;
extern Uint g_activeTextureUnit;
// Bumped when the backend ES context is destroyed; texture ids stamped with
// an older generation belong to a dead context and must not be deleted.
extern Uint g_textureContextGeneration;
} // namespace TextureImpl
namespace FramebufferImpl {
@@ -218,6 +407,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
BackendFramebufferObject();
void SyncToBackend(const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject,
FramebufferTarget asTarget);
// Apply only this FBO's read buffer (glReadBuffer) to the backend. Split out so it can
// still run when SyncCurrentFBO skips the READ-target sync because the same GL FBO is
// bound as both draw and read (otherwise glReadBuffer changes would be silently dropped).
void SyncReadBufferToBackend(const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject);
void InvalidateSyncedState();
Uint GetBackendFramebufferId() const { return m_backendFBOId; }
void Bind(FramebufferTarget target) const;
// FramebufferAttachmentType GetCompactedAttachmentTypeAtDrawBufferIndex(Int index);
@@ -250,24 +444,200 @@ 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;
// Driver-level READ/DRAW framebuffer-binding shadow. Every backend
// glBindFramebuffer routes through BindFramebufferId so scoped helpers can
// save/restore the current binding without a glGetIntegerv round-trip (that
// query forces a driver pipeline sync) and so redundant rebinds no-op.
// Starts unknown; the first CurrentFramebufferBinding() query pins it from
// the driver once. Invalidated on MakeCurrent (context may reset).
// GL_FRAMEBUFFER binds both targets.
void BindFramebufferId(GLenum fbTarget, Uint id);
Uint CurrentFramebufferBinding(FramebufferTarget target);
void InvalidateFramebufferBindingCache();
} // namespace FramebufferImpl
// Shared scratch framebuffers for the readback/copy/blit emulation paths, with a
// driver-side attachment shadow: repeated uses skip redundant detach/attach GL
// calls, and an attachment left by one use (e.g. a depth copy's DEPTH_STENCIL
// texture) is detached exactly when a later use of another aspect would
// otherwise inherit it (stale cross-aspect attachments made the shared temp FBO
// incomplete and silently degraded later readbacks).
namespace ScratchFBOImpl {
struct ScratchFramebuffer {
Uint id = 0;
// false => attachment state unknown; scrub every point on next use.
// A fresh FBO starts with nothing attached, so creation sets it true.
Bool attachmentsKnown = false;
Uint colorTex = 0;
GLenum colorTarget = 0;
GLint colorLevel = 0;
GLint colorLayer = -1; // >= 0 => attached via glFramebufferTextureLayer
Uint depthTex = 0;
GLenum depthTarget = 0;
GLint depthLevel = 0;
Bool depthHasStencil = false;
// Per-FBO read/draw buffer state (0 = unknown, set on first use).
GLenum readBuffer = 0;
GLenum drawBuffer = 0;
};
ScratchFramebuffer& TempFramebuffer(); // GetTexImage READ / CopyTex*Image2D depth DRAW
ScratchFramebuffer& BlitReadFramebuffer(); // texture-to-texture blit source
ScratchFramebuffer& BlitDrawFramebuffer(); // texture-to-texture blit destination
// Returns the GL id, generating it if needed (requires a current ES context).
Uint EnsureId(ScratchFramebuffer& fb);
// The fb must currently be bound at fbTarget (glReadBuffer/glDrawBuffers
// target the READ/DRAW binding respectively). Each Ensure* performs the
// minimal detach/attach set and keeps the shadow in sync; a failed attach
// records the point as detached so the completeness check fails instead of
// silently reading a stale attachment.
void EnsureColorAttachment2D(ScratchFramebuffer& fb, GLenum fbTarget, Uint tex, GLenum texTarget, GLint level);
void EnsureColorAttachmentLayer(ScratchFramebuffer& fb, GLenum fbTarget, Uint tex, GLint level, GLint layer);
void EnsureDepthAttachment2D(ScratchFramebuffer& fb, GLenum fbTarget, Uint tex, GLenum texTarget, GLint level,
Bool withStencil);
void EnsureNoColorAttachment(ScratchFramebuffer& fb, GLenum fbTarget);
void EnsureNoDepthAttachment(ScratchFramebuffer& fb, GLenum fbTarget);
void EnsureReadBuffer(ScratchFramebuffer& fb, GLenum readBuffer);
void EnsureDrawBuffer(ScratchFramebuffer& fb, GLenum drawBuffer);
// A 1x1 RGBA8-renderbuffer-complete FBO (GenerateMipmap needs a complete
// binding while respecifying texture storage). Attachment is set once at
// creation and never changes.
Uint EnsureCompleteTinyFramebufferId();
// A backend texture id is being deleted or respecified: a scratch FBO still
// referencing it would hold a dangling attachment (ES only auto-detaches
// from the *bound* framebuffer), and a recycled name could false-skip a
// re-attach; force a full scrub on next use.
void NoteTextureIdDeleted(Uint textureId);
// The ES context (and the scratch FBO ids with it) is going away.
void OnBackendContextDestroyed();
} // namespace ScratchFBOImpl
// Driver-level GL_PACK_* pixel-store shadow, the readback-side sibling of the
// upload path's ScopedDefaultUnpackState (Managers.cpp): the backend PACK state
// is written ONLY through ApplyPackState, so scoped helpers can save/restore it
// from the shadow instead of glGetIntegerv (which forces a driver pipeline
// sync), and redundant glPixelStorei calls no-op. The first Apply/Current call
// pins the driver to the shadow by writing all fields once. Invalidated on
// MakeCurrent (context may reset). PACK_IMAGE_HEIGHT/SKIP_IMAGES/SWAP_BYTES/
// LSB_FIRST have no ES equivalents; readbacks honor them on the CPU from the
// frontend context state instead.
namespace PixelStoreImpl {
struct PackState {
GLint Alignment = 4;
GLint RowLength = 0;
GLint SkipRows = 0;
GLint SkipPixels = 0;
Bool operator==(const PackState& o) const {
return Alignment == o.Alignment && RowLength == o.RowLength && SkipRows == o.SkipRows &&
SkipPixels == o.SkipPixels;
}
};
void ApplyPackState(const PackState& desired);
PackState CurrentPackState();
void InvalidatePackStateCache();
} // namespace PixelStoreImpl
// Image uniforms take their unit from the layout(binding=N) qualifier baked into
// the transpiled ESSL; unlike samplers they must not (and in ES cannot) be
// assigned through glUniform1i.
inline Bool IsImageUniformType(GLenum type) {
switch (type) {
case 0x904D: /*GL_IMAGE_2D*/
case 0x904E: /*GL_IMAGE_3D*/
case 0x9050: /*GL_IMAGE_CUBE*/
case 0x9051: /*GL_IMAGE_BUFFER*/
case 0x9053: /*GL_IMAGE_2D_ARRAY*/
case 0x9058: /*GL_INT_IMAGE_2D*/
case 0x9059: /*GL_INT_IMAGE_3D*/
case 0x905B: /*GL_INT_IMAGE_CUBE*/
case 0x905C: /*GL_INT_IMAGE_BUFFER*/
case 0x905E: /*GL_INT_IMAGE_2D_ARRAY*/
case 0x9063: /*GL_UNSIGNED_INT_IMAGE_2D*/
case 0x9064: /*GL_UNSIGNED_INT_IMAGE_3D*/
case 0x9066: /*GL_UNSIGNED_INT_IMAGE_CUBE*/
case 0x9067: /*GL_UNSIGNED_INT_IMAGE_BUFFER*/
case 0x9069: /*GL_UNSIGNED_INT_IMAGE_2D_ARRAY*/
return true;
default:
return false;
}
}
namespace PrgramImpl {
class BackendProgramObjectImpl {
public:
// Per-link cache of a sampler-style uniform's backend location: built once in
// SyncToBackend so draws stop issuing glGetUniformLocation string queries.
// lastAssignedUnit mirrors the program-state value set through glUniform1i
// (program state persists across binds, so caching per program is exact).
struct SamplerUniformBinding {
Uint frontendLocation = 0;
Int backendLocation = -1;
GLenum uniformType = 0;
Int lastAssignedUnit = -1;
};
BackendProgramObjectImpl();
~BackendProgramObjectImpl();
void SyncToBackend(const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject);
void Use() const;
void SetBaseInstance(Uint32 baseInstance) const;
void SetBaseInstanceWordIndex(Int32 wordIndex) const;
void SetDrawID(Uint32 drawId) const;
Int GetIndirectParamsBinding() const { return m_indirectParamsBinding; }
Uint GetBackendProgramId() const { return m_backendProgramId; }
Uint GetBackendGlobalUBOId() const { return m_backendGlobalUBOId; }
Uint32 GetSnormFallbackClampOutputMask() const { return m_snormFallbackClampOutputMask; }
Uint32 GetUnormFallbackClampOutputMask() const { return m_unormFallbackClampOutputMask; }
Bool HasGlobalUboBlock() const { return m_globalUboBackendBlockIndex >= 0; }
const Vector<Int>& GetUniformBlockBackendIndices() const { return m_uniformBlockBackendIndices; }
Vector<SamplerUniformBinding>& GetSamplerUniformBindings() { return m_samplerUniformBindings; }
Uint32 GetLastUploadedGlobalUboVersion() const { return m_lastUploadedGlobalUboVersion; }
void SetLastUploadedGlobalUboVersion(Uint32 version) { m_lastUploadedGlobalUboVersion = version; }
// Backend-reported GL_UNIFORM_BLOCK_DATA_SIZE of the global block; ring
// bindings must span at least this much (may exceed the frontend's
// reflected size when the transpiled block pads differently).
Int GetGlobalUboBackendBlockSize() const { return m_globalUboBackendBlockSize; }
BufferImpl::UboRingAllocation& GetGlobalUboRingAllocation() { return m_globalUboRingAllocation; }
// Frontend link version this backend program (and its resource caches) was
// built from; a mismatch means every link-derived cache here is stale.
Uint32 GetSyncedLinkVersion() const { return m_syncedLinkVersion; }
private:
void CacheResourceLocations(const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject);
Uint m_backendProgramId = 0;
Uint m_backendGlobalUBOId = 0;
Int m_baseInstanceUniformLocation = -1;
Int m_drawIdUniformLocation = -1;
Int m_baseInstanceWordIndexUniformLocation = -1;
Int m_indirectParamsBinding = -1;
Uint32 m_snormFallbackClampOutputMask = 0;
Uint32 m_unormFallbackClampOutputMask = 0;
Bool m_isInitialized = false;
Int m_globalUboBackendBlockIndex = -1;
Int m_globalUboBackendBlockSize = 0;
Vector<Int> m_uniformBlockBackendIndices; // frontend block index -> backend index (-1 = absent)
Vector<SamplerUniformBinding> m_samplerUniformBindings;
Uint32 m_lastUploadedGlobalUboVersion = ~0u;
BufferImpl::UboRingAllocation m_globalUboRingAllocation;
Uint32 m_syncedLinkVersion = ~0u;
};
extern Uint32 g_snormFallbackClampOutputMask;
extern Uint32 g_unormFallbackClampOutputMask;
// Backend id of the last glUseProgram issued through this backend; lets Use()
// skip redundant rebinds. Reset to 0 wherever glUseProgram(0) is issued or the
// ES context is recreated.
extern Uint g_lastUsedBackendProgramId;
extern StateBackendObjectRegistry<MG_State::GLState::ProgramObject, BackendProgramObjectImpl>
g_backendProgramObjects;
} // namespace PrgramImpl
@@ -309,6 +679,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
TextureInternalFormat m_cacheInternalFormat = TextureInternalFormat::Unknown;
Int m_cacheWidth = 0;
Int m_cacheHeight = 0;
Int m_cacheSamples = 0;
};
extern StateBackendObjectRegistry<MG_State::GLState::RenderbufferObject, BackendRenderbufferObject>
+678 -9
View File
@@ -9,6 +9,7 @@
#include "DirectGLES.h"
#include "Utils.h"
#include "Managers.h"
#include "MG_Backend/BackendObjects.h"
#include "MG_Util/Converters/GLToMG/FramebufferEnumConverter.h"
#include "MG_Util/Texture/TextureFormatProcessor.h"
@@ -17,19 +18,135 @@
#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 {
Flags<PixelFormatNormalizeOptionBit> GetForcedPixelFormatNormalizeOptions() {
Flags<PixelFormatNormalizeOptionBit> options;
if (g_GLESCapabilities.IsAngleRenderer) {
options |= PixelFormatNormalizeOptionBit::NoRgb16;
options |= PixelFormatNormalizeOptionBit::NoSnorm16;
options |= PixelFormatNormalizeOptionBit::NoSnorm8;
}
return options;
}
Flags<PixelFormatNormalizeOptionBit> GetDriverPixelFormatNormalizeOptions() {
Flags<PixelFormatNormalizeOptionBit> options = PixelFormatNormalizeOptionBit::NoDepthComponent32;
options |= PixelFormatNormalizeOptionBit::NoRGBA8Snorm;
options |= PixelFormatNormalizeOptionBit::NoRGB16Snorm;
if (!g_GLESCapabilities.SupportsNorm16Texture) {
options |= PixelFormatNormalizeOptionBit::NoNorm16;
}
return options;
}
Flags<PixelFormatNormalizeOptionBit> GetRuntimeFallbackNormalizeOptions(GLenum requestedInternalFormat) {
using namespace MG_Util::TextureFormatProcessor;
const Flags<PixelFormatNormalizeOptionBit> forcedOptions =
GetApplicablePixelFormatNormalizeOptions(requestedInternalFormat, GetForcedPixelFormatNormalizeOptions());
if (forcedOptions) {
return forcedOptions;
}
return GetApplicablePixelFormatNormalizeOptions(requestedInternalFormat,
GetDriverPixelFormatNormalizeOptions());
}
Bool HasCachedFormatCapability(TextureInternalFormat internalFormat,
SizeT targetIndex,
Bool caveat,
FormatCapability capability) {
if (!pActiveBackendObject || targetIndex >= kFormatCapabilityTargetCount) {
return false;
}
const SizeT formatIndex = static_cast<SizeT>(internalFormat);
if (formatIndex >= kFormatCapabilityFormatCount) {
return false;
}
const FormatCapabilityCache& cache = pActiveBackendObject->GetFormatCapabilities();
const FormatCapabilityFlags caps =
caveat ? cache.CaveatCaps[targetIndex][formatIndex] : cache.FullCaps[targetIndex][formatIndex];
return HasFormatCapability(caps, capability);
}
Bool HasAnyCachedFormatCapability(TextureInternalFormat internalFormat,
Bool caveat,
FormatCapability capability) {
for (SizeT targetIndex = 0; targetIndex < kFormatCapabilityTargetCount; ++targetIndex) {
if (HasCachedFormatCapability(internalFormat, targetIndex, caveat, capability)) {
return true;
}
}
return false;
}
Bool ShouldUseCaveatFormat(TextureInternalFormat internalFormat, SizeT targetIndex) {
if (targetIndex < kFormatCapabilityTargetCount) {
const Bool fullCreatable =
HasCachedFormatCapability(internalFormat, targetIndex, false, FormatCapability::Creatable);
const Bool caveatCreatable =
HasCachedFormatCapability(internalFormat, targetIndex, true, FormatCapability::Creatable);
const Bool fullRenderable =
HasCachedFormatCapability(internalFormat, targetIndex, false, FormatCapability::FramebufferRenderable);
const Bool caveatRenderable =
HasCachedFormatCapability(internalFormat, targetIndex, true, FormatCapability::FramebufferRenderable);
return (!fullCreatable && caveatCreatable) || (!fullRenderable && caveatRenderable);
}
if (HasAnyCachedFormatCapability(internalFormat, false, FormatCapability::Creatable)) {
return false;
}
return HasAnyCachedFormatCapability(internalFormat, true, FormatCapability::Creatable);
}
void GenerateFormatInfo(TextureInternalFormat internalFormat,
SizeT targetIndex,
GLenum* outInternalFormat,
GLenum* outFormat,
GLenum* outType) {
using namespace MobileGL::MG_Util::TextureFormatProcessor;
const GLenum requestedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(internalFormat);
Flags<PixelFormatNormalizeOptionBit> options;
if (!pActiveBackendObject || ShouldUseCaveatFormat(internalFormat, targetIndex)) {
options = GetRuntimeFallbackNormalizeOptions(requestedInternalFormat);
}
NormalizePixelFormat(requestedInternalFormat, options, outInternalFormat, outFormat, outType);
}
} // namespace
namespace TextureImpl {
void GenerateTextureFormatInfo(TextureInternalFormat internalFormat, GLenum* outInternalFormat,
GLenum* outFormat, GLenum* outType) {
GLenum* outFormat, GLenum* outType, TextureTarget target) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
using namespace MobileGL::MG_Util::TextureFormatProcessor;
auto options = (g_GLESCapabilities.SupportsNorm16Texture) ? PixelFormatNormalizeOptionBit::None
: PixelFormatNormalizeOptionBit::NoNorm16;
NormalizePixelFormat(MG_Util::ConvertTextureInternalFormatToGLEnum(internalFormat), options,
outInternalFormat, outFormat, outType);
const SizeT targetIndex =
target == TextureTarget::Unknown ? kFormatCapabilityTargetCount : GetFormatCapabilityTargetIndex(target);
GenerateFormatInfo(internalFormat, targetIndex, outInternalFormat, outFormat, outType);
}
void GenerateRenderbufferFormatInfo(TextureInternalFormat internalFormat, GLenum* outInternalFormat,
GLenum* outFormat, GLenum* outType) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
GenerateFormatInfo(internalFormat, GetRenderbufferFormatCapabilityTargetIndex(), outInternalFormat,
outFormat, outType);
}
Bool ShouldUseCaveatTextureFormat(TextureInternalFormat internalFormat, TextureTarget target) {
const SizeT targetIndex =
target == TextureTarget::Unknown ? kFormatCapabilityTargetCount : GetFormatCapabilityTargetIndex(target);
return ShouldUseCaveatFormat(internalFormat, targetIndex);
}
Bool ShouldUseCaveatRenderbufferFormat(TextureInternalFormat internalFormat) {
return ShouldUseCaveatFormat(internalFormat, GetRenderbufferFormatCapabilityTargetIndex());
}
} // namespace TextureImpl
namespace PrgramImpl {
@@ -101,14 +218,163 @@ namespace MobileGL::MG_Backend::DirectGLES {
return result;
}
String ClampNormFallbackOutputs(String glslCode, GLenum shaderType, Uint32 snormOutputMask,
Uint32 unormOutputMask) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
const Uint32 outputMask = snormOutputMask | unormOutputMask;
if (shaderType != GL_FRAGMENT_SHADER || outputMask == 0) {
return glslCode;
}
const std::regex outputPattern(
R"(layout\s*\(\s*location\s*=\s*([0-9]+)\s*\)\s*out\s+(?:(?:lowp|mediump|highp)\s+)?vec4\s+([A-Za-z_][A-Za-z0-9_]*)\s*;)");
std::sregex_iterator outputIt(glslCode.begin(), glslCode.end(), outputPattern);
std::sregex_iterator outputEnd;
struct OutputClamp {
String Name;
Bool Signed;
};
Vector<OutputClamp> outputClamps;
for (; outputIt != outputEnd; ++outputIt) {
const Uint location = static_cast<Uint>(std::stoul((*outputIt)[1].str()));
if (location < 32 && (outputMask & (1u << location))) {
outputClamps.push_back({(*outputIt)[2].str(), static_cast<Bool>(snormOutputMask & (1u << location))});
}
}
if (outputClamps.empty()) {
return glslCode;
}
const std::regex mainPattern(R"(void\s+main\s*\([^)]*\)\s*\{)");
std::smatch mainMatch;
if (!std::regex_search(glslCode, mainMatch, mainPattern)) {
return glslCode;
}
SizeT bracePos = static_cast<SizeT>(mainMatch.position(0) + mainMatch.length(0) - 1);
Int depth = 0;
for (SizeT pos = bracePos; pos < glslCode.size(); ++pos) {
if (glslCode[pos] == '{') {
++depth;
} else if (glslCode[pos] == '}') {
--depth;
if (depth == 0) {
String clampLine;
for (const OutputClamp& outputClamp : outputClamps) {
const String minValue = outputClamp.Signed ? "-1.0" : "0.0";
clampLine += "\n " + outputClamp.Name + " = clamp(" + outputClamp.Name +
", vec4(" + minValue + "), vec4(1.0));";
}
clampLine += "\n";
glslCode.insert(pos, clampLine);
return glslCode;
}
}
}
return glslCode;
}
String ForceFlatIntegerVaryings(const String& glslCode, GLenum shaderType) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
String result = glslCode;
const String integerType = R"((?:(?:lowp|mediump|highp)\s+)?(?:u?int|[iu]vec[234])\b)";
auto addFlatQualifier = [&result, &integerType](const String& qualifier) {
const std::regex pattern("(layout\\s*\\([^)]*\\)\\s*)(?!(?:flat|smooth|noperspective)\\s)(" +
qualifier + "\\s+" + integerType + ")");
result = std::regex_replace(result, pattern, "$1flat $2");
};
switch (shaderType) {
case GL_VERTEX_SHADER:
addFlatQualifier("out");
break;
case GL_GEOMETRY_SHADER:
addFlatQualifier("in");
addFlatQualifier("out");
break;
case GL_FRAGMENT_SHADER:
addFlatQualifier("in");
break;
default:
break;
}
return result;
}
String RemoveLayoutBinding(const String& glslCode) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
// Sampler and uniform-block bindings are re-established at draw time through the
// API, so their layout qualifiers are stripped (they may exceed ES limits). SSBO
// blocks and image uniforms are different: ES has no glShaderStorageBlockBinding,
// and image units cannot be set with glUniform1i, so for those declarations the
// binding qualifier is the only binding mechanism and must be preserved.
static std::regex bindingRegex(R"(layout\s*\(\s*binding\s*=\s*\d+\s*\)\s*)");
String result = std::regex_replace(glslCode, bindingRegex, "");
static std::regex bindingRegex2(R"(layout\s*\(\s*binding\s*=\s*\d+\s*,)");
result = std::regex_replace(result, bindingRegex2, "layout(");
static std::regex keepBindingRegex(R"(\b(buffer|[iu]?image[A-Za-z0-9]*)\b)");
String result;
result.reserve(glslCode.size());
SizeT lineStart = 0;
while (lineStart <= glslCode.size()) {
SizeT lineEnd = glslCode.find('\n', lineStart);
const Bool lastLine = lineEnd == String::npos;
String line = glslCode.substr(lineStart, lastLine ? String::npos : lineEnd - lineStart);
if (!std::regex_search(line, keepBindingRegex)) {
line = std::regex_replace(line, bindingRegex, "");
line = std::regex_replace(line, bindingRegex2, "layout(");
}
result += line;
if (lastLine) {
break;
}
result += '\n';
lineStart = lineEnd + 1;
}
return result;
}
String RemoveClipDistanceRedeclaration(const String& glslCode) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
// Adreno rejects any redeclaration of gl_ClipDistance/gl_CullDistance ("reserved
// built-in name") even with GL_EXT_clip_cull_distance required, but accepts plain
// usage of the builtin. Drop the desktop-style redeclaration line SPIRV-Cross
// prints; the "#extension GL_EXT_clip_cull_distance : require" line stays.
static const std::regex redeclarationRegex(
R"(^\s*(?:out|in)\s+(?:(?:high|medium|low)p\s+)?float\s+gl_(?:Clip|Cull)Distance\[[0-9]+\];\s*$)");
String result;
result.reserve(glslCode.size());
SizeT lineStart = 0;
Bool firstLine = true;
while (lineStart <= glslCode.size()) {
SizeT lineEnd = glslCode.find('\n', lineStart);
const Bool lastLine = lineEnd == String::npos;
String line = glslCode.substr(lineStart, lastLine ? String::npos : lineEnd - lineStart);
if (!std::regex_match(line, redeclarationRegex)) {
if (!firstLine) {
result += '\n';
}
result += line;
firstLine = false;
}
if (lastLine) {
break;
}
lineStart = lineEnd + 1;
}
return result;
}
} // namespace PrgramImpl
@@ -118,7 +384,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
while (GLenum err = g_GLESFuncs.glGetError() != GL_NO_ERROR) {
for (GLenum err = g_GLESFuncs.glGetError(); err != GL_NO_ERROR; err = g_GLESFuncs.glGetError()) {
MGLOG_E("-> GLES Error: %s", MG_Util::ConvertGLEnumToString(err).c_str());
}
}
@@ -221,4 +487,407 @@ 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;
}
}
}
}
}
}
static SizeT AlignReadbackRow(SizeT rowBytes, Int alignment) {
const SizeT align = alignment > 0 ? static_cast<SizeT>(alignment) : 1;
return (rowBytes + align - 1) / align * align;
}
// Repacks wide RGBA(_INTEGER) rows into the client's (format, type) layout, honoring the
// client-side PACK parameters and the bound pixel-pack buffer. `wide` holds
// `sliceHeight * sliceCount` rows of `width` texels (slice-major, tightly stacked),
// 4 components x GetReadbackComponentSize(wideType) bytes each.
// applyPackImageParams: GL_PACK_IMAGE_HEIGHT / GL_PACK_SKIP_IMAGES apply only to GetTexImage
// of 3D/array images; ReadPixels and 2D GetTexImage ignore them (GL 3.3 sections 4.3.1, 6.1.4).
// Per the GL addressing rules, slice k row j lands at
// SKIP_IMAGES*imageStride + SKIP_ROWS*rowStride + SKIP_PIXELS*pixelBytes
// + k*imageStride + j*rowStride, with imageStride = max(IMAGE_HEIGHT, sliceHeight)*rowStride.
Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei sliceHeight,
GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type,
void* pixels, Bool applyPackImageParams) {
const SizeT dstPixelBytes = GetReadbackDstPixelSize(mapping, type);
if (dstPixelBytes == 0) {
return false;
}
PackedReadbackLayout packedLayout{};
const Bool isPackedType = GetPackedReadbackLayout(type, packedLayout);
const SizeT dstComponentSize = GetReadbackComponentSize(type);
const auto& pixelPackBufferObject =
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
// Destination layout is computed from the client-side PACK parameters; only the actual pixel
// rows are written so skip regions of the destination stay untouched.
const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);
const SizeT rowPixels = static_cast<SizeT>(packParams.RowLength > 0 ? packParams.RowLength : width);
const SizeT dstRowStride = AlignReadbackRow(rowPixels * dstPixelBytes, packParams.Alignment);
const SizeT imageRows =
applyPackImageParams && packParams.ImageHeight > 0
? static_cast<SizeT>(packParams.ImageHeight)
: static_cast<SizeT>(sliceHeight);
const SizeT dstImageStride = imageRows * dstRowStride;
const SizeT skipImages =
applyPackImageParams ? static_cast<SizeT>(std::max(packParams.SkipImages, 0)) : SizeT{0};
const SizeT dstSkipOffset = skipImages * dstImageStride +
static_cast<SizeT>(std::max(packParams.SkipRows, 0)) * dstRowStride +
static_cast<SizeT>(std::max(packParams.SkipPixels, 0)) * dstPixelBytes;
const SizeT dstRowBytes = static_cast<SizeT>(width) * dstPixelBytes;
const SizeT pboBaseOffset = reinterpret_cast<SizeT>(pixels); // with a PBO, `pixels` is an offset
if (pixelPackBufferObject) {
const SizeT requiredSize = pboBaseOffset + dstSkipOffset +
static_cast<SizeT>(sliceCount - 1) * dstImageStride +
static_cast<SizeT>(sliceHeight - 1) * dstRowStride + dstRowBytes;
if (requiredSize > pixelPackBufferObject->GetSize()) {
MGLOG_E("Readback conversion: pixel pack buffer is too small");
return true;
}
}
const SizeT srcComponentSize = GetReadbackComponentSize(wideType);
const SizeT srcPixelBytes = 4 * srcComponentSize;
Vector<Uint8> convertedRow(dstRowBytes);
for (GLsizei slice = 0; slice < sliceCount; ++slice) {
for (GLsizei row = 0; row < sliceHeight; ++row) {
const SizeT flatRow = static_cast<SizeT>(slice) * static_cast<SizeT>(sliceHeight) +
static_cast<SizeT>(row);
const Uint8* srcRow = wide + flatRow * static_cast<SizeT>(width) * srcPixelBytes;
ConvertWideReadbackRow(srcRow, convertedRow.data(), static_cast<SizeT>(width), wideType,
mapping, type);
if (packParams.SwapBytes) {
const SizeT groupSize = isPackedType ? packedLayout.byteSize : dstComponentSize;
if (groupSize > 1) {
for (SizeT offset = 0; offset + groupSize <= dstRowBytes; offset += groupSize) {
std::reverse(convertedRow.data() + offset, convertedRow.data() + offset + groupSize);
}
}
}
const SizeT dstOffset = dstSkipOffset + static_cast<SizeT>(slice) * dstImageStride +
static_cast<SizeT>(row) * dstRowStride;
if (pixelPackBufferObject) {
pixelPackBufferObject->WritebackFromBackend({convertedRow.data(), dstRowBytes},
pboBaseOffset + dstOffset);
} else {
Memcpy(static_cast<Uint8*>(pixels) + dstOffset, convertedRow.data(), dstRowBytes);
}
}
}
return true;
}
} // namespace ReadbackImpl
} // namespace MobileGL::MG_Backend::DirectGLES
+63 -1
View File
@@ -35,15 +35,77 @@ namespace MobileGL::MG_Backend::DirectGLES {
namespace TextureImpl {
void GenerateTextureFormatInfo(TextureInternalFormat internalFormat, GLenum* outInternalFormat,
GLenum* outFormat, GLenum* outType);
GLenum* outFormat, GLenum* outType,
TextureTarget target = TextureTarget::Unknown);
void GenerateRenderbufferFormatInfo(TextureInternalFormat internalFormat, GLenum* outInternalFormat,
GLenum* outFormat, GLenum* outType);
Bool ShouldUseCaveatTextureFormat(TextureInternalFormat internalFormat, TextureTarget target);
Bool ShouldUseCaveatRenderbufferFormat(TextureInternalFormat internalFormat);
} // namespace TextureImpl
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);
// Stores wide RGBA(_INTEGER) rows into the client pointer or the bound PACK pixel buffer,
// honoring the client-side PACK pixel-store parameters (row length, alignment, skips,
// swap-bytes, and - when applyPackImageParams - image height/skip images). Shared by the
// DirectGLES and DirectVulkan readback conversion paths.
Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei sliceHeight,
GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type,
void* pixels, Bool applyPackImageParams);
} // namespace ReadbackImpl
namespace PrgramImpl {
String ProcessOutColorLocations(const String& glslCode);
String ForceSupporterOutput(const String& glslCode);
String ClampNormFallbackOutputs(String glslCode, GLenum shaderType, Uint32 snormOutputMask,
Uint32 unormOutputMask);
String ForceFlatIntegerVaryings(const String& glslCode, GLenum shaderType);
String RemoveLayoutBinding(const String& glslCode);
String RemoveClipDistanceRedeclaration(const String& glslCode);
} // namespace PrgramImpl
namespace Utils {
@@ -9,16 +9,334 @@
#include "BackendObject_DirectVulkan.h"
#include "MG_Backend/BackendObject.h"
#include "DirectVulkan.h"
#include "MG_State/GLState/FramebufferState/FramebufferObject.h"
#include "MG_State/GLState/TextureState/TextureState.h"
#include "MG_Util/Classifiers/TextureEnumClassifier.h"
#include "MG_Util/Converters/MGToGL/TextureEnumConverter.h"
#include "MG_Util/Converters/MGToStr/TextureEnumConverter.h"
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
#include "MG_Util/Texture/TextureFormatProcessor.h"
#include <Config.h>
#include <cstdlib>
#include <cstring>
namespace MobileGL::MG_Backend::DirectVulkan {
namespace {
Bool IsR11G11B10FFallbackEnabled() {
return MG_Config::Features.MagmaR11G11B10FFallback;
}
Bool IsReleaseCurrentRequest(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) {
return dpy == EGL_NO_DISPLAY && draw == EGL_NO_SURFACE && read == EGL_NO_SURFACE && ctx == EGL_NO_CONTEXT;
(void)dpy;
return draw == EGL_NO_SURFACE && read == EGL_NO_SURFACE && ctx == EGL_NO_CONTEXT;
}
Bool IsFormatIndexValid(TextureInternalFormat format) {
return format != TextureInternalFormat::Unknown && static_cast<Int>(format) >= 0 &&
static_cast<SizeT>(format) < kFormatCapabilityFormatCount;
}
Bool IsLayeredTarget(TextureTarget target) {
return target == TextureTarget::Texture3D || target == TextureTarget::Texture1DArray ||
target == TextureTarget::Texture2DArray || target == TextureTarget::TextureCubeMap ||
target == TextureTarget::TextureCubeMapArray ||
target == TextureTarget::Texture2DMultisampleArray;
}
Bool IsMultisampleTarget(TextureTarget target) {
return target == TextureTarget::Texture2DMultisample ||
target == TextureTarget::Texture2DMultisampleArray;
}
Bool IsTextureBufferTarget(TextureTarget target) {
return target == TextureTarget::TextureBuffer;
}
Bool IsIntegerInternalFormat(TextureInternalFormat format) {
const GLenum glFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(format);
GLenum normalizedInternalFormat = glFormat;
GLenum imageFormat = GL_RGBA;
GLenum imageType = GL_UNSIGNED_BYTE;
MG_Util::TextureFormatProcessor::NormalizePixelFormat(
glFormat, PixelFormatNormalizeOptionBit::None, &normalizedInternalFormat, &imageFormat, &imageType);
return imageFormat == GL_RED_INTEGER || imageFormat == GL_RG_INTEGER || imageFormat == GL_RGB_INTEGER ||
imageFormat == GL_RGBA_INTEGER;
}
FormatCapabilityFlags GetAttachmentCaps(TextureInternalFormat format) {
FormatCapabilityFlags caps = FormatCapability::FramebufferRenderable;
const Bool isDepth = MG_Util::IsDepthFormatInternalFormat(format);
const Bool isStencil = MG_Util::IsStencilFormatInternalFormat(format);
if (!isDepth && !isStencil) {
caps |= FormatCapability::ColorAttachment;
}
if (isDepth) {
caps |= FormatCapability::DepthAttachment;
}
if (isStencil) {
caps |= FormatCapability::StencilAttachment;
}
return caps;
}
FormatCapabilityFlags BuildVulkanCaps(TextureInternalFormat logicalFormat,
TextureTarget target,
VkFormatFeatureFlags features) {
FormatCapabilityFlags caps;
const Bool isDepth = MG_Util::IsDepthFormatInternalFormat(logicalFormat);
const Bool isStencil = MG_Util::IsStencilFormatInternalFormat(logicalFormat);
const Bool isInteger = IsIntegerInternalFormat(logicalFormat);
if (IsTextureBufferTarget(target)) {
if ((features & VK_FORMAT_FEATURE_UNIFORM_TEXEL_BUFFER_BIT) != 0) {
caps |= FormatCapability::Creatable;
caps |= FormatCapability::Sampled;
caps |= FormatCapability::TextureBuffer;
}
return caps;
}
const Bool sampled = (features & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) != 0;
const Bool linearFilter = (features & VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT) != 0;
const Bool colorRenderable = (features & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) != 0;
const Bool depthStencilRenderable =
(features & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT) != 0;
const Bool renderable = (isDepth || isStencil) ? depthStencilRenderable : colorRenderable;
if (sampled || renderable) {
caps |= FormatCapability::Creatable;
}
if (sampled) {
caps |= FormatCapability::Sampled;
if (linearFilter && !isInteger && !isStencil) {
caps |= FormatCapability::LinearFilter;
}
if (!isStencil && (features & VK_FORMAT_FEATURE_BLIT_SRC_BIT) != 0 &&
(features & VK_FORMAT_FEATURE_BLIT_DST_BIT) != 0) {
caps |= FormatCapability::GenerateMipmap;
}
if (!isInteger && !isDepth && !isStencil) {
caps |= FormatCapability::TextureGather;
}
if (isDepth && !isStencil) {
caps |= FormatCapability::TextureShadow;
}
}
if (renderable) {
caps |= GetAttachmentCaps(logicalFormat);
if (IsLayeredTarget(target)) {
caps |= FormatCapability::FramebufferLayered;
}
}
if (IsMultisampleTarget(target)) {
caps |= FormatCapability::MultisampleTexture;
}
return caps;
}
Optional<TextureInternalFormat> ResolveVulkanFallbackLogicalFormat(TextureInternalFormat format) {
switch (format) {
case TextureInternalFormat::RGB:
case TextureInternalFormat::RGB8:
return TextureInternalFormat::RGBA8;
// Legacy low-bit-depth formats with no (or rarely supported) native Vulkan
// encoding; a wider normalized fallback keeps at least the required precision.
case TextureInternalFormat::R3G3B2:
case TextureInternalFormat::RGB4:
case TextureInternalFormat::RGB5:
case TextureInternalFormat::RGBA2:
case TextureInternalFormat::RGBA4:
case TextureInternalFormat::RGB5A1:
return TextureInternalFormat::RGBA8;
case TextureInternalFormat::RGB10:
return TextureInternalFormat::RGB10A2;
case TextureInternalFormat::RGB12:
case TextureInternalFormat::RGBA12:
return TextureInternalFormat::RGBA16;
case TextureInternalFormat::SRGB8:
return TextureInternalFormat::SRGB8Alpha8;
case TextureInternalFormat::RGB8Snorm:
return TextureInternalFormat::RGBA8Snorm;
case TextureInternalFormat::RGB16:
return TextureInternalFormat::RGBA16;
case TextureInternalFormat::RGB16Snorm:
return TextureInternalFormat::RGBA16Snorm;
case TextureInternalFormat::RGB16F:
return TextureInternalFormat::RGBA16F;
case TextureInternalFormat::R11FG11FB10F:
if (IsR11G11B10FFallbackEnabled()) {
return TextureInternalFormat::RGBA16F;
}
return Nullopt;
case TextureInternalFormat::RGB32F:
return TextureInternalFormat::RGBA32F;
case TextureInternalFormat::RGB8I:
return TextureInternalFormat::RGBA8I;
case TextureInternalFormat::RGB8UI:
return TextureInternalFormat::RGBA8UI;
case TextureInternalFormat::RGB16I:
return TextureInternalFormat::RGBA16I;
case TextureInternalFormat::RGB16UI:
return TextureInternalFormat::RGBA16UI;
case TextureInternalFormat::RGB32I:
return TextureInternalFormat::RGBA32I;
case TextureInternalFormat::RGB32UI:
return TextureInternalFormat::RGBA32UI;
default:
return Nullopt;
}
}
Optional<VkFormat> ResolveVulkanFallbackFormat(TextureInternalFormat format) {
const Optional<TextureInternalFormat> fallbackLogicalFormat = ResolveVulkanFallbackLogicalFormat(format);
if (!fallbackLogicalFormat) {
return Nullopt;
}
return MG_Util::ConvertTextureInternalFormatToVkEnum(*fallbackLogicalFormat);
}
Bool HasNewCaveatFormatCaps(FormatCapabilityFlags nativeCaps, FormatCapabilityFlags fallbackCaps) {
for (FormatCapability capability : kReportedFormatCapabilities) {
if (HasFormatCapability(fallbackCaps, capability) &&
!HasFormatCapability(nativeCaps, capability)) {
return true;
}
}
return false;
}
void LogVulkanFormatCaveat(TextureInternalFormat logicalFormat,
SizeT targetIndex,
TextureInternalFormat fallbackFormat) {
MGLOG_D("Caveat: %s %s not fully supported. Reason: native Vulkan format is not fully supported. Fallback: %s",
GetFormatCapabilityTargetName(targetIndex).c_str(),
MG_Util::ConvertTextureInternalFormatToString(logicalFormat).c_str(),
MG_Util::ConvertTextureInternalFormatToString(fallbackFormat).c_str());
}
Vector<Int> BuildSampleCounts(Int maxSamples) {
Vector<Int> counts;
for (Int samples = std::max(maxSamples, 1); samples > 1; samples >>= 1) {
counts.push_back(samples);
}
counts.push_back(1);
return counts;
}
void PopulateFormatCapabilitiesImpl(VkPhysicalDevice physicalDevice,
PFN_vkGetPhysicalDeviceFormatProperties getFormatProperties,
const MG_External::VulkanCapabilities& capabilities,
FormatCapabilityCache& cache) {
cache.Clear();
if (physicalDevice == VK_NULL_HANDLE || getFormatProperties == nullptr) {
return;
}
for (SizeT formatIndex = 0; formatIndex < kFormatCapabilityFormatCount; ++formatIndex) {
const auto logicalFormat = static_cast<TextureInternalFormat>(formatIndex);
if (!IsFormatIndexValid(logicalFormat)) {
continue;
}
VkFormat nativeFormat = MG_Util::ConvertTextureInternalFormatToVkEnum(logicalFormat);
const Optional<TextureInternalFormat> fallbackLogicalFormat =
ResolveVulkanFallbackLogicalFormat(logicalFormat);
VkFormat fallbackFormat = ResolveVulkanFallbackFormat(logicalFormat).value_or(VK_FORMAT_UNDEFINED);
VkFormatProperties nativeProperties{};
if (nativeFormat != VK_FORMAT_UNDEFINED) {
getFormatProperties(physicalDevice, nativeFormat, &nativeProperties);
}
VkFormatProperties fallbackProperties{};
if (fallbackFormat != VK_FORMAT_UNDEFINED && fallbackFormat != nativeFormat) {
getFormatProperties(physicalDevice, fallbackFormat, &fallbackProperties);
}
for (SizeT targetIndex = 0; targetIndex < kFormatCapabilityTextureTargetCount; ++targetIndex) {
const auto target = static_cast<TextureTarget>(targetIndex);
const VkFormatFeatureFlags nativeFeatures =
IsTextureBufferTarget(target) ? nativeProperties.bufferFeatures
: nativeProperties.optimalTilingFeatures;
FormatCapabilityFlags nativeCaps = BuildVulkanCaps(logicalFormat, target, nativeFeatures);
cache.FullCaps[targetIndex][formatIndex] |= nativeCaps;
const VkFormatFeatureFlags fallbackFeatures =
IsTextureBufferTarget(target) ? fallbackProperties.bufferFeatures
: fallbackProperties.optimalTilingFeatures;
FormatCapabilityFlags fallbackCaps = BuildVulkanCaps(logicalFormat, target, fallbackFeatures);
if (fallbackFormat != VK_FORMAT_UNDEFINED && fallbackFormat != nativeFormat) {
cache.CaveatCaps[targetIndex][formatIndex] |= fallbackCaps;
if (fallbackLogicalFormat && HasNewCaveatFormatCaps(nativeCaps, fallbackCaps)) {
LogVulkanFormatCaveat(logicalFormat, targetIndex, *fallbackLogicalFormat);
}
}
if (HasFormatCapability(nativeCaps | fallbackCaps, FormatCapability::MultisampleTexture)) {
const Bool isDepth = MG_Util::IsDepthFormatInternalFormat(logicalFormat);
const Bool isStencil = MG_Util::IsStencilFormatInternalFormat(logicalFormat);
const Bool isInteger = IsIntegerInternalFormat(logicalFormat);
Int maxSamples = capabilities.MaxColorTextureSamples;
if (isDepth || isStencil) {
maxSamples = capabilities.MaxDepthTextureSamples;
} else if (isInteger) {
maxSamples = capabilities.MaxIntegerSamples;
}
cache.SampleCounts[targetIndex][formatIndex] = BuildSampleCounts(maxSamples);
}
}
const SizeT renderbufferTargetIndex = GetRenderbufferFormatCapabilityTargetIndex();
FormatCapabilityFlags renderbufferCaps =
BuildVulkanCaps(logicalFormat, TextureTarget::Texture2D, nativeProperties.optimalTilingFeatures);
renderbufferCaps &= FormatCapability::Creatable;
if ((nativeProperties.optimalTilingFeatures &
(VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT | VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT)) != 0) {
renderbufferCaps |= GetAttachmentCaps(logicalFormat);
renderbufferCaps |= FormatCapability::MultisampleRenderbuffer;
}
cache.FullCaps[renderbufferTargetIndex][formatIndex] |= renderbufferCaps;
if (fallbackFormat != VK_FORMAT_UNDEFINED && fallbackFormat != nativeFormat) {
FormatCapabilityFlags fallbackRenderbufferCaps =
BuildVulkanCaps(logicalFormat, TextureTarget::Texture2D,
fallbackProperties.optimalTilingFeatures);
fallbackRenderbufferCaps &= FormatCapability::Creatable;
if ((fallbackProperties.optimalTilingFeatures &
(VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT | VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT)) !=
0) {
fallbackRenderbufferCaps |= GetAttachmentCaps(logicalFormat);
fallbackRenderbufferCaps |= FormatCapability::MultisampleRenderbuffer;
}
cache.CaveatCaps[renderbufferTargetIndex][formatIndex] |= fallbackRenderbufferCaps;
if (fallbackLogicalFormat &&
HasNewCaveatFormatCaps(renderbufferCaps, fallbackRenderbufferCaps)) {
LogVulkanFormatCaveat(logicalFormat, renderbufferTargetIndex, *fallbackLogicalFormat);
}
}
const FormatCapabilityFlags rbCaps = cache.FullCaps[renderbufferTargetIndex][formatIndex] |
cache.CaveatCaps[renderbufferTargetIndex][formatIndex];
if (HasFormatCapability(rbCaps, FormatCapability::MultisampleRenderbuffer)) {
cache.SampleCounts[renderbufferTargetIndex][formatIndex] =
BuildSampleCounts(capabilities.MaxFramebufferSamples);
}
}
}
} // namespace
void PopulateFormatCapabilities(VkPhysicalDevice physicalDevice,
PFN_vkGetPhysicalDeviceFormatProperties getFormatProperties,
const MG_External::VulkanCapabilities& capabilities,
FormatCapabilityCache& cache) {
PopulateFormatCapabilitiesImpl(physicalDevice, getFormatProperties, capabilities, cache);
}
BackendObject_DirectVulkan::~BackendObject_DirectVulkan() = default;
BackendObject_DirectVulkan::BackendObject_DirectVulkan(): m_rendererInfo{GetRendererIdentity()} {}
Bool BackendObject_DirectVulkan::InitWindowSurface() {
if (!m_windowHandle.Handle) {
MGLOG_E("Cannot initialize DirectVulkan window surface: native window handle is null");
@@ -27,12 +345,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
auto nativeWindow = reinterpret_cast<NativeWindowType>(m_windowHandle.Handle);
// Any renderer instance this assignment replaces is destroyed here;
// fence/timer-query handles stamped with the old generation go stale.
BumpRendererGeneration();
pVulkanRenderer = MakeUnique<MG_Backend::DirectVulkan::VulkanRenderer>(nativeWindow);
MOBILEGL_ASSERT(pVulkanRenderer != nullptr, "InitWindowSurface: VulkanRenderer creation failed");
pVulkanRenderer->Initialize();
return true;
}
Bool BackendObject_DirectVulkan::InitPbufferSurface(EGLint width, EGLint height) {
VulkanRendererConfig config;
config.SurfaceWidth = static_cast<Uint32>(std::max<EGLint>(width, 1));
config.SurfaceHeight = static_cast<Uint32>(std::max<EGLint>(height, 1));
// Any renderer instance this assignment replaces is destroyed here;
// fence/timer-query handles stamped with the old generation go stale.
BumpRendererGeneration();
pVulkanRenderer = MakeUnique<MG_Backend::DirectVulkan::VulkanRenderer>(NativeWindowType{}, config);
MOBILEGL_ASSERT(pVulkanRenderer != nullptr, "InitPbufferSurface: VulkanRenderer creation failed");
pVulkanRenderer->Initialize();
return true;
}
void BackendObject_DirectVulkan::Initialize() {
m_initialized = true;
}
@@ -47,8 +381,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return false;
}
MG_Util::BackendLoader::FillInVulkanCapabilities(m_vulkanCaps, pVulkanRenderer->GetPhysicalDevice().properties);
const auto& physicalDevice = pVulkanRenderer->GetPhysicalDevice();
if (!MG_Util::BackendLoader::QueryVulkanCapabilities(m_vulkanCaps, pVulkanRenderer->GetInstance(),
physicalDevice.handle)) {
MGLOG_W("DirectVulkan: failed to query extended Vulkan capabilities, using basic properties");
MG_Util::BackendLoader::FillInVulkanCapabilities(m_vulkanCaps, physicalDevice.properties);
}
UpdateDynamicBackendParameters();
UpdateAdvertisedExtensions();
PopulateFormatCapabilities(physicalDevice.handle, vkGetPhysicalDeviceFormatProperties, m_vulkanCaps,
MutableFormatCapabilities());
PrintFormatCapabilities(GetFormatCapabilities());
return true;
}
@@ -60,40 +403,49 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return BackendObject::InitializeEGLDisplay(dpy, major, minor);
}
Bool BackendObject_DirectVulkan::CreateEGLWindowSurface(const WindowHandle& handle) {
Bool BackendObject_DirectVulkan::CreateEGLWindowSurface(EGLSurface surface, const WindowHandle& handle) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
if (!m_initialized) {
MGLOG_E("DirectVulkan backend not initialized");
return false;
}
if (handle.Backend != WindowBackend::Android || !handle.Handle) {
MGLOG_E("DirectVulkan backend only supports Android native windows");
if (!handle.Handle || (handle.Backend != WindowBackend::Android &&
handle.Backend != WindowBackend::X11 &&
handle.Backend != WindowBackend::MetalLayer &&
handle.Backend != WindowBackend::Win32)) {
MGLOG_E("DirectVulkan backend only supports Android, X11, CAMetalLayer, and Win32 native windows");
return false;
}
const Bool sameHandle =
m_eglWindowSurfaceInitialized && m_windowHandle.Backend == handle.Backend && m_windowHandle.Handle == handle.Handle;
if (sameHandle) {
return true;
}
return RegisterEGLWindowSurface(surface, handle);
}
if (m_eglWindowSurfaceInitialized || pVulkanRenderer) {
pVulkanRenderer.reset();
ResetEGLRuntimeState();
Bool BackendObject_DirectVulkan::ResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
if (!m_initialized) {
MGLOG_E("DirectVulkan backend not initialized");
return false;
}
if (!BackendObject::ResizeEGLWindowSurface(surface, width, height)) {
return false;
}
if (pVulkanRenderer && m_eglSurface == surface) {
pVulkanRenderer->RequestSwapchainResize(width, height);
}
return true;
}
return BackendObject::CreateEGLWindowSurface(handle);
Bool BackendObject_DirectVulkan::CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
if (!m_initialized) {
MGLOG_E("DirectVulkan backend not initialized");
return false;
}
return RegisterEGLPbufferSurface(surface, width, height);
}
Bool BackendObject_DirectVulkan::MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
if (IsReleaseCurrentRequest(dpy, draw, read, ctx)) {
return BackendObject::MakeEGLCurrent(dpy, draw, read, ctx);
}
if (!pVulkanRenderer) {
MGLOG_E("DirectVulkan renderer is not initialized");
return false;
}
return BackendObject::MakeEGLCurrent(dpy, draw, read, ctx);
}
@@ -106,33 +458,95 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return BackendObject::SwapEGLBuffers(dpy, draw);
}
void BackendObject_DirectVulkan::ReleaseEGLSurface(EGLSurface surface) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
BackendObject::ReleaseEGLSurface(surface);
}
void BackendObject_DirectVulkan::ReleaseEGLResources() {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
// Outstanding fence/timer-query handles now refer to a dead renderer;
// treat them as signaled/available with zero results from here on.
BumpRendererGeneration();
pVulkanRenderer.reset();
BackendObject::ReleaseEGLResources();
}
void BackendObject_DirectVulkan::OnEGLSurfaceReleased(EGLSurface surface) {
(void)surface;
// Outstanding fence/timer-query handles now refer to a dead renderer;
// treat them as signaled/available with zero results from here on.
BumpRendererGeneration();
pVulkanRenderer.reset();
}
const RendererInfo& BackendObject_DirectVulkan::GetRendererInfo() const {
static RendererInfo RendererInfo = {
.RendererName = "Magma", // Renderer Name
.BackendName = "Direct (Vulkan)", // Backend Name
.ExtraVendor = Nullopt, // Extra vendor
.RendererGLInfo =
{
.TargetGLVersion = {3, 3, 0}, // Target OpenGL Version
.TargetGLSLVersion = {4, 6, 0}, // Target Shading Language Version
.Extensions = {V_OpenGL30, V_OpenGL31, V_OpenGL32, // OpenGL Extensions
V_OpenGL33},
.IsCompatibilityProfile = false // Is Compatibility Profile
},
.StaticBackendCapability = {.AllowVSOnlyPrograms = false} // Backend Capability
};
return RendererInfo;
return m_rendererInfo;
}
String BackendObject_DirectVulkan::GetBackendAPIVersionString() const {
if (!m_initialized) {
return "<uninitialized DirectVulkan backend>";
}
return FormatBackendAPIVersionString(m_vulkanCaps.DeviceName, m_vulkanCaps.VulkanAPIVersion.toString(),
m_vulkanCaps.DriverVersionString);
}
const RendererInfo& GetRendererIdentity() {
static const RendererInfo rendererInfo = {
.RendererName = "Magma",
.BackendName = "Direct (Vulkan)",
.ExtraVendor = Nullopt,
.RendererGLInfo =
{
.TargetGLVersion = {3, 3, 0},
.TargetGLSLVersion = {4, 6, 0},
// Baseline advertisement (no shader subgroup, no timer queries); a
// live backend reconciles its copy in UpdateAdvertisedExtensions.
.Extensions = BuildAdvertisedExtensions(false, false, false),
.IsCompatibilityProfile = false
},
.StaticBackendCapability = {.AllowVSOnlyPrograms = false}};
return rendererInfo;
}
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,
E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object,
E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters,
E_GL_EXT_framebuffer_object, E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage,
E_GL_ARB_texture_storage, E_GL_ARB_texture_storage_multisample,
E_GL_ARB_clear_texture, E_GL_ARB_direct_state_access,
E_GL_ARB_shader_draw_parameters, E_GL_ARB_gpu_shader_int64, E_GL_KHR_debug,
E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind, E_GL_ARB_shading_language_420pack,
E_GL_ARB_vertex_attrib_binding, E_GL_ARB_shader_image_size};
if (shaderSubgroupSupported && !MG_Config::Features.DisableSubgroup) {
extensions.push_back(E_GL_KHR_shader_subgroup);
}
// GL_ARB_timer_query gates MC's F3 GPU% (LWJGL checks the extension string);
// only advertised when the device actually supports timestamp queries and the
// MOBILEGL_DISABLE_TIMERQUERY escape hatch is off.
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;
}
String FormatBackendAPIVersionString(const String& deviceName, const String& vulkanApiVersionString,
const String& driverVersionString) {
// Format:
// <GPU Name>, Vulkan <Vulkan Version>, Driver <Driver Version>
String str = m_vulkanCaps.DeviceName + ", Vulkan " + m_vulkanCaps.VulkanAPIVersion.toString() + ", Driver " +
m_vulkanCaps.DriverVersionString;
return str;
return deviceName + ", Vulkan " + vulkanApiVersionString + ", Driver " + driverVersionString;
}
BackendType BackendObject_DirectVulkan::GetBackendType() const {
@@ -147,10 +561,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
funcsTable.GL.DrawArrays = DrawArrays;
funcsTable.GL.DrawElements = DrawElements;
funcsTable.GL.DrawElementsBaseVertex = DrawElementsBaseVertex;
funcsTable.GL.MultiDrawArrays = MultiDrawArrays;
funcsTable.GL.MultiDrawElements = MultiDrawElements;
funcsTable.GL.MultiDrawElementsBaseVertex = MultiDrawElementsBaseVertex;
funcsTable.GL.MultiDrawElementsIndirect = MultiDrawElementsIndirect;
funcsTable.GL.MultiDrawArraysIndirect = MultiDrawArraysIndirect;
funcsTable.GL.MultiDrawElementsIndirectCount = MultiDrawElementsIndirectCount;
funcsTable.GL.MultiDrawArraysIndirectCount = MultiDrawArraysIndirectCount;
funcsTable.GL.DrawRangeElementsBaseVertex = DrawRangeElementsBaseVertex;
funcsTable.GL.DrawRangeElements = DrawRangeElements;
funcsTable.GL.DrawElementsInstancedBaseVertexBaseInstance = DrawElementsInstancedBaseVertexBaseInstance;
@@ -166,12 +583,51 @@ namespace MobileGL::MG_Backend::DirectVulkan {
funcsTable.GL.ClearBufferfv = ClearBufferfv;
funcsTable.GL.ClearBufferuiv = ClearBufferuiv;
funcsTable.GL.ClearBufferiv = ClearBufferiv;
funcsTable.GL.ClearNamedFramebufferfv = ClearNamedFramebufferfv;
funcsTable.GL.ClearNamedFramebufferfi = ClearNamedFramebufferfi;
funcsTable.GL.BlitFramebuffer = BlitFramebuffer;
funcsTable.GL.BlitNamedFramebuffer = BlitNamedFramebuffer;
funcsTable.GL.CopyTexImage2D = CopyTexImage2D;
funcsTable.GL.CopyTexSubImage2D = CopyTexSubImage2D;
funcsTable.GL.CopyImageSubData = CopyImageSubData;
funcsTable.GL.GenerateMipmap = GenerateMipmap;
funcsTable.GL.ReadPixels = ReadPixels;
funcsTable.GL.GetTexImage = GetTexImage;
funcsTable.GL.GetTextureImage = GetTextureImage;
funcsTable.GL.DispatchCompute = DispatchCompute;
funcsTable.GL.DispatchComputeIndirect = DispatchComputeIndirect;
funcsTable.GL.MemoryBarrier = MemoryBarrier;
funcsTable.GL.MemoryBarrierByRegion = MemoryBarrierByRegion;
funcsTable.GL.BindImageTexture = BindImageTexture;
funcsTable.GL.GetIntegeri_v = GetIntegeri_v;
funcsTable.GL.GetInteger64i_v = GetInteger64i_v;
funcsTable.GL.GetProgramiv = GetProgramiv;
funcsTable.GL.GetProgramInterfaceiv = GetProgramInterfaceiv;
funcsTable.GL.GetProgramResourceIndex = GetProgramResourceIndex;
funcsTable.GL.GetProgramResourceName = GetProgramResourceName;
funcsTable.GL.GetProgramResourceiv = GetProgramResourceiv;
funcsTable.GL.GetProgramResourceLocation = GetProgramResourceLocation;
funcsTable.GL.GetProgramResourceLocationIndex = GetProgramResourceLocationIndex;
funcsTable.GL.ShaderStorageBlockBinding = ShaderStorageBlockBinding;
funcsTable.GL.FenceSync = FenceSync;
funcsTable.GL.ClientWaitSync = ClientWaitSync;
funcsTable.GL.WaitSync = WaitSync;
funcsTable.GL.DeleteSync = DeleteSync;
funcsTable.GL.GetSyncStatus = GetSyncStatus;
// Optional timer-query group: left null (the frontend then falls
// back) when disabled via MOBILEGL_DISABLE_TIMERQUERY. The hooks
// themselves additionally degrade to null handles when the device
// lacks timestamp support.
if (!MG_Config::Features.DisableTimerQuery) {
funcsTable.GL.IsTimerQuerySupported = IsTimerQuerySupported;
funcsTable.GL.BeginTimeElapsedQuery = BeginTimeElapsedQuery;
funcsTable.GL.EndTimeElapsedQuery = EndTimeElapsedQuery;
funcsTable.GL.QueryCounterTimestamp = QueryCounterTimestamp;
funcsTable.GL.IsQueryResultAvailable = IsQueryResultAvailable;
funcsTable.GL.GetQueryResult64 = GetQueryResult64;
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
funcsTable.GL.GetGpuTimestampNs = GetGpuTimestampNs;
}
funcsTableInitialized = true;
}
return funcsTable;
@@ -181,7 +637,204 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return m_dynamicParameters;
}
void BackendObject_DirectVulkan::ApplyVulkanCapabilitiesForTesting(
const MG_External::VulkanCapabilities& capabilities) {
m_vulkanCaps = capabilities;
UpdateDynamicBackendParameters();
UpdateAdvertisedExtensions();
MutableFormatCapabilities().Clear();
}
void BackendObject_DirectVulkan::UpdateAdvertisedExtensions() {
// GL_ARB_timer_query gates MC's F3 GPU% (LWJGL checks the extension
// string). InitCapabilities runs after InitWindowSurface has created
// and initialized the renderer, so the advertisement can be gated on
// real device timestamp support. ApplyVulkanCapabilitiesForTesting may
// 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(),
pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported());
}
void BackendObject_DirectVulkan::UpdateDynamicBackendParameters() {
const auto mapShaderStages = [](Uint32 vkStages) {
Uint32 glStages = 0;
if ((vkStages & VK_SHADER_STAGE_VERTEX_BIT) != 0) glStages |= GL_VERTEX_SHADER_BIT;
if ((vkStages & VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT) != 0) glStages |= GL_TESS_CONTROL_SHADER_BIT;
if ((vkStages & VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT) != 0) {
glStages |= GL_TESS_EVALUATION_SHADER_BIT;
}
if ((vkStages & VK_SHADER_STAGE_GEOMETRY_BIT) != 0) glStages |= GL_GEOMETRY_SHADER_BIT;
if ((vkStages & VK_SHADER_STAGE_FRAGMENT_BIT) != 0) glStages |= GL_FRAGMENT_SHADER_BIT;
if ((vkStages & VK_SHADER_STAGE_COMPUTE_BIT) != 0) glStages |= GL_COMPUTE_SHADER_BIT;
return glStages;
};
const auto mapSubgroupFeatures = [](Uint32 vkFeatures) {
Uint32 glFeatures = 0;
if ((vkFeatures & VK_SUBGROUP_FEATURE_BASIC_BIT) != 0) {
glFeatures |= GL_SUBGROUP_FEATURE_BASIC_BIT_KHR;
}
if ((vkFeatures & VK_SUBGROUP_FEATURE_VOTE_BIT) != 0) {
glFeatures |= GL_SUBGROUP_FEATURE_VOTE_BIT_KHR;
}
if ((vkFeatures & VK_SUBGROUP_FEATURE_ARITHMETIC_BIT) != 0) {
glFeatures |= GL_SUBGROUP_FEATURE_ARITHMETIC_BIT_KHR;
}
if ((vkFeatures & VK_SUBGROUP_FEATURE_BALLOT_BIT) != 0) {
glFeatures |= GL_SUBGROUP_FEATURE_BALLOT_BIT_KHR;
}
if ((vkFeatures & VK_SUBGROUP_FEATURE_SHUFFLE_BIT) != 0) {
glFeatures |= GL_SUBGROUP_FEATURE_SHUFFLE_BIT_KHR;
}
if ((vkFeatures & VK_SUBGROUP_FEATURE_SHUFFLE_RELATIVE_BIT) != 0) {
glFeatures |= GL_SUBGROUP_FEATURE_SHUFFLE_RELATIVE_BIT_KHR;
}
if ((vkFeatures & VK_SUBGROUP_FEATURE_CLUSTERED_BIT) != 0) {
glFeatures |= GL_SUBGROUP_FEATURE_CLUSTERED_BIT_KHR;
}
if ((vkFeatures & VK_SUBGROUP_FEATURE_QUAD_BIT) != 0) {
glFeatures |= GL_SUBGROUP_FEATURE_QUAD_BIT_KHR;
}
return glFeatures;
};
static constexpr SizeT kMaxAdvertisedShaderStorageBlockSize = 512ull * 1024ull * 1024ull;
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;
m_dynamicParameters.PointSizeRangeMin = m_vulkanCaps.PointSizeRangeMin;
m_dynamicParameters.PointSizeRangeMax = m_vulkanCaps.PointSizeRangeMax;
m_dynamicParameters.PointSizeGranularity = m_vulkanCaps.PointSizeGranularity;
m_dynamicParameters.Max3DTextureSize = m_vulkanCaps.Max3DTextureSize;
m_dynamicParameters.MaxArrayTextureLayers = m_vulkanCaps.MaxArrayTextureLayers;
m_dynamicParameters.MaxCubeMapTextureSize = m_vulkanCaps.MaxCubeMapTextureSize;
m_dynamicParameters.MaxFramebufferWidth = m_vulkanCaps.MaxFramebufferWidth;
m_dynamicParameters.MaxFramebufferHeight = m_vulkanCaps.MaxFramebufferHeight;
m_dynamicParameters.MaxFramebufferLayers = m_vulkanCaps.MaxFramebufferLayers;
m_dynamicParameters.MaxRenderbufferSize = m_vulkanCaps.MaxRenderbufferSize;
m_dynamicParameters.MaxTextureSize = m_vulkanCaps.MaxTextureSize;
m_dynamicParameters.MaxColorTextureSamples = m_vulkanCaps.MaxColorTextureSamples;
m_dynamicParameters.MaxDepthTextureSamples = m_vulkanCaps.MaxDepthTextureSamples;
m_dynamicParameters.MaxFramebufferSamples = m_vulkanCaps.MaxFramebufferSamples;
m_dynamicParameters.MaxIntegerSamples = m_vulkanCaps.MaxIntegerSamples;
m_dynamicParameters.MaxSamples = m_vulkanCaps.MaxSamples;
m_dynamicParameters.MaxSampleMaskWords = m_vulkanCaps.MaxSampleMaskWords;
const Int maxSupportedTextureUnits =
static_cast<Int>(MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS);
// GL_MAX_TEXTURE_IMAGE_UNITS is a *per-stage* sampler limit. Adreno/Qualcomm report a huge
// maxPerStageDescriptorSampledImages (descriptor-indexing scale), so clamping it only to our
// combined array capacity (192) still advertises 192 per stage. Host code treats this value as
// an array bound: Minecraft's Blaze3D GlStateManager.TEXTURES[] holds 128 entries and Iris
// iterates [0, GL_MAX_TEXTURE_IMAGE_UNITS) over it (CompositeRenderer.renderAll), so any value
// > 128 throws ArrayIndexOutOfBoundsException. Match desktop drivers (32) for the per-stage
// limits while keeping the combined limit at our texture-unit array capacity.
constexpr Int maxPerStageTextureUnits =
static_cast<Int>(MG_State::GLState::TextureState::MAX_PER_STAGE_TEXTURE_IMAGE_UNITS);
m_dynamicParameters.MaxTextureImageUnits =
std::min(m_vulkanCaps.MaxTextureImageUnits, maxPerStageTextureUnits);
m_dynamicParameters.MaxVertexTextureImageUnits =
std::min(m_vulkanCaps.MaxVertexTextureImageUnits, maxPerStageTextureUnits);
m_dynamicParameters.MaxComputeTextureImageUnits =
std::min(m_vulkanCaps.MaxComputeTextureImageUnits, maxPerStageTextureUnits);
m_dynamicParameters.MaxCombinedTextureImageUnits =
std::min(m_vulkanCaps.MaxCombinedTextureImageUnits, maxSupportedTextureUnits);
// Never advertise more attributes than the state layer can store: the current-value array and
// the Uint32 attribute masks the draw path passes around are both bounded by MAX_VERTEX_ATTRIBS.
m_dynamicParameters.MaxVertexAttribs =
std::min(m_vulkanCaps.MaxVertexAttribs,
static_cast<Int>(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS));
m_dynamicParameters.MaxComputeShaderStorageBlocks = m_vulkanCaps.MaxComputeShaderStorageBlocks;
m_dynamicParameters.MaxCombinedShaderStorageBlocks = m_vulkanCaps.MaxCombinedShaderStorageBlocks;
m_dynamicParameters.MaxComputeUniformBlocks = m_vulkanCaps.MaxComputeUniformBlocks;
m_dynamicParameters.MaxComputeWorkGroupInvocations = m_vulkanCaps.MaxComputeWorkGroupInvocations;
m_dynamicParameters.MaxShaderStorageBufferBindings = m_vulkanCaps.MaxShaderStorageBufferBindings;
m_dynamicParameters.MaxTextureBufferSize = m_vulkanCaps.MaxTextureBufferSize;
m_dynamicParameters.MaxUniformBufferBindings = m_vulkanCaps.MaxUniformBufferBindings;
m_dynamicParameters.MaxUniformBlockSize = m_vulkanCaps.MaxUniformBlockSize;
m_dynamicParameters.MaxImageUnits =
std::max(std::min(m_vulkanCaps.MaxImageUnits, maxSupportedTextureUnits), 0);
m_dynamicParameters.MaxCombinedImageUniforms = std::max(m_vulkanCaps.MaxCombinedImageUniforms, 0);
const Int maxPerStageImageUniforms =
std::min(m_dynamicParameters.MaxImageUnits, m_dynamicParameters.MaxCombinedImageUniforms);
// Vulkan uses one descriptor limit for every stage, but non-compute stores/atomics are
// optional device features. VulkanRenderer enables each feature whenever the physical
// device reports it, so these are the exact limits the logical device can compile and run.
m_dynamicParameters.MaxVertexImageUniforms =
m_vulkanCaps.SupportsVertexPipelineStoresAndAtomics ? maxPerStageImageUniforms : 0;
m_dynamicParameters.MaxGeometryImageUniforms =
m_vulkanCaps.SupportsVertexPipelineStoresAndAtomics && m_vulkanCaps.SupportsGeometryShader
? maxPerStageImageUniforms
: 0;
m_dynamicParameters.MaxFragmentImageUniforms =
m_vulkanCaps.SupportsFragmentStoresAndAtomics ? maxPerStageImageUniforms : 0;
m_dynamicParameters.MaxComputeImageUniforms =
std::min(std::max(m_vulkanCaps.MaxComputeImageUniforms, 0), maxPerStageImageUniforms);
const Int maxSupportedDrawBuffers =
static_cast<Int>(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS);
m_dynamicParameters.MaxDrawBuffers = std::min(m_vulkanCaps.MaxDrawBuffers, maxSupportedDrawBuffers);
m_dynamicParameters.MaxColorAttachments = std::min(m_vulkanCaps.MaxColorAttachments, maxSupportedDrawBuffers);
m_dynamicParameters.MaxClipDistances = m_vulkanCaps.MaxClipDistances;
m_dynamicParameters.MaxViewports = m_vulkanCaps.MaxViewports;
m_dynamicParameters.MaxViewportWidth = m_vulkanCaps.MaxViewportWidth;
m_dynamicParameters.MaxViewportHeight = m_vulkanCaps.MaxViewportHeight;
m_dynamicParameters.ViewportBoundsRangeMin = m_vulkanCaps.ViewportBoundsRangeMin;
m_dynamicParameters.ViewportBoundsRangeMax = m_vulkanCaps.ViewportBoundsRangeMax;
m_dynamicParameters.ViewportSubpixelBits = m_vulkanCaps.ViewportSubpixelBits;
m_dynamicParameters.SupportsWideLines = m_vulkanCaps.SupportsWideLines;
m_dynamicParameters.MaxShaderStorageBlockSize =
std::min(m_vulkanCaps.MaxShaderStorageBlockSize, kMaxAdvertisedShaderStorageBlockSize);
if (m_vulkanCaps.SupportsShaderSubgroup) {
m_dynamicParameters.SubgroupSize = m_vulkanCaps.SubgroupSize;
m_dynamicParameters.SubgroupSupportedStages = mapShaderStages(m_vulkanCaps.SubgroupSupportedStages);
m_dynamicParameters.SubgroupSupportedFeatures = mapSubgroupFeatures(m_vulkanCaps.SubgroupSupportedOperations);
m_dynamicParameters.SubgroupQuadOperationsInAllStages = m_vulkanCaps.SubgroupQuadOperationsInAllStages;
} else {
m_dynamicParameters.SubgroupSize = 0;
m_dynamicParameters.SubgroupSupportedStages = 0;
m_dynamicParameters.SubgroupSupportedFeatures = 0;
m_dynamicParameters.SubgroupQuadOperationsInAllStages = false;
}
if (m_dynamicParameters.MaxShaderStorageBlockSize != m_vulkanCaps.MaxShaderStorageBlockSize) {
MGLOG_I("DirectVulkan: clamped GL_MAX_SHADER_STORAGE_BLOCK_SIZE from %zu to %zu",
m_vulkanCaps.MaxShaderStorageBlockSize,
m_dynamicParameters.MaxShaderStorageBlockSize);
}
switch (m_vulkanCaps.VendorId) {
case 0x5143u: // VK_VENDOR_ID: Qualcomm
m_dynamicParameters.GpuVendor = GpuVendorKind::Qualcomm;
break;
case 0x13B5u: // ARM
m_dynamicParameters.GpuVendor = GpuVendorKind::Arm;
break;
case 0x10DEu: // NVIDIA
m_dynamicParameters.GpuVendor = GpuVendorKind::Nvidia;
break;
case 0x1002u: // AMD
m_dynamicParameters.GpuVendor = GpuVendorKind::Amd;
break;
case 0x8086u: // Intel
m_dynamicParameters.GpuVendor = GpuVendorKind::Intel;
break;
case 0x1010u: // Imagination
m_dynamicParameters.GpuVendor = GpuVendorKind::ImgTec;
break;
case 0x10005u: // Mesa software (lavapipe)
case 0x1AE0u: // Google (SwiftShader)
m_dynamicParameters.GpuVendor = GpuVendorKind::Software;
break;
default:
m_dynamicParameters.GpuVendor = GpuVendorKind::Unknown;
break;
}
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -12,29 +12,73 @@
#include <MG_Util/BackendLoaders/Vulkan/Loader.h>
namespace MobileGL::MG_Backend::DirectVulkan {
// Populates the same format-capability cache used by backend startup. Passing the
// instance-resolved function keeps standalone callers independent of global loader
// initialization; the physical device must remain valid for the duration of the call.
void PopulateFormatCapabilities(VkPhysicalDevice physicalDevice,
PFN_vkGetPhysicalDeviceFormatProperties getFormatProperties,
const MG_External::VulkanCapabilities& capabilities,
FormatCapabilityCache& cache);
class BackendObject_DirectVulkan : public BackendObject {
public:
BackendObject_DirectVulkan();
~BackendObject_DirectVulkan() override;
void Initialize() override;
Bool InitWindowSurface() override;
Bool InitCapabilities() override;
Bool InitializeEGLDisplay(EGLDisplay dpy, EGLint* major, EGLint* minor) override;
Bool CreateEGLWindowSurface(const WindowHandle& handle) override;
Bool CreateEGLWindowSurface(EGLSurface surface, const WindowHandle& handle) override;
Bool ResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height) override;
Bool CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height) override;
Bool MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) override;
Bool SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) override;
void ReleaseEGLSurface(EGLSurface surface) override;
void ReleaseEGLResources() override;
const RendererInfo& GetRendererInfo() const override;
String GetBackendAPIVersionString() const override;
const GlobalBackendFunctionsTable& GetBackendFunctions() const override;
const DynamicBackendParameters& GetDynamicParameters() const override;
BackendType GetBackendType() const override;
void ApplyVulkanCapabilitiesForTesting(const MG_External::VulkanCapabilities& capabilities);
private:
Bool InitPbufferSurface(EGLint width, EGLint height) override;
void OnEGLSurfaceReleased(EGLSurface surface) override;
void UpdateAdvertisedExtensions();
void UpdateDynamicBackendParameters();
Bool m_initialized = false;
DynamicBackendParameters m_dynamicParameters;
MG_External::VulkanCapabilities m_vulkanCaps;
RendererInfo m_rendererInfo;
};
// Single-source-of-truth helpers shared with the driver POST
// (MG_Util/SelfTest/DriverPost.cpp), so the identity strings and extension list
// MobileGL reports to applications on this backend cannot drift from what the
// POST screen shows.
// Static identity of the Magma renderer (renderer/backend names, target GL/GLSL
// versions, ExtraVendor) with the baseline extension advertisement (no shader
// subgroup, no timer queries). A live backend copies this in its constructor and
// reconciles the Extensions in UpdateAdvertisedExtensions once real capabilities
// exist; callers that need the advertised list for a known capability set must
// use BuildAdvertisedExtensions instead.
const RendererInfo& GetRendererIdentity();
// The full OpenGL extension list Magma advertises (glGetString(GL_EXTENSIONS)) for
// 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,
Bool anisotropicFilteringSupported);
// Format: <GPU Name>, Vulkan <Vulkan Version>, Driver <Driver Version> — the exact
// string an initialized backend returns from GetBackendAPIVersionString (and that
// ends up inside the application-visible GL_RENDERER string).
String FormatBackendAPIVersionString(const String& deviceName, const String& vulkanApiVersionString,
const String& driverVersionString);
} // namespace MobileGL::MG_Backend::DirectVulkan
File diff suppressed because it is too large Load Diff
@@ -8,25 +8,44 @@
#pragma once
#include <Includes.h>
#include <MG_Backend/BackendObject.h>
#include "Renderer/VulkanRenderer.h"
namespace MobileGL::MG_Backend::DirectVulkan {
extern UniquePtr<VulkanRenderer> pVulkanRenderer;
extern UniquePtr<VulkanRenderer>& pVulkanRenderer;
// Generation of the live VulkanRenderer instance, mirroring DirectGLES's
// g_syncContextGeneration. BackendObject_DirectVulkan bumps it wherever
// pVulkanRenderer is reset or recreated; fence and timer-query handles
// stamped with an older generation are stale and resolve as signaled /
// available with zero results instead of dereferencing the destroyed
// renderer's frame serials and query-pool slots.
Uint64 GetRendererGeneration();
void BumpRendererGeneration();
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);
void ClearBufferiv(GLenum buffer, GLint drawbuffer, const GLint* value);
void ClearNamedFramebufferfv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
GLint drawbuffer, const GLfloat* value);
void ClearNamedFramebufferfi(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
GLint drawbuffer, GLfloat depth, GLint stencil);
void Clear(GLbitfield mask);
void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices);
void DrawArrays(GLenum mode, GLint first, GLsizei count);
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const GLvoid* indices, GLint basevertex);
void MultiDrawArrays(GLenum mode, const GLint* first, const GLsizei* count, GLsizei drawcount);
void MultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount);
void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount, const GLint* basevertex);
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride);
void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride);
void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount,
GLsizei maxdrawcount, GLsizei stride);
void MultiDrawArraysIndirectCount(GLenum mode, const void* indirect, GLintptr drawcount,
GLsizei maxdrawcount, GLsizei stride);
void DrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type,
const void* indices, GLint basevertex);
void DrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices);
@@ -44,12 +63,72 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void DrawArraysIndirect(GLenum mode, const void* indirect);
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1,
GLint dstY1, GLbitfield mask, GLenum filter);
void BlitNamedFramebuffer(const SharedPtr<MG_State::GLState::FramebufferObject>& readFramebuffer,
const SharedPtr<MG_State::GLState::FramebufferObject>& drawFramebuffer,
GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
GLbitfield mask, GLenum filter);
void CopyTexImage2D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width,
GLsizei height, GLint border);
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
GLsizei height);
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
void GenerateMipmap(GLenum target);
void DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ);
void DispatchComputeIndirect(GLintptr indirect);
void MemoryBarrier(GLbitfield barriers);
void MemoryBarrierByRegion(GLbitfield barriers);
void BindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access,
GLenum format);
void GetIntegeri_v(GLenum target, GLuint index, GLint* data);
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data);
void GetProgramiv(GLuint program, GLenum pname, GLint* params);
void GetProgramInterfaceiv(GLuint program, GLenum programInterface, GLenum pname, GLint* params);
GLuint GetProgramResourceIndex(GLuint program, GLenum programInterface, const GLchar* name);
void GetProgramResourceName(GLuint program, GLenum programInterface, GLuint index, GLsizei bufSize,
GLsizei* length, GLchar* name);
void GetProgramResourceiv(GLuint program, GLenum programInterface, GLuint index, GLsizei propCount,
const GLenum* props, GLsizei bufSize, GLsizei* length, GLint* params);
GLint GetProgramResourceLocation(GLuint program, GLenum programInterface, const GLchar* name);
GLint GetProgramResourceLocationIndex(GLuint program, GLenum programInterface, const GLchar* name);
void ShaderStorageBlockBinding(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding);
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels);
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels);
void GetTextureImage(const SharedPtr<MG_State::GLState::ITextureObject>& texture, TextureUploadTarget uploadTarget,
GLint level, GLenum format, GLenum type, GLsizei bufSize, GLvoid* pixels);
// GL fence sync objects, mapped onto the renderer's frame-serial busy
// tracking: a fence captures the frame serial current at creation and is
// signaled once every command recorded under that serial has completed on
// the GPU.
BackendSyncHandle FenceSync();
GLenum ClientWaitSync(BackendSyncHandle sync, GLbitfield flags, GLuint64 timeout);
void WaitSync(BackendSyncHandle sync, GLbitfield flags, GLuint64 timeout);
void DeleteSync(BackendSyncHandle sync);
Bool GetSyncStatus(BackendSyncHandle sync);
// GPU timer queries (GL_TIME_ELAPSED spans and GL_TIMESTAMP one-shots),
// backed by per-frame VkQueryPool timestamp slots. All hooks degrade
// gracefully: null handles when the renderer is absent, the device lacks
// timestamp support, or the frame's pool is exhausted.
// Dynamic support check (GLFunctionsTable::IsTimerQuerySupported): true
// only while a live renderer exists whose device can actually time.
Bool IsTimerQuerySupported();
BackendQueryHandle BeginTimeElapsedQuery();
void EndTimeElapsedQuery(BackendQueryHandle query);
BackendQueryHandle QueryCounterTimestamp();
Bool IsQueryResultAvailable(BackendQueryHandle query);
// Returns true when a final value was produced (outNanoseconds set; the
// frontend may cache it and release the handle), false when the result
// cannot be obtained yet (e.g. a wait refused because the records' frame
// serial is the current unsubmitted frame) - the handle then stays
// readable later.
Bool GetQueryResult64(BackendQueryHandle query, Bool wait, Uint64* outNanoseconds);
void DeleteBackendQuery(BackendQueryHandle query);
// Always 0: Vulkan cannot synchronously sample the GPU clock (timestamps
// only exist as vkCmdWriteTimestamp results); the frontend falls back.
Int64 GetGpuTimestampNs();
void Present();
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -0,0 +1,20 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/DirectVulkanResourceState.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_State::GLState {
class ProgramObject;
}
namespace MobileGL::MG_Backend::DirectVulkan {
GLuint GetShaderStorageBlockIndex(const MG_State::GLState::ProgramObject& program, const String& name);
GLuint GetShaderStorageBlockBinding(const MG_State::GLState::ProgramObject& program, GLuint blockIndex);
}
@@ -13,6 +13,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Destroy(device, commandPool);
m_frames.assign(frameCount, {});
currentFrameIndex = 0;
m_device = device;
m_commandPool = commandPool;
Vector<VkCommandBuffer> commandBuffers(frameCount, VK_NULL_HANDLE);
VkCommandBufferAllocateInfo allocInfo{};
@@ -55,10 +57,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
DestroySwapchainSemaphores(device);
if (device != VK_NULL_HANDLE && commandPool != VK_NULL_HANDLE && !m_frames.empty()) {
for (auto& frame : m_frames) {
FreeRetiredCommandBuffers(frame);
}
vkFreeCommandBuffers(device, commandPool, frameCount, commandBuffers.data());
}
m_frames.clear();
currentFrameIndex = 0;
m_device = VK_NULL_HANDLE;
m_commandPool = VK_NULL_HANDLE;
}
FrameContext::FrameData& FrameContext::GetCurrent() {
@@ -97,6 +104,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VK_VERIFY(vkBeginCommandBuffer(frame.commandBuffer, &beginInfo), "BeginCommandRecording, vkBeginCommandBuffer");
frame.isCommandRecording = true;
if (m_recordingObserver != nullptr) {
m_recordingObserver->OnFrameCommandRecordingBegan(frame.commandBuffer);
}
return frame.commandBuffer;
}
@@ -178,9 +188,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
packet.signalSemaphore = m_swapchainImageRenderFinishedSemaphores[swapchainImageIndex];
packet.commandBuffer = frame.commandBuffer;
packet.submitInfo.waitSemaphoreCount = 1;
packet.submitInfo.pWaitSemaphores = &packet.waitSemaphore;
packet.submitInfo.pWaitDstStageMask = &packet.waitDstStageMask;
packet.submitInfo.waitSemaphoreCount = frame.imageAvailableSemaphoreConsumed ? 0U : 1U;
packet.submitInfo.pWaitSemaphores = frame.imageAvailableSemaphoreConsumed ? nullptr : &packet.waitSemaphore;
packet.submitInfo.pWaitDstStageMask = frame.imageAvailableSemaphoreConsumed ? nullptr : &packet.waitDstStageMask;
packet.submitInfo.commandBufferCount = shouldSubmitCommandBuffer ? 1U : 0U;
packet.submitInfo.pCommandBuffers = shouldSubmitCommandBuffer ? &packet.commandBuffer : nullptr;
packet.submitInfo.signalSemaphoreCount = 1;
@@ -188,18 +198,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return packet;
}
FrameContext::PresentInfoPacket FrameContext::GetPresentInfo(VkSwapchainKHR swapchain, const Uint32& imageIndex) const {
FrameContext::PresentInfoPacket FrameContext::GetPresentInfo(VkSwapchainKHR swapchain, Uint32 imageIndex) const {
AssertValidSwapchainImageIndex(imageIndex);
PresentInfoPacket packet{};
packet.waitSemaphore = m_swapchainImageRenderFinishedSemaphores[imageIndex];
packet.swapchain = swapchain;
packet.imageIndex = &imageIndex;
packet.imageIndex = imageIndex;
packet.presentInfo.waitSemaphoreCount = 1;
packet.presentInfo.pWaitSemaphores = &packet.waitSemaphore;
packet.presentInfo.swapchainCount = 1;
packet.presentInfo.pSwapchains = &packet.swapchain;
packet.presentInfo.pImageIndices = packet.imageIndex;
packet.presentInfo.pImageIndices = &packet.imageIndex;
packet.presentInfo.pResults = nullptr;
return packet;
}
@@ -211,14 +221,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (result != VK_SUCCESS) {
return result;
}
// The slot's fence has been waited: every command buffer this slot
// submitted (including mid-frame flushes) has finished executing.
FreeRetiredCommandBuffers(frame);
result = vkResetFences(device, 1, &frame.imageInFlightFence);
result = vkAcquireNextImageKHR(device, swapchain, timeout, frame.imageAvailableSemaphore, acquireFence,
&outImageIndex);
if (result != VK_SUCCESS) {
return result;
}
return vkAcquireNextImageKHR(device, swapchain, timeout, frame.imageAvailableSemaphore, acquireFence,
&outImageIndex);
frame.imageAvailableSemaphoreConsumed = false;
return vkResetFences(device, 1, &frame.imageInFlightFence);
}
Uint32 FrameContext::GetCurrentFrameIndex() const {
@@ -229,6 +243,43 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return static_cast<Uint32>(m_frames.size());
}
void FrameContext::SetRecordingObserver(IRecordingObserver* observer) {
m_recordingObserver = observer;
}
VkResult FrameContext::RetireCurrentCommandBuffer() {
MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE && m_commandPool != VK_NULL_HANDLE,
"RetireCurrentCommandBuffer requires an initialized FrameContext");
auto& frame = GetCurrent();
MOBILEGL_ASSERT(!frame.isCommandRecording,
"RetireCurrentCommandBuffer called while the command buffer is still recording");
VkCommandBufferAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
allocInfo.commandPool = m_commandPool;
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocInfo.commandBufferCount = 1;
VkCommandBuffer replacement = VK_NULL_HANDLE;
const VkResult result = vkAllocateCommandBuffers(m_device, &allocInfo, &replacement);
if (result != VK_SUCCESS) {
return result;
}
frame.retiredCommandBuffers.push_back(frame.commandBuffer);
frame.commandBuffer = replacement;
return VK_SUCCESS;
}
void FrameContext::FreeRetiredCommandBuffers(FrameData& frame) {
if (frame.retiredCommandBuffers.empty()) {
return;
}
if (m_device != VK_NULL_HANDLE && m_commandPool != VK_NULL_HANDLE) {
vkFreeCommandBuffers(m_device, m_commandPool, static_cast<Uint32>(frame.retiredCommandBuffers.size()),
frame.retiredCommandBuffers.data());
}
frame.retiredCommandBuffers.clear();
}
void FrameContext::AssertValidFrameIndex(Uint32 frameIndex) const {
MOBILEGL_ASSERT(frameIndex < m_frames.size(), "FrameContext index out of range");
}
@@ -260,6 +311,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
frame.hasCommandBufferRecorded = false;
frame.isCommandRecording = false;
frame.imageAvailableSemaphoreConsumed = false;
return VK_SUCCESS;
}
@@ -277,5 +329,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
frame.imageAvailableSemaphore = VK_NULL_HANDLE;
frame.isCommandRecording = false;
frame.hasCommandBufferRecorded = false;
frame.imageAvailableSemaphoreConsumed = false;
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -14,6 +14,17 @@
namespace MobileGL::MG_Backend::DirectVulkan {
class FrameContext {
public:
// Notified immediately after a frame command buffer begins recording
// (before any render pass has been begun); every BeginCommandRecording
// caller funnels through this single seam. Implemented by the renderer
// to prepare per-frame timer-query pools (vkCmdResetQueryPool must be
// recorded outside a render pass).
class IRecordingObserver {
public:
virtual ~IRecordingObserver() = default;
virtual void OnFrameCommandRecordingBegan(VkCommandBuffer commandBuffer) = 0;
};
struct SubmitInfoPacket {
VkPipelineStageFlags waitDstStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
VkSemaphore waitSemaphore = VK_NULL_HANDLE;
@@ -25,7 +36,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
struct PresentInfoPacket {
VkSemaphore waitSemaphore = VK_NULL_HANDLE;
VkSwapchainKHR swapchain = VK_NULL_HANDLE;
const Uint32* imageIndex = nullptr;
Uint32 imageIndex = 0;
VkPresentInfoKHR presentInfo{VK_STRUCTURE_TYPE_PRESENT_INFO_KHR};
};
@@ -35,6 +46,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkFence imageInFlightFence = VK_NULL_HANDLE;
Bool isCommandRecording = false;
Bool hasCommandBufferRecorded = false;
Bool imageAvailableSemaphoreConsumed = false;
// Command buffers submitted mid-frame (FlushPendingCommands) whose
// execution is only known complete once this slot's fence has been
// waited again; freed at that point.
Vector<VkCommandBuffer> retiredCommandBuffers;
// Submit-tracker index of this slot's most recent queue submission
// (written by the renderer at submit time).
Uint64 lastSubmitIndex = 0;
};
VkResult Initialize(VkDevice device, VkCommandPool commandPool, Uint32 frameCount);
@@ -53,13 +72,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool TransitionToPresent(VkImage image, VkImageLayout oldLayout,
VkImageLayout presentLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR);
SubmitInfoPacket GetSubmitInfo(Bool shouldSubmitCommandBuffer, Uint32 swapchainImageIndex) const;
PresentInfoPacket GetPresentInfo(VkSwapchainKHR swapchain, const Uint32& imageIndex) const;
PresentInfoPacket GetPresentInfo(VkSwapchainKHR swapchain, Uint32 imageIndex) const;
VkResult WaitAndAcquireNextImage(VkDevice device, VkSwapchainKHR swapchain, Uint32& outImageIndex,
Uint64 timeout = UINT64_MAX, VkFence acquireFence = VK_NULL_HANDLE);
// Parks the current (already ended and submitted) command buffer on the
// slot's retired list and installs a freshly allocated one, so recording
// can restart while the submitted buffer is still executing. Retired
// buffers are freed after the slot's fence is next waited.
VkResult RetireCurrentCommandBuffer();
Uint32 GetCurrentFrameIndex() const;
Uint32 GetFrameCount() const;
// Observer may be null (no notifications). Not owned.
void SetRecordingObserver(IRecordingObserver* observer);
private:
void AssertValidFrameIndex(Uint32 frameIndex) const;
void AssertValidSwapchainImageIndex(Uint32 imageIndex) const;
@@ -68,9 +96,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const VkSemaphoreCreateInfo& semaphoreInfo,
const VkFenceCreateInfo& fenceInfo);
void DestroySyncObjectsForFrame(VkDevice device, Uint32 frameIndex);
void FreeRetiredCommandBuffers(FrameData& frame);
Vector<FrameData> m_frames;
Vector<VkSemaphore> m_swapchainImageRenderFinishedSemaphores;
Uint32 currentFrameIndex = 0;
IRecordingObserver* m_recordingObserver = nullptr;
// Stored at Initialize for retired-command-buffer management.
VkDevice m_device = VK_NULL_HANDLE;
VkCommandPool m_commandPool = VK_NULL_HANDLE;
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -8,9 +8,188 @@
#include "PipelineFactory.h"
namespace MobileGL::MG_Backend::DirectVulkan {
static const char* PrimitiveTopologyToString(VkPrimitiveTopology topology) {
switch (topology) {
ENUM_STR_CASE(VK_PRIMITIVE_TOPOLOGY_POINT_LIST)
ENUM_STR_CASE(VK_PRIMITIVE_TOPOLOGY_LINE_LIST)
ENUM_STR_CASE(VK_PRIMITIVE_TOPOLOGY_LINE_STRIP)
ENUM_STR_CASE(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST)
ENUM_STR_CASE(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP)
ENUM_STR_CASE(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN)
ENUM_STR_CASE(VK_PRIMITIVE_TOPOLOGY_LINE_LIST_WITH_ADJACENCY)
ENUM_STR_CASE(VK_PRIMITIVE_TOPOLOGY_LINE_STRIP_WITH_ADJACENCY)
ENUM_STR_CASE(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST_WITH_ADJACENCY)
ENUM_STR_CASE(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP_WITH_ADJACENCY)
ENUM_STR_CASE(VK_PRIMITIVE_TOPOLOGY_PATCH_LIST)
default:
return "VK_PRIMITIVE_TOPOLOGY_UNKNOWN";
}
}
static const char* SampleCountToString(VkSampleCountFlagBits sampleCount) {
switch (sampleCount) {
ENUM_STR_CASE(VK_SAMPLE_COUNT_1_BIT)
ENUM_STR_CASE(VK_SAMPLE_COUNT_2_BIT)
ENUM_STR_CASE(VK_SAMPLE_COUNT_4_BIT)
ENUM_STR_CASE(VK_SAMPLE_COUNT_8_BIT)
ENUM_STR_CASE(VK_SAMPLE_COUNT_16_BIT)
ENUM_STR_CASE(VK_SAMPLE_COUNT_32_BIT)
ENUM_STR_CASE(VK_SAMPLE_COUNT_64_BIT)
default:
return "VK_SAMPLE_COUNT_UNKNOWN";
}
}
static const char* CullModeToString(VkCullModeFlags cullMode) {
switch (cullMode) {
case VK_CULL_MODE_NONE:
return "VK_CULL_MODE_NONE";
case VK_CULL_MODE_FRONT_BIT:
return "VK_CULL_MODE_FRONT_BIT";
case VK_CULL_MODE_BACK_BIT:
return "VK_CULL_MODE_BACK_BIT";
case VK_CULL_MODE_FRONT_AND_BACK:
return "VK_CULL_MODE_FRONT_AND_BACK";
default:
return "VK_CULL_MODE_UNKNOWN";
}
}
static const char* CompareOpToString(VkCompareOp compareOp) {
switch (compareOp) {
ENUM_STR_CASE(VK_COMPARE_OP_NEVER)
ENUM_STR_CASE(VK_COMPARE_OP_LESS)
ENUM_STR_CASE(VK_COMPARE_OP_EQUAL)
ENUM_STR_CASE(VK_COMPARE_OP_LESS_OR_EQUAL)
ENUM_STR_CASE(VK_COMPARE_OP_GREATER)
ENUM_STR_CASE(VK_COMPARE_OP_NOT_EQUAL)
ENUM_STR_CASE(VK_COMPARE_OP_GREATER_OR_EQUAL)
ENUM_STR_CASE(VK_COMPARE_OP_ALWAYS)
default:
return "VK_COMPARE_OP_UNKNOWN";
}
}
static const char* LogicOpToString(VkLogicOp logicOp) {
switch (logicOp) {
ENUM_STR_CASE(VK_LOGIC_OP_CLEAR)
ENUM_STR_CASE(VK_LOGIC_OP_AND)
ENUM_STR_CASE(VK_LOGIC_OP_AND_REVERSE)
ENUM_STR_CASE(VK_LOGIC_OP_COPY)
ENUM_STR_CASE(VK_LOGIC_OP_AND_INVERTED)
ENUM_STR_CASE(VK_LOGIC_OP_NO_OP)
ENUM_STR_CASE(VK_LOGIC_OP_XOR)
ENUM_STR_CASE(VK_LOGIC_OP_OR)
ENUM_STR_CASE(VK_LOGIC_OP_NOR)
ENUM_STR_CASE(VK_LOGIC_OP_EQUIVALENT)
ENUM_STR_CASE(VK_LOGIC_OP_INVERT)
ENUM_STR_CASE(VK_LOGIC_OP_OR_REVERSE)
ENUM_STR_CASE(VK_LOGIC_OP_COPY_INVERTED)
ENUM_STR_CASE(VK_LOGIC_OP_OR_INVERTED)
ENUM_STR_CASE(VK_LOGIC_OP_NAND)
ENUM_STR_CASE(VK_LOGIC_OP_SET)
default:
return "VK_LOGIC_OP_UNKNOWN";
}
}
PipelineFactory::PipelineFactory(VkDevice device, const VulkanRendererConfig& config):
m_device(device), m_config(config) {
MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE, "PipelineFactory: device is null");
if (m_config.DisablePipelineCache) {
MGLOG_I("DirectVulkan: pipeline cache disabled");
return;
}
VkPipelineCacheCreateInfo pipelineCacheInfo{VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO};
VK_VERIFY(vkCreatePipelineCache(m_device, &pipelineCacheInfo, nullptr, &m_pipelineCache),
"vkCreatePipelineCache");
}
// Must be called once, before any pipeline is created: the flag is not part of the
// pipeline hash, so flipping it mid-life would serve cached pipelines built under the
// old value.
void PipelineFactory::SetSuppressBlendedDepthWrite(Bool enabled) {
s_suppressBlendedDepthWrite = enabled;
}
Bool PipelineFactory::ShouldSuppressBlendedDepthWriteForDevice(MG_Config::QuirkOverride quirkOverride,
Uint32 vendorId) {
static constexpr Uint32 kVendorIdQualcomm = 0x5143;
switch (quirkOverride) {
case MG_Config::QuirkOverride::ForceOn:
return true;
case MG_Config::QuirkOverride::ForceOff:
return false;
case MG_Config::QuirkOverride::Auto:
default:
return vendorId == kVendorIdQualcomm;
}
}
namespace {
// MIN/MAX extremum blending: the signature of a depth-bounds accumulation pass
// (MC 26.3 OIT writes vec4(-linD, linD, deviceZ, 0) under GL_MAX while writing
// depth for its equality chain). MIN/MAX ignore blend factors per the Vulkan spec.
//
// Deliberately the ONLY shape stripped. A quirk should touch as little unrelated
// content as possible, and a trace sweep of every fixture showed the wider
// alternatives all cost more than they fix:
// - additive ONE+ONE with a depth write matched zero draws of the 26.3 chain
// (its transmittance/accumulate passes disable depth writes themselves) - the
// only real content it caught was harmless additive glow effects (Create);
// - sorted-transparency "over" blends (SRC_ALPHA-style) are order-dependent,
// drawn once per surface, and rely on their depth writes for occlusion;
// - separate-alpha accumulation over an over-blending color channel has no
// known pairing with a depth-equality chain (color channel only, see tests).
// If a future workload pairs another blend shape with an equality chain, widen
// this with that evidence in hand rather than pre-emptively.
Bool IsAccumulationBlend(const VkPipelineColorBlendAttachmentState& attachment) {
return attachment.colorBlendOp == VK_BLEND_OP_MIN ||
attachment.colorBlendOp == VK_BLEND_OP_MAX;
}
} // namespace
Bool PipelineFactory::ShouldSuppressDepthWrite(const PipelineCreatePayload& payload) {
if (!payload.depthWriteEnable) {
return false;
}
// A shader that assigns gl_FragDepth supplies depth itself rather than taking the
// pipeline's interpolated Z, so a driver that varies the vertex position math
// between pipelines cannot desynchronize it. (A gl_FragDepth = gl_FragCoord.z
// passthrough is the exception that stays exposed; no known content pairs one with
// an equality chain, and 26.3's composite is a genuine computed-depth writer.)
if (payload.fragmentReplacesDepth) {
return false;
}
for (Uint32 i = 0; i < payload.colorAttachmentCount; ++i) {
const VkPipelineColorBlendAttachmentState& attachment = payload.colorBlendAttachments[i];
if (attachment.blendEnable != VK_TRUE) {
continue;
}
// All color writes masked: blending is moot (depth-prepass pattern that left
// GL_BLEND enabled); stripping the depth write would delete the whole prepass.
if (attachment.colorWriteMask == 0) {
continue;
}
// Any attachment qualifies, not just attachment 0: the 26.3 transmittance pass
// accumulates into a 2-target MRT and must stay stripped.
if (IsAccumulationBlend(attachment)) {
return true;
}
}
return false;
}
PipelineFactory::~PipelineFactory() {
DestroyAll();
if (m_pipelineCache != VK_NULL_HANDLE) {
vkDestroyPipelineCache(m_device, m_pipelineCache, nullptr);
m_pipelineCache = VK_NULL_HANDLE;
}
}
PipelineFactory::HashType PipelineFactory::ComputeHash(const PipelineCreatePayload& payload) const {
@@ -19,19 +198,44 @@ namespace MobileGL::MG_Backend::DirectVulkan {
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.vertexInputHash, sizeof(payload.vertexInputHash)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.pipelineLayout, sizeof(payload.pipelineLayout)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.renderPass, sizeof(payload.renderPass)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.colorAttachmentCount, sizeof(payload.colorAttachmentCount)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.rasterizationSamples, sizeof(payload.rasterizationSamples)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.subpass, sizeof(payload.subpass)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.topology, sizeof(payload.topology)));
XXHASH_VERIFY(
XXH64_update(m_hashState, &payload.primitiveRestartEnable, sizeof(payload.primitiveRestartEnable)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.polygonMode, sizeof(payload.polygonMode)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.cullMode, sizeof(payload.cullMode)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.frontFace, sizeof(payload.frontFace)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthTestEnable, sizeof(payload.depthTestEnable)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthWriteEnable, sizeof(payload.depthWriteEnable)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthBiasEnable, sizeof(payload.depthBiasEnable)));
XXHASH_VERIFY(
XXH64_update(m_hashState, &payload.rasterizerDiscardEnable, sizeof(payload.rasterizerDiscardEnable)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.logicOpEnable, sizeof(payload.logicOpEnable)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.stencilTestEnable, sizeof(payload.stencilTestEnable)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthCompareOp, sizeof(payload.depthCompareOp)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.blendEnable, sizeof(payload.blendEnable)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.srcColorBlendFactor, sizeof(payload.srcColorBlendFactor)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.dstColorBlendFactor, sizeof(payload.dstColorBlendFactor)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.srcAlphaBlendFactor, sizeof(payload.srcAlphaBlendFactor)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.dstAlphaBlendFactor, sizeof(payload.dstAlphaBlendFactor)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.colorWriteMask, sizeof(payload.colorWriteMask)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.logicOp, sizeof(payload.logicOp)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.frontStencilFailOp, sizeof(payload.frontStencilFailOp)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.frontStencilPassOp, sizeof(payload.frontStencilPassOp)));
XXHASH_VERIFY(
XXH64_update(m_hashState, &payload.frontStencilDepthFailOp, sizeof(payload.frontStencilDepthFailOp)));
XXHASH_VERIFY(
XXH64_update(m_hashState, &payload.frontStencilCompareOp, sizeof(payload.frontStencilCompareOp)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.backStencilFailOp, sizeof(payload.backStencilFailOp)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.backStencilPassOp, sizeof(payload.backStencilPassOp)));
XXHASH_VERIFY(
XXH64_update(m_hashState, &payload.backStencilDepthFailOp, sizeof(payload.backStencilDepthFailOp)));
XXHASH_VERIFY(
XXH64_update(m_hashState, &payload.backStencilCompareOp, sizeof(payload.backStencilCompareOp)));
XXHASH_VERIFY(
XXH64_update(m_hashState, &payload.fragmentReplacesDepth, sizeof(payload.fragmentReplacesDepth)));
if (payload.colorAttachmentCount > 0) {
XXHASH_VERIFY(XXH64_update(
m_hashState,
payload.colorBlendAttachments.data(),
sizeof(payload.colorBlendAttachments[0]) * payload.colorAttachmentCount));
}
return XXH64_digest(m_hashState);
}
@@ -61,10 +265,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MOBILEGL_ASSERT(payload.vertexInputState != nullptr, "PipelineFactory: vertexInputState is null");
MOBILEGL_ASSERT(payload.pipelineLayout != VK_NULL_HANDLE, "PipelineFactory: pipelineLayout is null");
MOBILEGL_ASSERT(payload.renderPass != VK_NULL_HANDLE, "PipelineFactory: renderPass is null");
MOBILEGL_ASSERT(payload.colorAttachmentCount <= PipelineCreatePayload::kMaxColorAttachments,
"PipelineFactory: colorAttachmentCount=%u is unexpectedly large",
payload.colorAttachmentCount);
MGLOG_D("PipelineFactory::CreatePipeline: programHash=0x%llx vertexInputHash=0x%llx colorAttachmentCount=%u subpass=%u",
static_cast<unsigned long long>(payload.programHash),
static_cast<unsigned long long>(payload.vertexInputHash),
payload.colorAttachmentCount,
payload.subpass);
static constexpr VkDynamicState kDynamicStates[] = {
VK_DYNAMIC_STATE_VIEWPORT,
VK_DYNAMIC_STATE_SCISSOR
VK_DYNAMIC_STATE_SCISSOR,
VK_DYNAMIC_STATE_BLEND_CONSTANTS,
VK_DYNAMIC_STATE_DEPTH_BIAS,
VK_DYNAMIC_STATE_LINE_WIDTH,
VK_DYNAMIC_STATE_STENCIL_COMPARE_MASK,
VK_DYNAMIC_STATE_STENCIL_WRITE_MASK,
VK_DYNAMIC_STATE_STENCIL_REFERENCE
};
VkPipelineDynamicStateCreateInfo dynamicState{};
@@ -74,39 +292,66 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkPipelineInputAssemblyStateCreateInfo ia{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO};
ia.topology = payload.topology;
ia.primitiveRestartEnable = payload.primitiveRestartEnable ? VK_TRUE : VK_FALSE;
VkPipelineViewportStateCreateInfo vpci{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO};
vpci.viewportCount = 1;
vpci.scissorCount = 1;
VkPipelineRasterizationStateCreateInfo raster{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO};
raster.polygonMode = VK_POLYGON_MODE_FILL;
raster.polygonMode = payload.polygonMode;
raster.cullMode = payload.cullMode;
raster.frontFace = payload.frontFace;
raster.depthBiasEnable = payload.depthBiasEnable ? VK_TRUE : VK_FALSE;
raster.rasterizerDiscardEnable = payload.rasterizerDiscardEnable ? VK_TRUE : VK_FALSE;
raster.lineWidth = 1.0f;
VkPipelineMultisampleStateCreateInfo ms{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO};
ms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
ms.rasterizationSamples = payload.rasterizationSamples;
VkPipelineDepthStencilStateCreateInfo depthStencil{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO};
depthStencil.depthTestEnable = payload.depthTestEnable ? VK_TRUE : VK_FALSE;
depthStencil.depthWriteEnable = payload.depthWriteEnable ? VK_TRUE : VK_FALSE;
depthStencil.depthCompareOp = payload.depthCompareOp;
depthStencil.depthBoundsTestEnable = VK_FALSE;
depthStencil.stencilTestEnable = VK_FALSE;
depthStencil.stencilTestEnable = payload.stencilTestEnable ? VK_TRUE : VK_FALSE;
if (payload.stencilTestEnable) {
depthStencil.front.failOp = payload.frontStencilFailOp;
depthStencil.front.passOp = payload.frontStencilPassOp;
depthStencil.front.depthFailOp = payload.frontStencilDepthFailOp;
depthStencil.front.compareOp = payload.frontStencilCompareOp;
depthStencil.front.compareMask = 0xffffffffu;
depthStencil.front.writeMask = 0xffffffffu;
depthStencil.front.reference = 0;
depthStencil.back.failOp = payload.backStencilFailOp;
depthStencil.back.passOp = payload.backStencilPassOp;
depthStencil.back.depthFailOp = payload.backStencilDepthFailOp;
depthStencil.back.compareOp = payload.backStencilCompareOp;
depthStencil.back.compareMask = 0xffffffffu;
depthStencil.back.writeMask = 0xffffffffu;
depthStencil.back.reference = 0;
}
VkPipelineColorBlendAttachmentState colorAttach{};
colorAttach.colorWriteMask = payload.colorWriteMask;
colorAttach.blendEnable = payload.blendEnable ? VK_TRUE : VK_FALSE;
colorAttach.srcColorBlendFactor = payload.srcColorBlendFactor;
colorAttach.dstColorBlendFactor = payload.dstColorBlendFactor;
colorAttach.colorBlendOp = VK_BLEND_OP_ADD;
colorAttach.srcAlphaBlendFactor = payload.srcAlphaBlendFactor;
colorAttach.dstAlphaBlendFactor = payload.dstAlphaBlendFactor;
colorAttach.alphaBlendOp = VK_BLEND_OP_ADD;
Vector<VkPipelineColorBlendAttachmentState> colorAttachments(payload.colorAttachmentCount);
for (Uint32 i = 0; i < payload.colorAttachmentCount; ++i) {
colorAttachments[i] = payload.colorBlendAttachments[i];
}
// Suppress depth writes on accumulation-blended pipelines when the active driver
// cannot keep vertex positions invariant across the pipelines of a multi-pass
// depth-equality chain (see SetSuppressBlendedDepthWrite). The decision is narrowed
// in ShouldSuppressDepthWrite: sorted-transparency "over" blends (vanilla MC water),
// gl_FragDepth writers, and masked-out attachments keep their depth writes.
// This bakes the decision into the pipeline, which only works because depth write is
// static state here - adding VK_DYNAMIC_STATE_DEPTH_WRITE_ENABLE to kDynamicStates
// would let the record-time value override it and silently disable the quirk.
if (s_suppressBlendedDepthWrite && ShouldSuppressDepthWrite(payload)) {
depthStencil.depthWriteEnable = VK_FALSE;
}
VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO};
blend.attachmentCount = 1;
blend.pAttachments = &colorAttach;
blend.logicOpEnable = payload.logicOpEnable ? VK_TRUE : VK_FALSE;
blend.logicOp = payload.logicOp;
blend.attachmentCount = payload.colorAttachmentCount;
blend.pAttachments = colorAttachments.empty() ? nullptr : colorAttachments.data();
VkGraphicsPipelineCreateInfo gpi{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO};
gpi.stageCount = static_cast<Uint32>(payload.stages->size());
@@ -124,8 +369,52 @@ namespace MobileGL::MG_Backend::DirectVulkan {
gpi.subpass = payload.subpass;
VkPipeline pipeline = VK_NULL_HANDLE;
VK_VERIFY(vkCreateGraphicsPipelines(m_device, VK_NULL_HANDLE, 1, &gpi, nullptr, &pipeline),
"vkCreateGraphicsPipelines");
const VkResult result = vkCreateGraphicsPipelines(m_device, m_pipelineCache, 1, &gpi, nullptr, &pipeline);
if (result != VK_SUCCESS) {
MGLOG_F("PipelineFactory::CreatePipeline failed: result=%s (%d) programHash=0x%llx vertexInputHash=0x%llx stageCount=%u topology=%s(%d) colorAttachmentCount=%u samples=%s(%d) subpass=%u",
VkResultToString(result),
result,
static_cast<unsigned long long>(payload.programHash),
static_cast<unsigned long long>(payload.vertexInputHash),
gpi.stageCount,
PrimitiveTopologyToString(payload.topology),
payload.topology,
payload.colorAttachmentCount,
SampleCountToString(payload.rasterizationSamples),
payload.rasterizationSamples,
payload.subpass);
MGLOG_F("PipelineFactory::CreatePipeline state: cullMode=%s(0x%x) frontFace=%d depthTest=%d depthWrite=%d depthCompare=%s(%d) depthBias=%d rasterizerDiscard=%d stencilTest=%d logicOpEnable=%d logicOp=%s(%d)",
CullModeToString(payload.cullMode),
static_cast<Uint32>(payload.cullMode),
payload.frontFace,
payload.depthTestEnable ? 1 : 0,
payload.depthWriteEnable ? 1 : 0,
CompareOpToString(payload.depthCompareOp),
payload.depthCompareOp,
payload.depthBiasEnable ? 1 : 0,
payload.rasterizerDiscardEnable ? 1 : 0,
payload.stencilTestEnable ? 1 : 0,
payload.logicOpEnable ? 1 : 0,
LogicOpToString(payload.logicOp),
payload.logicOp);
MGLOG_F("PipelineFactory::CreatePipeline vertex input: bindingCount=%u attributeCount=%u",
payload.vertexInputState->vertexBindingDescriptionCount,
payload.vertexInputState->vertexAttributeDescriptionCount);
for (Uint32 i = 0; i < payload.colorAttachmentCount; ++i) {
const auto& attachment = payload.colorBlendAttachments[i];
MGLOG_F("PipelineFactory::CreatePipeline colorAttachment[%u]: blend=%d colorWriteMask=0x%x srcColor=%d dstColor=%d colorOp=%d srcAlpha=%d dstAlpha=%d alphaOp=%d",
i,
attachment.blendEnable == VK_TRUE ? 1 : 0,
static_cast<Uint32>(attachment.colorWriteMask),
attachment.srcColorBlendFactor,
attachment.dstColorBlendFactor,
attachment.colorBlendOp,
attachment.srcAlphaBlendFactor,
attachment.dstAlphaBlendFactor,
attachment.alphaBlendOp);
}
}
VK_VERIFY(result, "vkCreateGraphicsPipelines");
return pipeline;
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -10,6 +10,7 @@
#include "Config.h"
#include "../VkIncludes.h"
#include "MG_State/GLState/FramebufferState/FramebufferObject.h"
#include <Includes.h>
namespace MobileGL::MG_Backend::DirectVulkan {
@@ -18,31 +19,45 @@ namespace MobileGL::MG_Backend::DirectVulkan {
using HashType = Uint64;
struct PipelineCreatePayload {
static constexpr Uint32 kMaxColorAttachments = MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS;
HashType programHash = 0;
HashType vertexInputHash = 0;
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
VkRenderPass renderPass = VK_NULL_HANDLE;
Uint32 colorAttachmentCount = 1;
VkSampleCountFlagBits rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
Uint32 subpass = 0;
VkPrimitiveTopology topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
Bool primitiveRestartEnable = false;
VkPolygonMode polygonMode = VK_POLYGON_MODE_FILL;
VkCullModeFlags cullMode = VK_CULL_MODE_BACK_BIT;
VkFrontFace frontFace = VK_FRONT_FACE_CLOCKWISE;
Bool depthTestEnable = false;
Bool depthWriteEnable = false;
Bool depthBiasEnable = false;
Bool rasterizerDiscardEnable = false;
Bool logicOpEnable = false;
Bool stencilTestEnable = false;
VkCompareOp depthCompareOp = VK_COMPARE_OP_ALWAYS;
Bool blendEnable = false;
VkBlendFactor srcColorBlendFactor = VK_BLEND_FACTOR_ONE;
VkBlendFactor dstColorBlendFactor = VK_BLEND_FACTOR_ZERO;
VkBlendFactor srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE;
VkBlendFactor dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO;
VkColorComponentFlags colorWriteMask =
VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT |
VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
VkLogicOp logicOp = VK_LOGIC_OP_COPY;
VkStencilOp frontStencilFailOp = VK_STENCIL_OP_KEEP;
VkStencilOp frontStencilPassOp = VK_STENCIL_OP_KEEP;
VkStencilOp frontStencilDepthFailOp = VK_STENCIL_OP_KEEP;
VkCompareOp frontStencilCompareOp = VK_COMPARE_OP_ALWAYS;
VkStencilOp backStencilFailOp = VK_STENCIL_OP_KEEP;
VkStencilOp backStencilPassOp = VK_STENCIL_OP_KEEP;
VkStencilOp backStencilDepthFailOp = VK_STENCIL_OP_KEEP;
VkCompareOp backStencilCompareOp = VK_COMPARE_OP_ALWAYS;
// The fragment module writes gl_FragDepth (SPIR-V DepthReplacing); exempts the
// pipeline from the blended depth-write quirk (see ShouldSuppressDepthWrite).
Bool fragmentReplacesDepth = false;
Array<VkPipelineColorBlendAttachmentState, kMaxColorAttachments> colorBlendAttachments{};
const Vector<VkPipelineShaderStageCreateInfo>* stages = nullptr;
const VkPipelineVertexInputStateCreateInfo* vertexInputState = nullptr;
};
explicit PipelineFactory(VkDevice device, const VulkanRendererConfig& config):
m_device(device), m_config(config) {}
explicit PipelineFactory(VkDevice device, const VulkanRendererConfig& config);
~PipelineFactory();
PipelineFactory(const PipelineFactory&) = delete;
@@ -50,12 +65,35 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkPipeline GetOrCreatePipeline(const PipelineCreatePayload& payload);
void DestroyAll();
// Driver quirk: suppress depth writes on accumulation-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. Only MIN/MAX extremum blends are stripped - the signature of such a
// chain's depth-bounds pass (MC 26.3 OIT), and per a fixture-wide trace sweep the
// only depth-writing shape the chain actually uses - so every other blend
// (sorted-transparency "over" like vanilla MC water, additive glows, ...) keeps
// its depth writes. Set at renderer initialization based on the active driver.
static void SetSuppressBlendedDepthWrite(Bool enabled);
static Bool IsSuppressBlendedDepthWriteEnabled() { return s_suppressBlendedDepthWrite; }
// Device gate for the quirk: ForceOn/ForceOff bypass detection, Auto enables it on
// the known-affected vendor (Qualcomm).
static Bool ShouldSuppressBlendedDepthWriteForDevice(MG_Config::QuirkOverride quirkOverride,
Uint32 vendorId);
// Pure per-pipeline strip decision (exempts gl_FragDepth writers, masked-out and
// non-accumulation blends); combined with the device flag in CreatePipeline. Static
// and payload-only so tests can pin the contract without a VkDevice.
static Bool ShouldSuppressDepthWrite(const PipelineCreatePayload& payload);
private:
VkPipeline CreatePipeline(const PipelineCreatePayload& payload) const;
VkDevice m_device = VK_NULL_HANDLE;
const VulkanRendererConfig& m_config;
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
File diff suppressed because it is too large Load Diff
@@ -14,14 +14,25 @@
#include "MG_State/GLState/TextureState/TextureEnum.h"
#include <Includes.h>
#include <spirv_reflect.h>
namespace MobileGL::MG_Backend::DirectVulkan {
enum class SamplerNumericDomain : Uint8 {
Unknown = 0,
Float,
SignedInteger,
UnsignedInteger,
};
class ProgramFactory {
public:
enum class DescriptorBindingKind : Uint8 {
None = 0,
UniformBufferDynamic,
CombinedImageSampler
CombinedImageSampler,
UniformTexelBuffer,
StorageBuffer,
StorageImage
};
enum class CompileOptionBit : Uint {
@@ -36,6 +47,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
using HashType = Uint64;
struct VkProgramObject {
static constexpr Uint32 kMaxVertexInputLocations = 32;
HashType hash = 0;
Vector<VkPipelineShaderStageCreateInfo> stages;
Vector<VkShaderModule> modules;
@@ -45,9 +58,36 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
Vector<DescriptorBindingKind> bindingKinds;
Vector<Uint32> dynamicBindings;
Vector<Int> uniformBlockIndexByBinding;
// Descriptor count per binding (1 except for UBO instance arrays, which occupy one
// binding with descriptorCount = N).
Vector<Uint16> bindingDescriptorCounts;
// Per-element GL uniform block indices for arrayed UBO bindings (count > 1);
// element 0 of a non-arrayed binding stays in uniformBlockIndexByBinding.
UnorderedMap<Uint32, Vector<Int>> arrayedUniformBlockIndicesByBinding;
Vector<String> samplerNameByBinding;
Vector<Int> samplerUniformLocationByBinding;
Vector<TextureTarget> samplerTextureTargetByBinding;
Vector<SamplerNumericDomain> samplerNumericDomainByBinding;
Vector<VkFormat> storageImageFormatByBinding;
Vector<Bool> storageImageUsesBindingFormatByBinding;
Vector<String> storageBlockNameByBinding;
Vector<Int> storageBlockIndexByBinding;
// Set once during ReflectLayout so the per-draw path can skip the whole
// storage-image preparation for the overwhelming majority of programs.
Bool hasStorageImages = false;
Int globalUboBinding = -1;
Uint32 activeVertexInputLocationMask = 0;
Array<GLenum, kMaxVertexInputLocations> vertexInputTypes{};
Uint32 activeFragmentOutputLocationMask = 0;
Array<GLenum, kMaxVertexInputLocations> fragmentOutputTypes{};
ShaderStage rasterizationProducerStage = ShaderStage::Unknown;
Uint32 producerOutputComponentCount = 0;
Uint32 fragmentInputComponentCount = 0;
// The fragment module declares the DepthReplacing execution mode (writes
// gl_FragDepth); shader-computed depth is immune to the cross-pipeline
// position-invariance quirk (see PipelineFactory::ShouldSuppressDepthWrite).
Bool fragmentReplacesDepth = false;
static inline VkDevice s_device = VK_NULL_HANDLE;
@@ -62,13 +102,39 @@ namespace MobileGL::MG_Backend::DirectVulkan {
pipelineLayout = other.pipelineLayout;
bindingKinds = std::move(other.bindingKinds);
dynamicBindings = std::move(other.dynamicBindings);
uniformBlockIndexByBinding = std::move(other.uniformBlockIndexByBinding);
bindingDescriptorCounts = std::move(other.bindingDescriptorCounts);
arrayedUniformBlockIndicesByBinding = std::move(other.arrayedUniformBlockIndicesByBinding);
samplerNameByBinding = std::move(other.samplerNameByBinding);
samplerUniformLocationByBinding = std::move(other.samplerUniformLocationByBinding);
samplerTextureTargetByBinding = std::move(other.samplerTextureTargetByBinding);
samplerNumericDomainByBinding = std::move(other.samplerNumericDomainByBinding);
storageImageFormatByBinding = std::move(other.storageImageFormatByBinding);
storageImageUsesBindingFormatByBinding =
std::move(other.storageImageUsesBindingFormatByBinding);
storageBlockNameByBinding = std::move(other.storageBlockNameByBinding);
storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding);
hasStorageImages = other.hasStorageImages;
globalUboBinding = other.globalUboBinding;
activeVertexInputLocationMask = other.activeVertexInputLocationMask;
vertexInputTypes = other.vertexInputTypes;
activeFragmentOutputLocationMask = other.activeFragmentOutputLocationMask;
fragmentOutputTypes = other.fragmentOutputTypes;
rasterizationProducerStage = other.rasterizationProducerStage;
producerOutputComponentCount = other.producerOutputComponentCount;
fragmentInputComponentCount = other.fragmentInputComponentCount;
fragmentReplacesDepth = other.fragmentReplacesDepth;
other.hash = 0;
other.descriptorSetLayout = VK_NULL_HANDLE;
other.pipelineLayout = VK_NULL_HANDLE;
other.hasStorageImages = false;
other.globalUboBinding = -1;
other.activeVertexInputLocationMask = 0;
other.activeFragmentOutputLocationMask = 0;
other.rasterizationProducerStage = ShaderStage::Unknown;
other.producerOutputComponentCount = 0;
other.fragmentInputComponentCount = 0;
other.fragmentReplacesDepth = false;
}
VkProgramObject& operator=(VkProgramObject&& other) noexcept {
if (this == &other) {
@@ -82,13 +148,39 @@ namespace MobileGL::MG_Backend::DirectVulkan {
pipelineLayout = other.pipelineLayout;
bindingKinds = std::move(other.bindingKinds);
dynamicBindings = std::move(other.dynamicBindings);
uniformBlockIndexByBinding = std::move(other.uniformBlockIndexByBinding);
bindingDescriptorCounts = std::move(other.bindingDescriptorCounts);
arrayedUniformBlockIndicesByBinding = std::move(other.arrayedUniformBlockIndicesByBinding);
samplerNameByBinding = std::move(other.samplerNameByBinding);
samplerUniformLocationByBinding = std::move(other.samplerUniformLocationByBinding);
samplerTextureTargetByBinding = std::move(other.samplerTextureTargetByBinding);
samplerNumericDomainByBinding = std::move(other.samplerNumericDomainByBinding);
storageImageFormatByBinding = std::move(other.storageImageFormatByBinding);
storageImageUsesBindingFormatByBinding =
std::move(other.storageImageUsesBindingFormatByBinding);
storageBlockNameByBinding = std::move(other.storageBlockNameByBinding);
storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding);
hasStorageImages = other.hasStorageImages;
globalUboBinding = other.globalUboBinding;
activeVertexInputLocationMask = other.activeVertexInputLocationMask;
vertexInputTypes = other.vertexInputTypes;
activeFragmentOutputLocationMask = other.activeFragmentOutputLocationMask;
fragmentOutputTypes = other.fragmentOutputTypes;
rasterizationProducerStage = other.rasterizationProducerStage;
producerOutputComponentCount = other.producerOutputComponentCount;
fragmentInputComponentCount = other.fragmentInputComponentCount;
fragmentReplacesDepth = other.fragmentReplacesDepth;
other.hash = 0;
other.descriptorSetLayout = VK_NULL_HANDLE;
other.pipelineLayout = VK_NULL_HANDLE;
other.hasStorageImages = false;
other.globalUboBinding = -1;
other.activeVertexInputLocationMask = 0;
other.activeFragmentOutputLocationMask = 0;
other.rasterizationProducerStage = ShaderStage::Unknown;
other.producerOutputComponentCount = 0;
other.fragmentInputComponentCount = 0;
other.fragmentReplacesDepth = false;
return *this;
}
@@ -118,8 +210,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
};
explicit ProgramFactory(VkDevice device, const VulkanRendererConfig& config, Uint32 maxBindings = 16)
: m_device(device), m_config(config), m_maxBindings(maxBindings) {
explicit ProgramFactory(VkDevice device, const VulkanRendererConfig& config, Uint32 maxBindings = 16,
Bool shaderDrawParametersEnabled = false,
Bool unformattedFloatStorageImagesEnabled = false)
: m_device(device), m_maxBindings(maxBindings), m_config(config),
m_shaderDrawParametersEnabled(shaderDrawParametersEnabled),
m_unformattedFloatStorageImagesEnabled(unformattedFloatStorageImagesEnabled) {
VkProgramObject::s_device = device;
}
~ProgramFactory() = default;
@@ -130,15 +226,46 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags);
static VkShaderStageFlagBits ToVkStage(ShaderStage stage);
static VkFormat ConvertSpirvImageFormatToVkFormat(SpvImageFormat format);
static SamplerNumericDomain UniformTypeToSamplerNumericDomain(GLenum glType);
// True when any entry point declares the DepthReplacing execution mode, i.e. the
// shader assigns gl_FragDepth. Exposed so the blended depth-write quirk's exemption
// can be pinned by tests. A false negative loses the exemption, so such a shader is
// stripped conservatively and forfeits its depth write.
static Bool ReflectedFragmentReplacesDepth(const SpvReflectShaderModule& reflectModule);
// True when an entry point reads the InstanceIndex builtin. Only gates a diagnostic:
// without shaderDrawParameters such a shader cannot have gl_InstanceID rebased.
static Bool ReflectedReadsInstanceIndexBuiltin(const SpvReflectShaderModule& reflectModule);
private:
struct ProgramLookupCache {
const MG_State::GLState::ProgramObject* program = nullptr;
Uint32 backendStateVersion = 0;
CompileOptionFlags flags{};
HashType hash = 0;
};
static TextureTarget UniformTypeToTextureTarget(GLenum glType);
void ReflectLayout(const MG_State::GLState::ProgramObject& program, VkProgramObject& entry) const;
void ReflectVertexInputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
const Vector<Vector<Uint>>& spirv,
VkProgramObject& entry) const;
void ReflectFragmentOutputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
const Vector<Vector<Uint>>& spirv,
VkProgramObject& entry) const;
void ReflectLayout(const MG_State::GLState::ProgramObject& program, const Vector<Vector<Uint>>& spirv,
VkProgramObject& entry) const;
VkDevice m_device = VK_NULL_HANDLE;
Uint32 m_maxBindings = 0;
UnorderedMap<HashType, VkProgramObject> m_cache;
const VulkanRendererConfig& m_config;
// True when the device enabled shaderDrawParameters; gates the InstanceIndex rebase pass
// (which needs the DrawParameters capability / gl_BaseInstance builtin).
Bool m_shaderDrawParametersEnabled = false;
// True only when the logical device enabled both
// shaderStorageImageReadWithoutFormat and shaderStorageImageWriteWithoutFormat.
Bool m_unformattedFloatStorageImagesEnabled = false;
mutable ProgramLookupCache m_lastLookup;
static inline XXH64_state_t* m_hashState = XXH64_createState();
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -31,8 +31,19 @@ static const char* string_VkColorSpaceKHR(VkColorSpaceKHR) {
return "VkColorSpaceKHR(unknown)";
}
static const char* string_VkPresentModeKHR(VkPresentModeKHR) {
return "VkPresentModeKHR(unknown)";
static const char* string_VkPresentModeKHR(VkPresentModeKHR presentMode) {
switch (presentMode) {
case VK_PRESENT_MODE_IMMEDIATE_KHR:
return "VK_PRESENT_MODE_IMMEDIATE_KHR";
case VK_PRESENT_MODE_MAILBOX_KHR:
return "VK_PRESENT_MODE_MAILBOX_KHR";
case VK_PRESENT_MODE_FIFO_KHR:
return "VK_PRESENT_MODE_FIFO_KHR";
case VK_PRESENT_MODE_FIFO_RELAXED_KHR:
return "VK_PRESENT_MODE_FIFO_RELAXED_KHR";
default:
return "VkPresentModeKHR(unknown)";
}
}
static const char* string_VkSurfaceTransformFlagBitsKHR(VkSurfaceTransformFlagBitsKHR) {
@@ -105,7 +116,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkSurfaceFormatKHR SwapchainObject::ChooseSwapchainSurfaceFormat(
const Vector<VkSurfaceFormatKHR>& availableFormats) {
for (const auto& availableFormat : availableFormats) {
if (availableFormat.format == VK_FORMAT_B8G8R8A8_SRGB &&
if ((availableFormat.format == VK_FORMAT_B8G8R8A8_UNORM ||
availableFormat.format == VK_FORMAT_R8G8B8A8_UNORM) &&
availableFormat.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) {
return availableFormat;
}
}
for (const auto& availableFormat : availableFormats) {
if ((availableFormat.format == VK_FORMAT_B8G8R8A8_SRGB ||
availableFormat.format == VK_FORMAT_R8G8B8A8_SRGB) &&
availableFormat.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) {
return availableFormat;
}
@@ -130,7 +149,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
void SwapchainObject::Create(VkDevice device, VkPhysicalDevice physicalDevice, VkSurfaceKHR surface,
Uint32 graphicsQueueFamily, Uint32 presentQueueFamily, Uint32 minImageCountHint) {
Uint32 graphicsQueueFamily, Uint32 presentQueueFamily, Uint32 minImageCountHint,
VkExtent2D desiredExtent) {
const auto swapchainCapabilities = GetSwapchainCapabilities(physicalDevice, surface);
MOBILEGL_ASSERT(swapchainCapabilities.IsComplete(),
"SwapchainObject::Create failed: incomplete swapchain capabilities");
@@ -153,7 +173,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MGLOG_I("Picked present mode: %s", string_VkPresentModeKHR(presentMode));
const auto& swapchainCaps = swapchainCapabilities.capabilities;
const auto targetImageCount = std::max<Uint32>(minImageCountHint, swapchainCaps.minImageCount);
Uint32 targetImageCount = std::max<Uint32>(minImageCountHint, swapchainCaps.minImageCount);
if (swapchainCaps.maxImageCount != 0) {
targetImageCount = std::min(targetImageCount, swapchainCaps.maxImageCount);
}
MGLOG_I("Set minImageCount = %u", targetImageCount);
MGLOG_I("Swapchain currentTransform = %s",
string_VkSurfaceTransformFlagBitsKHR(swapchainCaps.currentTransform));
@@ -164,6 +187,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
createInfo.imageFormat = pickedSurfaceFormat.format;
createInfo.imageColorSpace = pickedSurfaceFormat.colorSpace;
createInfo.imageExtent = swapchainCaps.currentExtent;
if (createInfo.imageExtent.width == UINT32_MAX || createInfo.imageExtent.height == UINT32_MAX) {
createInfo.imageExtent.width = std::clamp(desiredExtent.width,
swapchainCaps.minImageExtent.width,
swapchainCaps.maxImageExtent.width);
createInfo.imageExtent.height = std::clamp(desiredExtent.height,
swapchainCaps.minImageExtent.height,
swapchainCaps.maxImageExtent.height);
}
const VkExtent2D defaultFramebufferExtent = createInfo.imageExtent;
if (swapchainCaps.currentTransform == VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR ||
swapchainCaps.currentTransform == VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR) {
std::swap(createInfo.imageExtent.width, createInfo.imageExtent.height);
@@ -234,11 +266,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Properly initialize Default FBO here
auto& defaultFBOInfo = MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo;
const Int extentWidth = static_cast<Int>(defaultFramebufferExtent.width);
const Int extentHeight = static_cast<Int>(defaultFramebufferExtent.height);
const SizeT defaultAttachmentByteSize =
static_cast<SizeT>(defaultFramebufferExtent.width) *
static_cast<SizeT>(defaultFramebufferExtent.height) * 4;
auto* colorTex = static_cast<MG_State::GLState::TextureObject2D*>(defaultFBOInfo->colorAttachment.get());
colorTex->AllocateStorage(
TextureUploadTarget::Texture2D, 0, {
{(Int)createInfo.imageExtent.width, (Int)createInfo.imageExtent.height, 1},
createInfo.imageExtent.width * (Int)createInfo.imageExtent.height * 4}); // TODO: 4 is format size
{extentWidth, extentHeight, 1},
defaultAttachmentByteSize}); // TODO: 4 is format size
TextureInternalFormat depthFormat = TextureInternalFormat::Depth24Stencil8;
switch (m_depthStencilFormat) {
case VK_FORMAT_D24_UNORM_S8_UINT:
@@ -257,8 +295,32 @@ namespace MobileGL::MG_Backend::DirectVulkan {
auto* depthTex = static_cast<MG_State::GLState::TextureObject2D*>(defaultFBOInfo->depthAttachment.get());
depthTex->SetInternalFormat(depthFormat);
depthTex->AllocateStorage(TextureUploadTarget::Texture2D, 0, {
{(Int)createInfo.imageExtent.width, (Int)createInfo.imageExtent.height, 1},
createInfo.imageExtent.width * createInfo.imageExtent.width * 4}); // TODO: 4 is format size
{extentWidth, extentHeight, 1},
defaultAttachmentByteSize}); // TODO: 4 is format size
// The default FBO's stencil attachment must track the swapchain extent:
// FramebufferObject::CheckCompleteness requires every valid attachment
// to share the same dimensions, and Init.cpp leaves a 512x512 placeholder.
// Without this the retrace-layer glReadPixels snapshot fails with
// GL_INVALID_FRAMEBUFFER_OPERATION on DirectVulkan.
TextureInternalFormat stencilFormat = TextureInternalFormat::Depth24Stencil8;
switch (m_depthStencilFormat) {
case VK_FORMAT_D32_SFLOAT_S8_UINT:
stencilFormat = TextureInternalFormat::Depth32FStencil8;
break;
case VK_FORMAT_D24_UNORM_S8_UINT:
stencilFormat = TextureInternalFormat::Depth24Stencil8;
break;
default:
// No stencil plane; mirror the depth format for consistency.
stencilFormat = depthFormat;
break;
}
auto* stencilTex = static_cast<MG_State::GLState::TextureObject2D*>(defaultFBOInfo->stencilAttachment.get());
stencilTex->SetInternalFormat(stencilFormat);
stencilTex->AllocateStorage(TextureUploadTarget::Texture2D, 0, {
{extentWidth, extentHeight, 1},
defaultAttachmentByteSize}); // TODO: 4 is format size
}
@@ -290,7 +352,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
imageInfo.format = m_depthStencilFormat;
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
imageInfo.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;
imageInfo.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
VK_VERIFY(vkCreateImage(device, &imageInfo, nullptr, &m_depthStencilImages[i]), "vkCreateImage(depth)");
@@ -29,7 +29,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
static VkPresentModeKHR ChooseSwapchainPresentMode(const Vector<VkPresentModeKHR>& availablePresentModes);
void Create(VkDevice device, VkPhysicalDevice physicalDevice, VkSurfaceKHR surface, Uint32 graphicsQueueFamily,
Uint32 presentQueueFamily, Uint32 minImageCountHint);
Uint32 presentQueueFamily, Uint32 minImageCountHint, VkExtent2D desiredExtent);
void Shutdown(VkDevice device);
VkSwapchainKHR GetHandle() const { return m_swapchain; }
File diff suppressed because it is too large Load Diff
@@ -28,6 +28,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 binding = 0;
MG_State::GLState::ITextureObject* texture = nullptr;
const MG_State::GLState::SamplerObject* sampler = nullptr;
VkImageView imageView = VK_NULL_HANDLE;
};
Bool Initialize(VkDevice device, VkBufferManager* bufferManager,
@@ -41,12 +42,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool CollectSampledTextures(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj,
Vector<MG_State::GLState::ITextureObject*>& outTextures);
Bool CollectStorageImageTextures(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj,
Vector<MG_State::GLState::ITextureObject*>& outTextures) const;
Bool BindProgramUniformBuffers(VkCommandBuffer commandBuffer,
const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj,
Uint32 frameIndex,
VkPipelineBindPoint bindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
const SamplerBindingOverride* samplerBindingOverride = nullptr);
// Pure format-policy helper kept public for host regression tests. Formatted storage
// images use their shader qualifier; transformed float images use glBindImageTexture's
// format and never silently fall back to the backing image format.
static VkFormat ResolveStorageImageViewFormat(VkFormat reflectedFormat, GLenum bindingFormat,
VkFormat resourceFormat, Bool useBindingFormat);
private:
struct DescriptorPoolBucket {
VkDescriptorPool handle = VK_NULL_HANDLE;
@@ -54,27 +65,65 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 allocatedSets = 0;
};
struct DescriptorSetCacheEntry {
Vector<VkDescriptorSet> sets;
Uint32 cursor = 0;
};
struct FrameResources {
Vector<DescriptorPoolBucket> descriptorPools;
UnorderedMap<VkDescriptorSetLayout, DescriptorSetCacheEntry> descriptorSetCacheByLayout;
Vector<VkBufferView> texelBufferViews;
Uint32 activeDescriptorPoolIndex = 0;
Uint32 allocatedSetsThisFrame = 0;
Uint32 peakAllocatedSetsThisFrame = 0;
};
Bool ResolveSamplerTexture(const MG_State::GLState::ProgramObject& program,
static Bool ResolveSamplerTexture(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
SharedPtr<MG_State::GLState::ITextureObject>& outTexture) const;
SharedPtr<MG_State::GLState::ITextureObject>& outTexture);
// Raw-pointer variant for the per-draw sampled-texture walk (CollectSampledTextures):
// the bound texture stays alive through the draw via GL binding state, so callers that
// only need the pointer skip the SharedPtr copy's atomic refcount churn.
static MG_State::GLState::ITextureObject* ResolveSamplerTextureRaw(
const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding);
SharedPtr<MG_State::GLState::ITextureObject> GetFallbackTexture(TextureTarget target) const;
Bool ResolveSamplerDescriptor(VkCommandBuffer commandBuffer, const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
VkDescriptorImageInfo& outImageInfo) const;
Bool ResolveSamplerDescriptorOverride(const SamplerBindingOverride& samplerBindingOverride,
VkDescriptorImageInfo& outImageInfo) const;
Bool GatherBindingPayloads(const MG_State::GLState::ProgramObject& program, Vector<const void*>& outData,
Vector<VkDeviceSize>& outSizes) const;
Bool ResolveTexelBufferDescriptor(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
Uint32 frameIndex, VkBufferView& outBufferView);
Bool ResolveStorageBufferDescriptor(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
VkDescriptorBufferInfo& outBufferInfo) const;
Bool ResolveStorageImageDescriptor(VkCommandBuffer commandBuffer,
const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
VkDescriptorImageInfo& outImageInfo) const;
// Result of resolving a UBO binding: either a zero-copy direct bind to the app's resident
// VkBuffer (the GLES backend's approach - no per-draw copy) or the CPU payload to upload.
struct UboBindResult {
Bool directBindable = false;
VkBuffer buffer = VK_NULL_HANDLE;
VkDeviceSize range = 0; // reflected block size; constant across draws (hashed)
VkDeviceSize dynamicOffset = 0; // block range start; moves per draw (NOT hashed)
const void* payload = nullptr; // fallback UploadTransient path
VkDeviceSize payloadSize = 0;
};
Bool ResolveUniformBufferPayload(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
Uint32 arrayElement, UboBindResult& out) const;
Bool CreateDescriptorPool(Uint32 maxSets, VkDescriptorPool& outPool) const;
Bool GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex);
VkResult AllocateDescriptorSetsFromActivePool(
Uint32 frameIndex, const ProgramFactory::VkProgramObject& programObj, VkDescriptorSet& outDescriptorSet);
VkResult AcquireDescriptorSet(Uint32 frameIndex,
const ProgramFactory::VkProgramObject& programObj,
VkDescriptorSet& outDescriptorSet);
VkDevice m_device = VK_NULL_HANDLE;
VkBufferManager* m_bufferManager = nullptr;
@@ -88,6 +137,53 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 m_peakDescriptorSetsObserved = 0;
VkTextureManager* m_textureManager = nullptr;
VkSamplerManager* m_samplerManager = nullptr;
mutable SharedPtr<MG_State::GLState::ITextureObject> m_fallbackTexture2D;
// Per-draw scratch buffers for BindProgramUniformBuffers: reused (clear keeps
// capacity) so the descriptor-write path stops allocating on every draw.
Vector<VkWriteDescriptorSet> m_writesScratch;
Vector<VkDescriptorBufferInfo> m_bufferInfosScratch;
Vector<VkDescriptorImageInfo> m_imageInfosScratch;
Vector<VkBufferView> m_texelBufferViewsScratch;
Vector<Uint32> m_dynamicOffsetsScratch;
// Descriptor-set reuse across consecutive draws (see BindProgramUniformBuffers).
// When a draw's resolved descriptor content is byte-identical to the previous
// draw's, reuse the same VkDescriptorSet and skip AcquireDescriptorSet +
// vkUpdateDescriptorSets - only the bind-time dynamic offsets differ. Reset each
// frame in BeginFrame because the frame's descriptor sets are recycled there.
VkDescriptorSet m_lastBoundDescriptorSet = VK_NULL_HANDLE;
Uint64 m_lastDescriptorSignature = 0;
Bool m_hasLastDescriptor = false;
// Per-binding fast path over VkSamplerManager's content-hashed sampler cache, which
// stays the source of truth: its key hashes all sampler+texture state, so two distinct
// sampler objects with identical state still resolve to one VkSampler. This memo only
// skips recomputing that hash. Across a draw batch the bound sampler set is stable, so a
// binding whose sampler (lifetime id + version, bumped on every setter) and texture
// (lifetime id + params version, bumped on the format/border-color setters that feed the
// key) are unchanged recycles the VkSampler it resolved last draw; a param change bumps
// a version and forces a re-resolve. Both objects are keyed by a never-reused monotonic
// lifetime id, so a freed-and-reallocated sampler or texture at the same heap address
// always gets a fresh id and misses (a raw pointer would false-hit that ABA) - so a
// stale guess can only miss and fall through to the hash, never resolve wrong. Still
// reset each frame alongside the descriptor-set cache. Indexed by binding.
struct SamplerResolveMemo {
Uint64 samplerLifetimeId = 0;
Uint64 textureLifetimeId = 0;
VkSampler sampler = VK_NULL_HANDLE;
Uint16 samplerVersion = 0;
Uint16 textureParamsVersion = 0;
Bool forceNearestFiltering = false;
Bool valid = false;
// ResolveSampledImageViewFormat is pure in (image format, numeric domain), but a
// domain mismatch walks a ~184-entry format table. Memo the resolution per binding
// so a reinterpreted sampler pays that scan once, not once per draw.
VkFormat viewFormatSource = VK_FORMAT_UNDEFINED;
SamplerNumericDomain viewFormatDomain = SamplerNumericDomain::Unknown;
VkFormat viewFormat = VK_FORMAT_UNDEFINED;
Bool viewFormatValid = false;
};
mutable Vector<SamplerResolveMemo> m_samplerResolveMemo;
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -29,6 +29,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Stride, sizeof(attr.Stride)));
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Offset, sizeof(attr.Offset)));
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsInteger, sizeof(attr.IsInteger)));
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsBgra, sizeof(attr.IsBgra)));
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Divisor, sizeof(attr.Divisor)));
const SizeT bufferKey = reinterpret_cast<SizeT>(attr.Buffer.get());
@@ -38,70 +39,127 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return XXH64_digest(m_hashState);
}
VertexInputStateFactory::HashType VertexInputStateFactory::GetOrComputeHash(
const MG_State::GLState::VertexArrayObject& vao) const {
HashType hash = 0;
if (!vao.GetBackendHashMemo(hash)) {
hash = ComputeHash(vao);
vao.SetBackendHashMemo(hash);
}
return hash;
}
const VertexInputStateFactory::BackendVertexInputState& VertexInputStateFactory::GetOrCreateVertexInputState(
const MG_State::GLState::VertexArrayObject& vao) {
const HashType hash = ComputeHash(vao);
return GetOrCreateVertexInputState(vao, GetOrComputeHash(vao));
}
const VertexInputStateFactory::BackendVertexInputState& VertexInputStateFactory::GetOrCreateVertexInputState(
const MG_State::GLState::VertexArrayObject& vao, HashType hash) {
auto it = m_cache.find(hash);
if (it != m_cache.end()) {
return it->second;
}
VertexInputStateBuilder builder;
UnorderedMap<SizeT, Uint32> bindingByBufferKey;
UnorderedMap<SizeT, Uint32> strideByBufferKey;
UnorderedMap<SizeT, VkVertexInputRate> inputRateByBufferKey;
Vector<SizeT> bindingBufferKeys;
Vector<SizeT> bindingBaseOffsets;
Vector<Uint32> bindingAttributeLocations;
Vector<Bool> bindingUsesClientMemory;
Vector<VertexStreamConversion> bindingConversions;
Uint32 unsupportedAttribMask = 0;
for (Uint32 location = 0; location < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS; ++location) {
const auto& attr = vao.GetAttribute(location);
if (!attr.Enabled || !attr.Buffer) {
if (!attr.Enabled) {
continue;
}
const auto vkFormat = ToVkVertexFormat(attr.Type, attr.Size, attr.Normalized, attr.IsInteger);
if (vkFormat == VK_FORMAT_UNDEFINED) {
MGLOG_D("Skipping unsupported vertex attribute layout (location=%u, type=%s, size=%d)",
const VkFormat sourceVkFormat =
ToVkVertexFormat(attr.Type, attr.Size, attr.Normalized, attr.IsInteger, attr.IsBgra);
if (sourceVkFormat == VK_FORMAT_UNDEFINED) {
MGLOG_E("Unsupported vertex attribute layout (location=%u, type=%s, size=%d): the array is "
"enabled but cannot be mapped to a VkFormat",
location, MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size);
unsupportedAttribMask |= (1u << location);
continue;
}
const SizeT componentSize = GetComponentSize(attr.Type);
if (componentSize == 0) {
MGLOG_D("Skipping vertex attribute with unknown component size (location=%u, type=%s)",
VkFormat vkFormat = sourceVkFormat;
VertexStreamConversion conversion = VertexStreamConversion::None;
if (!SupportsVertexBufferFormat(vkFormat)) {
if (IsScaledIntegerVertexFormat(vkFormat)) {
const VkFormat fallbackFormat = ToFloat32VertexFormat(attr.Size);
if (fallbackFormat != VK_FORMAT_UNDEFINED && SupportsVertexBufferFormat(fallbackFormat)) {
vkFormat = fallbackFormat;
conversion = VertexStreamConversion::ScaledIntegerToFloat32;
MGLOG_W("Vertex attribute location=%u format=%d lacks "
"VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT; using float32 stream format=%d "
"(type=%s size=%d normalized=%s integer=%s)",
location, static_cast<Int>(sourceVkFormat), static_cast<Int>(vkFormat),
MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size,
attr.Normalized ? "true" : "false", attr.IsInteger ? "true" : "false");
}
}
if (conversion == VertexStreamConversion::None) {
MGLOG_E("Unsupported Vulkan vertex format (location=%u, format=%d, type=%s, size=%d): "
"VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT is unavailable and no semantic fallback exists",
location, static_cast<Int>(sourceVkFormat),
MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size);
unsupportedAttribMask |= (1u << location);
continue;
}
}
const SizeT attribByteSize = GetAttributeByteSize(attr.Type, attr.Size, attr.IsBgra);
if (attribByteSize == 0) {
MGLOG_E("Vertex attribute with unknown component size (location=%u, type=%s): the array is "
"enabled but cannot be sized",
location, MG_Util::ConvertDataTypeToString(attr.Type).c_str());
unsupportedAttribMask |= (1u << location);
continue;
}
const Uint32 stride = attr.Stride > 0
? static_cast<Uint32>(attr.Stride)
: static_cast<Uint32>(componentSize * static_cast<SizeT>(attr.Size));
const Uint32 sourceStride =
attr.Stride > 0 ? static_cast<Uint32>(attr.Stride) : static_cast<Uint32>(attribByteSize);
const Bool packedAttribute = attr.Type == DataType::Int2101010Rev ||
attr.Type == DataType::Uint2101010Rev;
const SizeT requiredAlignment = packedAttribute ? attribByteSize : GetComponentSize(attr.Type);
// For a client-memory array attr.Offset holds the raw client pointer, and the
// draw path re-uploads the data to a 16-aligned transient slice with attribute
// offset 0, so only the stride can violate Vulkan's fetch alignment there.
const Bool clientMemoryAttribute = attr.Buffer == nullptr;
if (conversion == VertexStreamConversion::None && requiredAlignment > 1 &&
((sourceStride % requiredAlignment) != 0 ||
(!clientMemoryAttribute && (attr.Offset % requiredAlignment) != 0))) {
// GL accepts arbitrary byte strides and offsets. Core Vulkan vertex fetches do not
// unless VK_EXT_legacy_vertex_attributes is available, so deinterleave this one
// attribute into a tightly packed transient stream without changing its format.
conversion = VertexStreamConversion::Repack;
MGLOG_W("Vertex attribute location=%u uses Vulkan-incompatible alignment "
"(offset=%zu stride=%u required=%zu); using a tightly packed stream",
location, attr.Offset, sourceStride, requiredAlignment);
}
Uint32 stride = sourceStride;
if (conversion == VertexStreamConversion::Repack) {
stride = static_cast<Uint32>(attribByteSize);
} else if (conversion == VertexStreamConversion::ScaledIntegerToFloat32) {
stride = static_cast<Uint32>(attr.Size * static_cast<Int>(sizeof(Float)));
}
const VkVertexInputRate inputRate =
(attr.Divisor == 0) ? VK_VERTEX_INPUT_RATE_VERTEX : VK_VERTEX_INPUT_RATE_INSTANCE;
const SizeT bufferKey = reinterpret_cast<SizeT>(attr.Buffer.get());
Uint32 binding = 0;
auto itBinding = bindingByBufferKey.find(bufferKey);
if (itBinding == bindingByBufferKey.end()) {
binding = static_cast<Uint32>(bindingByBufferKey.size());
bindingByBufferKey.emplace(bufferKey, binding);
strideByBufferKey.emplace(bufferKey, stride);
inputRateByBufferKey.emplace(bufferKey, inputRate);
bindingBufferKeys.push_back(bufferKey);
builder.AddBinding(binding, stride, inputRate);
} else {
binding = itBinding->second;
if (strideByBufferKey[bufferKey] != stride) {
MGLOG_D("Skipping vertex attribute at location %u: stride mismatch (%u vs %u) on same buffer",
location, stride, strideByBufferKey[bufferKey]);
continue;
}
if (inputRateByBufferKey[bufferKey] != inputRate) {
MGLOG_D("Skipping vertex attribute at location %u: input-rate mismatch on same buffer", location);
continue;
}
}
builder.AddAttribute(location, binding, vkFormat, static_cast<Uint32>(attr.Offset));
const Uint32 binding = static_cast<Uint32>(bindingBufferKeys.size());
bindingBufferKeys.push_back(bufferKey);
bindingBaseOffsets.push_back(attr.Buffer ? attr.Offset : 0);
bindingAttributeLocations.push_back(location);
bindingUsesClientMemory.push_back(attr.Buffer == nullptr);
bindingConversions.push_back(conversion);
builder.AddBinding(binding, stride, inputRate);
builder.AddAttribute(location, binding, vkFormat, 0);
}
const auto& state = builder.Build();
@@ -111,14 +169,43 @@ namespace MobileGL::MG_Backend::DirectVulkan {
entry.bindings = builder.GetBindings();
entry.attributes = builder.GetAttributes();
entry.bindingBufferKeys = std::move(bindingBufferKeys);
entry.bindingBaseOffsets = std::move(bindingBaseOffsets);
entry.bindingAttributeLocations = std::move(bindingAttributeLocations);
entry.bindingUsesClientMemory = std::move(bindingUsesClientMemory);
entry.bindingConversions = std::move(bindingConversions);
entry.unsupportedAttribMask = unsupportedAttribMask;
entry.state = state;
entry.state.pVertexBindingDescriptions = entry.bindings.empty() ? nullptr : entry.bindings.data();
entry.state.pVertexAttributeDescriptions = entry.attributes.empty() ? nullptr : entry.attributes.data();
return entry;
}
VkFormat VertexInputStateFactory::ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger) {
VkFormat VertexInputStateFactory::ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger,
Bool isBgra) {
if (isBgra) {
// GL_BGRA: four reversed-order components, always normalized (enforced at validation), only
// legal with GL_UNSIGNED_BYTE or a 2_10_10_10 type. The reversed VkFormats put the
// components back into R,G,B,A order for the shader.
switch (type) {
case DataType::Uint8:
return VK_FORMAT_B8G8R8A8_UNORM;
case DataType::Uint2101010Rev:
return VK_FORMAT_A2R10G10B10_UNORM_PACK32;
case DataType::Int2101010Rev:
return VK_FORMAT_A2R10G10B10_SNORM_PACK32;
default:
return VK_FORMAT_UNDEFINED;
}
}
switch (type) {
case DataType::Uint2101010Rev:
// Packed 2_10_10_10 travels the float-normalizing path only; size is always 4. SNORM/UNORM
// normalize, SSCALED/USCALED cast the packed field to float.
if (isInteger || size != 4) return VK_FORMAT_UNDEFINED;
return normalized ? VK_FORMAT_A2B10G10R10_UNORM_PACK32 : VK_FORMAT_A2B10G10R10_USCALED_PACK32;
case DataType::Int2101010Rev:
if (isInteger || size != 4) return VK_FORMAT_UNDEFINED;
return normalized ? VK_FORMAT_A2B10G10R10_SNORM_PACK32 : VK_FORMAT_A2B10G10R10_SSCALED_PACK32;
case DataType::Float32:
switch (size) {
case 1: return VK_FORMAT_R32_SFLOAT;
@@ -127,6 +214,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
case 4: return VK_FORMAT_R32G32B32A32_SFLOAT;
default: return VK_FORMAT_UNDEFINED;
}
case DataType::Float16:
// GL_HALF_FLOAT is a floating-point array type: it is never an integer attribute, and
// GL_TRUE for `normalized` is ignored for float types rather than selecting a *NORM format.
if (isInteger) return VK_FORMAT_UNDEFINED;
switch (size) {
case 1: return VK_FORMAT_R16_SFLOAT;
case 2: return VK_FORMAT_R16G16_SFLOAT;
case 3: return VK_FORMAT_R16G16B16_SFLOAT;
case 4: return VK_FORMAT_R16G16B16A16_SFLOAT;
default: return VK_FORMAT_UNDEFINED;
}
case DataType::Int32:
if (!isInteger || normalized) return VK_FORMAT_UNDEFINED;
switch (size) {
@@ -148,8 +246,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
case DataType::Int16:
switch (size) {
case 1:
return isInteger ? VK_FORMAT_R16_SINT
: (normalized ? VK_FORMAT_R16_SNORM : VK_FORMAT_R16_SSCALED);
return isInteger ? VK_FORMAT_R16_SINT : (normalized ? VK_FORMAT_R16_SNORM : VK_FORMAT_R16_SSCALED);
case 2:
return isInteger ? VK_FORMAT_R16G16_SINT
: (normalized ? VK_FORMAT_R16G16_SNORM : VK_FORMAT_R16G16_SSCALED);
@@ -164,8 +261,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
case DataType::Uint16:
switch (size) {
case 1:
return isInteger ? VK_FORMAT_R16_UINT
: (normalized ? VK_FORMAT_R16_UNORM : VK_FORMAT_R16_USCALED);
return isInteger ? VK_FORMAT_R16_UINT : (normalized ? VK_FORMAT_R16_UNORM : VK_FORMAT_R16_USCALED);
case 2:
return isInteger ? VK_FORMAT_R16G16_UINT
: (normalized ? VK_FORMAT_R16G16_UNORM : VK_FORMAT_R16G16_USCALED);
@@ -180,8 +276,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
case DataType::Int8:
switch (size) {
case 1:
return isInteger ? VK_FORMAT_R8_SINT
: (normalized ? VK_FORMAT_R8_SNORM : VK_FORMAT_R8_SSCALED);
return isInteger ? VK_FORMAT_R8_SINT : (normalized ? VK_FORMAT_R8_SNORM : VK_FORMAT_R8_SSCALED);
case 2:
return isInteger ? VK_FORMAT_R8G8_SINT
: (normalized ? VK_FORMAT_R8G8_SNORM : VK_FORMAT_R8G8_SSCALED);
@@ -196,8 +291,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
case DataType::Uint8:
switch (size) {
case 1:
return isInteger ? VK_FORMAT_R8_UINT
: (normalized ? VK_FORMAT_R8_UNORM : VK_FORMAT_R8_USCALED);
return isInteger ? VK_FORMAT_R8_UINT : (normalized ? VK_FORMAT_R8_UNORM : VK_FORMAT_R8_USCALED);
case 2:
return isInteger ? VK_FORMAT_R8G8_UINT
: (normalized ? VK_FORMAT_R8G8_UNORM : VK_FORMAT_R8G8_USCALED);
@@ -234,4 +328,57 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return 0;
}
}
SizeT VertexInputStateFactory::GetAttributeByteSize(DataType type, Int size, Bool isBgra) {
// The packed 2_10_10_10 types are a single 32-bit word for all 4 components; GL_BGRA is always
// 4 components (GL_UNSIGNED_BYTE x4 = 4 bytes, or a packed word = 4 bytes) -- both are 4 bytes.
if (type == DataType::Int2101010Rev || type == DataType::Uint2101010Rev || isBgra) {
return 4;
}
const SizeT componentSize = GetComponentSize(type);
return componentSize == 0 ? 0 : componentSize * static_cast<SizeT>(size);
}
Bool VertexInputStateFactory::IsScaledIntegerVertexFormat(VkFormat format) {
switch (format) {
case VK_FORMAT_R8_USCALED:
case VK_FORMAT_R8_SSCALED:
case VK_FORMAT_R8G8_USCALED:
case VK_FORMAT_R8G8_SSCALED:
case VK_FORMAT_R8G8B8_USCALED:
case VK_FORMAT_R8G8B8_SSCALED:
case VK_FORMAT_R8G8B8A8_USCALED:
case VK_FORMAT_R8G8B8A8_SSCALED:
case VK_FORMAT_R16_USCALED:
case VK_FORMAT_R16_SSCALED:
case VK_FORMAT_R16G16_USCALED:
case VK_FORMAT_R16G16_SSCALED:
case VK_FORMAT_R16G16B16_USCALED:
case VK_FORMAT_R16G16B16_SSCALED:
case VK_FORMAT_R16G16B16A16_USCALED:
case VK_FORMAT_R16G16B16A16_SSCALED:
return true;
default:
return false;
}
}
VkFormat VertexInputStateFactory::ToFloat32VertexFormat(Int componentCount) {
switch (componentCount) {
case 1: return VK_FORMAT_R32_SFLOAT;
case 2: return VK_FORMAT_R32G32_SFLOAT;
case 3: return VK_FORMAT_R32G32B32_SFLOAT;
case 4: return VK_FORMAT_R32G32B32A32_SFLOAT;
default: return VK_FORMAT_UNDEFINED;
}
}
Bool VertexInputStateFactory::SupportsVertexBufferFormat(VkFormat format) const {
if (m_physicalDevice == VK_NULL_HANDLE || format == VK_FORMAT_UNDEFINED) {
return false;
}
VkFormatProperties properties{};
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &properties);
return (properties.bufferFeatures & VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT) != 0;
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -19,29 +19,56 @@ namespace MobileGL::MG_Backend::DirectVulkan {
public:
using HashType = Uint64;
enum class VertexStreamConversion : Uint8 {
None = 0,
Repack,
ScaledIntegerToFloat32,
};
struct BackendVertexInputState {
HashType hash = 0;
Vector<VkVertexInputBindingDescription> bindings;
Vector<VkVertexInputAttributeDescription> attributes;
Vector<SizeT> bindingBufferKeys;
Vector<SizeT> bindingBaseOffsets;
Vector<Uint32> bindingAttributeLocations;
Vector<Bool> bindingUsesClientMemory;
Vector<VertexStreamConversion> bindingConversions;
// Locations whose array is ENABLED but whose GL format has no VkFormat mapping. They are
// absent from `attributes`, so without this mask the draw path cannot tell them apart from
// a genuinely disabled array and would silently feed the shader the current attribute value.
Uint32 unsupportedAttribMask = 0;
VkPipelineVertexInputStateCreateInfo state{
VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO
};
};
explicit VertexInputStateFactory(const VulkanRendererConfig& config):
m_config(config) {}
VertexInputStateFactory(const VulkanRendererConfig& config, VkPhysicalDevice physicalDevice):
m_config(config), m_physicalDevice(physicalDevice) {}
~VertexInputStateFactory() = default;
VertexInputStateFactory(const VertexInputStateFactory&) = delete;
HashType ComputeHash(const MG_State::GLState::VertexArrayObject& vao) const;
// Memoized ComputeHash: reuses the VAO's cached hash while its config version
// is unchanged. Use this on per-draw paths.
HashType GetOrComputeHash(const MG_State::GLState::VertexArrayObject& vao) const;
const BackendVertexInputState& GetOrCreateVertexInputState(
const MG_State::GLState::VertexArrayObject& vao, HashType hash);
const BackendVertexInputState& GetOrCreateVertexInputState(const MG_State::GLState::VertexArrayObject& vao);
static SizeT GetComponentSize(DataType type);
// Tightly-packed byte size of one vertex element for this attribute: componentSize * size for
// normal types, and 4 (one packed word) for the 2_10_10_10 types and GL_BGRA. Returns 0 for
// an unknown/unsupported type.
static SizeT GetAttributeByteSize(DataType type, Int size, Bool isBgra);
private:
static VkFormat ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger);
static SizeT GetComponentSize(DataType type);
static VkFormat ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger, Bool isBgra = false);
static Bool IsScaledIntegerVertexFormat(VkFormat format);
static VkFormat ToFloat32VertexFormat(Int componentCount);
Bool SupportsVertexBufferFormat(VkFormat format) const;
const VulkanRendererConfig& m_config;
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
UnorderedMap<HashType, BackendVertexInputState> m_cache;
static inline XXH64_state_t* m_hashState = XXH64_createState();
};
@@ -10,9 +10,73 @@
namespace MobileGL::MG_Backend::DirectVulkan {
namespace {
constexpr Uint32 kResidentBufferGCInterval = 60;
constexpr VmaAllocationCreateFlags kResidentBufferAllocationFlags =
VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT;
constexpr SizeT kLiveResourcePruneThreshold = 256;
// A zero-copy persistent buffer is created once and never recreated (the app holds
// its mapped pointer), and may be bound to any role, so it carries every usage.
// TRANSFER_DST is added by CreateResidentStorage.
constexpr VkBufferUsageFlags kPersistentBackedUsage =
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT |
VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT |
VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT |
VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
// The app writes into the persistent map with no explicit flush, so its memory must
// be host-coherent (Adreno host-visible memory is; requiring it keeps us portable).
constexpr VkMemoryPropertyFlags kPersistentBackedRequiredFlags =
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
using MG_State::GLState::BackendBufferResource;
using MG_State::GLState::BufferBackendOps;
using MG_State::GLState::BufferObject;
// The manager owned by the active VulkanRenderer; immediate ops route here.
VkBufferManager* g_activeBufferManager = nullptr;
void Ops_Respecify(BufferObject& bufferObject) {
if (g_activeBufferManager) {
g_activeBufferManager->OnRespecify(bufferObject);
}
}
void Ops_SubData(BufferObject& bufferObject, SizeT offset, SizeT size) {
if (g_activeBufferManager) {
g_activeBufferManager->OnSubData(bufferObject, offset, size);
}
}
void Ops_FlushMappedRange(BufferObject& bufferObject, Range1D range,
Flags<BufferMappingAccessBit> appAccess) {
if (g_activeBufferManager) {
g_activeBufferManager->OnFlushMappedRange(bufferObject, range, appAccess);
}
}
void* Ops_AcquirePersistentMap(BufferObject& bufferObject) {
if (g_activeBufferManager) {
return g_activeBufferManager->AcquirePersistentMap(bufferObject);
}
return nullptr;
}
void Ops_OnDestroy(SharedPtr<BackendBufferResource>&& resource) {
if (g_activeBufferManager) {
g_activeBufferManager->OnResourceDestroyed(std::move(resource));
}
// No active manager: the device/allocator is gone or going away and
// Shutdown() already destroyed the storage; dropping the handle here
// must not touch Vulkan. VkBufferResource's dtor destroys via VMA only
// when the allocation is still valid, which Shutdown() cleared.
}
const BufferBackendOps g_vulkanBufferBackendOps = {
.Respecify = Ops_Respecify,
.SubData = Ops_SubData,
.FlushMappedRange = Ops_FlushMappedRange,
.OnDestroy = Ops_OnDestroy,
.AcquirePersistentMap = Ops_AcquirePersistentMap,
};
} // namespace
Bool VkBufferManager::Initialize(const VkBufferManagerInitInfo& initInfo) {
@@ -22,40 +86,93 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MOBILEGL_ASSERT(initInfo.frameCount > 0, "VkBufferManager::Initialize requires non-zero frame count");
m_initInfo = initInfo;
m_deferredResidentReleases.resize(initInfo.frameCount);
m_deferredBufferReleases.resize(initInfo.frameCount);
m_deferredResourceReleases.resize(initInfo.frameCount);
m_currentFrameIndex = 0;
return InitializeTransientArenas();
m_frameSerial = 1;
m_completedSerialFloor = 0;
if (!InitializeTransientArenas()) {
return false;
}
g_activeBufferManager = this;
MG_State::GLState::SetBufferBackendOps(&g_vulkanBufferBackendOps);
return true;
}
void VkBufferManager::Shutdown() {
if (g_activeBufferManager == this) {
g_activeBufferManager = nullptr;
if (MG_State::GLState::GetBufferBackendOps() == &g_vulkanBufferBackendOps) {
MG_State::GLState::SetBufferBackendOps(nullptr);
}
}
m_transientUploadArena.Shutdown();
DestroyResidentBuffers();
DestroyDeferredResidentReleases();
DestroyAllDeferredReleases();
ReleaseAllLiveResources();
m_copyProvider = nullptr;
m_initInfo = {};
m_currentFrameIndex = 0;
m_residentGcTick = 0;
m_frameSerial = 1;
m_completedSerialFloor = 0;
}
Bool VkBufferManager::RecreateTransientArenas(Uint32 frameCount) {
MOBILEGL_ASSERT(m_initInfo.allocator != nullptr, "VkBufferManager::RecreateTransientArenas requires initialized manager");
MOBILEGL_ASSERT(m_initInfo.allocator != nullptr,
"VkBufferManager::RecreateTransientArenas requires initialized manager");
MOBILEGL_ASSERT(frameCount > 0, "VkBufferManager::RecreateTransientArenas requires non-zero frame count");
// Callers guarantee the device is idle around arena recreation.
NotifyDeviceIdle();
m_transientUploadArena.Shutdown();
m_initInfo.frameCount = frameCount;
DestroyDeferredResidentReleases();
m_deferredResidentReleases.resize(frameCount);
DestroyAllDeferredReleases();
m_deferredBufferReleases.resize(frameCount);
m_deferredResourceReleases.resize(frameCount);
m_currentFrameIndex = 0;
return InitializeTransientArenas();
}
void VkBufferManager::BeginFrame(Uint32 frameIndex) {
MOBILEGL_ASSERT(frameIndex < m_deferredResidentReleases.size(),
MOBILEGL_ASSERT(frameIndex < m_deferredBufferReleases.size(),
"VkBufferManager::BeginFrame frame index out of range");
m_currentFrameIndex = frameIndex;
CollectDeferredResidentReleases(frameIndex);
++m_frameSerial;
CollectDeferredReleases(frameIndex);
m_transientUploadArena.BeginFrame(frameIndex);
}
void VkBufferManager::NotifyDeviceIdle() {
// Everything submitted so far has completed. Work recorded for the
// current frame has not been submitted yet, so the current serial
// remains busy.
if (m_frameSerial > 0) {
m_completedSerialFloor = m_frameSerial - 1;
}
}
void VkBufferManager::NotifyFrameSerialComplete(Uint64 serial) {
// The current serial's work is still being recorded; a completion
// report for it (or beyond) can only come from a stale caller.
if (serial >= m_frameSerial) {
return;
}
m_completedSerialFloor = std::max(m_completedSerialFloor, serial);
}
void VkBufferManager::SetCopyCommandProvider(IBufferCopyCommandProvider* provider) {
m_copyProvider = provider;
}
Uint64 VkBufferManager::GetCompletedSerial() const {
const Uint64 frameCount = m_initInfo.frameCount > 0 ? m_initInfo.frameCount : 1;
const Uint64 completed = m_frameSerial > frameCount ? m_frameSerial - frameCount : 0;
return std::max(completed, m_completedSerialFloor);
}
Bool VkBufferManager::IsResourceBusy(const VkBufferResource& resource) const {
return resource.lastUseSerial > GetCompletedSerial();
}
Bool VkBufferManager::UploadTransient(BufferKind kind, Uint32 frameIndex, const void* data,
VkDeviceSize size, VkDeviceSize alignment, BufferSlice& outSlice) {
(void)kind;
@@ -67,7 +184,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
.allocator = m_initInfo.allocator,
.frameCount = m_initInfo.frameCount,
.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT |
VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT |
VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
.memoryUsage = m_initInfo.transientMemoryUsage,
.allocationFlags = m_initInfo.transientAllocationFlags,
.minBufferSize = m_initInfo.minUploadBytes,
@@ -75,115 +193,394 @@ namespace MobileGL::MG_Backend::DirectVulkan {
});
}
Bool VkBufferManager::SyncResidentBuffer(BufferKind kind,
const SharedPtr<MG_State::GLState::BufferObject>& bufferObject,
BufferSlice& outSlice) {
const VkBufferUsageFlags requiredUsage = GetVkBufferUsage(kind);
MOBILEGL_ASSERT(requiredUsage != 0,
"VkBufferManager::SyncResidentBuffer only supports resident vertex/index buffers");
MOBILEGL_ASSERT(bufferObject != nullptr, "VkBufferManager::SyncResidentBuffer requires valid buffer object");
CollectResidentGarbageIfNeeded();
VkBufferResource* VkBufferManager::ResourceOf(MG_State::GLState::BufferObject& bufferObject) {
return static_cast<VkBufferResource*>(bufferObject.GetBackendResource().get());
}
const auto* bufferData = bufferObject->GetDataReadOnly().get();
MOBILEGL_ASSERT(bufferData != nullptr, "VkBufferManager::SyncResidentBuffer requires frontend buffer data");
VkBufferResource* VkBufferManager::GetOrCreateResource(
const SharedPtr<MG_State::GLState::BufferObject>& bufferObject) {
// 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 static_cast<VkBufferResource*>(existing.get());
}
auto resource = MakeShared<VkBufferResource>();
VkBufferResource* raw = resource.get();
bufferObject->SetBackendResource(resource);
TrackLiveResource(resource);
return raw;
}
const VkDeviceSize bufferSize = static_cast<VkDeviceSize>(bufferObject->GetSize());
if (bufferSize == 0) {
MGLOG_E("VkBufferManager::SyncResidentBuffer failed: buffer size is zero");
void VkBufferManager::TrackLiveResource(const SharedPtr<VkBufferResource>& resource) {
if (m_liveResources.size() >= kLiveResourcePruneThreshold) {
std::erase_if(m_liveResources, [](const WeakPtr<VkBufferResource>& weak) { return weak.expired(); });
}
m_liveResources.push_back(resource);
}
void VkBufferManager::ReleaseAllLiveResources() {
for (auto& weak : m_liveResources) {
if (auto resource = weak.lock()) {
resource->buffer.Destroy();
resource->storageSize = 0;
resource->usageFlags = 0;
resource->lastUseSerial = 0;
resource->pendingFullUpload = true;
resource->transientSlice = {};
resource->transientFrameSerial = 0;
}
}
m_liveResources.clear();
}
Bool VkBufferManager::CreateResidentStorage(VkBufferResource& resource, VkDeviceSize size,
VkBufferUsageFlags usage, VkMemoryPropertyFlags requiredFlags) {
// Staged range copies write resident storage with vkCmdCopyBuffer.
usage |= VK_BUFFER_USAGE_TRANSFER_DST_BIT;
const Bool created = resource.buffer.Create({
.allocator = m_initInfo.allocator,
.size = size,
.usage = usage,
.memoryUsage = VMA_MEMORY_USAGE_AUTO,
.allocationFlags = kResidentBufferAllocationFlags,
.requiredFlags = requiredFlags,
});
if (!created || resource.buffer.Map() == nullptr) {
MGLOG_E("VkBufferManager::CreateResidentStorage failed (size=%llu)",
static_cast<unsigned long long>(size));
resource.buffer.Destroy();
resource.storageSize = 0;
resource.usageFlags = 0;
return false;
}
auto& entry = m_residentBuffers[bufferObject.get()];
entry.aliveRef = bufferObject;
const auto changeBits = bufferObject->GetChangeBits();
const Bool needsRecreate = !entry.buffer.IsValid() || entry.size != bufferSize ||
((entry.usage & requiredUsage) != requiredUsage) ||
(changeBits & BufferChangeBits::PreferReallocationBit);
if (needsRecreate) {
const VkBufferUsageFlags recreatedUsage = entry.usage | requiredUsage;
DeferResidentRelease(std::move(entry.buffer));
const Bool created = entry.buffer.Create({
.allocator = m_initInfo.allocator,
.size = bufferSize,
.usage = recreatedUsage,
.memoryUsage = VMA_MEMORY_USAGE_AUTO,
.allocationFlags = kResidentBufferAllocationFlags,
});
if (!created || entry.buffer.Map() == nullptr) {
MGLOG_E("VkBufferManager::SyncResidentBuffer failed: unable to create resident buffer");
entry.buffer.Destroy();
entry.size = 0;
entry.usage = 0;
return false;
}
if (!entry.buffer.Upload(bufferData->data(), bufferSize, 0)) {
MGLOG_E("VkBufferManager::SyncResidentBuffer failed: initial upload failed");
entry.buffer.Destroy();
entry.size = 0;
entry.usage = 0;
return false;
}
entry.size = bufferSize;
entry.usage = recreatedUsage;
bufferObject->ClearDirty();
outSlice = entry.buffer.GetSlice(0, bufferSize);
return true;
}
if (changeBits & BufferChangeBits::DirtyBit) {
const auto& dirtyRanges = bufferObject->GetDirtyRanges();
for (const auto& range : dirtyRanges) {
const VkDeviceSize rangeOffset = static_cast<VkDeviceSize>(range.start);
const VkDeviceSize rangeSize = static_cast<VkDeviceSize>(range.end - range.start);
if (rangeSize == 0) {
continue;
}
if (!entry.buffer.Upload(bufferData->data() + range.start, rangeSize, rangeOffset)) {
MGLOG_E("VkBufferManager::SyncResidentBuffer failed: dirty range upload failed");
return false;
}
}
bufferObject->ClearDirty();
}
outSlice = entry.buffer.GetSlice(0, bufferSize);
resource.storageSize = size;
resource.usageFlags = usage;
return true;
}
void VkBufferManager::DowngradeResidentBufferToTransient(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject) {
if (bufferObject == nullptr) {
return;
Bool VkBufferManager::SwapStorageAndUploadAll(VkBufferResource& resource,
MG_State::GLState::BufferObject& bufferObject) {
const VkDeviceSize size = static_cast<VkDeviceSize>(bufferObject.GetSize());
const VkBufferUsageFlags usage = resource.usageFlags;
DeferRelease(std::move(resource.buffer));
if (!CreateResidentStorage(resource, size, usage)) {
resource.pendingFullUpload = true;
return false;
}
auto it = m_residentBuffers.find(bufferObject.get());
if (it == m_residentBuffers.end()) {
return;
if (!resource.buffer.Upload(bufferObject.MappedData(), size, 0)) {
MGLOG_E("VkBufferManager::SwapStorageAndUploadAll: upload failed");
resource.pendingFullUpload = true;
return false;
}
DeferResidentRelease(std::move(it->second.buffer));
m_residentBuffers.erase(it);
resource.pendingFullUpload = false;
return true;
}
void VkBufferManager::DeferResidentRelease(VkBufferObject&& buffer) {
Bool VkBufferManager::StagedRangeCopy(VkBufferResource& resource, MG_State::GLState::BufferObject& bufferObject,
SizeT offset, SizeT size) {
if (!m_copyProvider) {
return false;
}
BufferSlice staging{};
if (!m_transientUploadArena.Upload(m_currentFrameIndex, bufferObject.MappedData() + offset,
static_cast<VkDeviceSize>(size), 16, staging)) {
return false;
}
VkCommandBuffer commandBuffer = m_copyProvider->AcquireBufferCopyCommandBuffer();
if (commandBuffer == VK_NULL_HANDLE) {
return false;
}
// Order the copy after every prior read/write of this buffer, both from
// in-flight frames (submission order) and from commands already recorded
// in this frame's command buffer.
VkMemoryBarrier beforeBarrier{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
beforeBarrier.srcAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
beforeBarrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1,
&beforeBarrier, 0, nullptr, 0, nullptr);
VkBufferCopy region{};
region.srcOffset = staging.offset;
region.dstOffset = static_cast<VkDeviceSize>(offset);
region.size = static_cast<VkDeviceSize>(size);
vkCmdCopyBuffer(commandBuffer, staging.buffer, resource.buffer.GetHandle(), 1, &region);
VkMemoryBarrier afterBarrier{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
afterBarrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
afterBarrier.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 1,
&afterBarrier, 0, nullptr, 0, nullptr);
resource.lastUseSerial = m_frameSerial;
return true;
}
void VkBufferManager::OnRespecify(MG_State::GLState::BufferObject& bufferObject) {
auto* resource = ResourceOf(bufferObject);
if (!resource) {
return; // lazy: AcquireResidentSlice performs a full upload on creation
}
// Any cached streaming slice refers to the previous contents.
resource->transientFrameSerial = 0;
if (!resource->buffer.IsValid()) {
return; // streaming-only resource: shadow + serial are enough
}
const VkDeviceSize size = static_cast<VkDeviceSize>(bufferObject.GetSize());
if (size == 0) {
DeferRelease(std::move(resource->buffer));
resource->storageSize = 0;
resource->pendingFullUpload = false;
return;
}
if (size != resource->storageSize || IsResourceBusy(*resource)) {
// Conditional orphan: only swap the storage when the old one is
// still referenced by the GPU (or no longer fits).
SwapStorageAndUploadAll(*resource, bufferObject);
return;
}
if (!resource->buffer.Upload(bufferObject.MappedData(), size, 0)) {
MGLOG_E("VkBufferManager::OnRespecify: in-place upload failed");
resource->pendingFullUpload = true;
}
}
void VkBufferManager::OnSubData(MG_State::GLState::BufferObject& bufferObject, SizeT offset, SizeT size) {
auto* resource = ResourceOf(bufferObject);
if (!resource) {
return;
}
resource->transientFrameSerial = 0;
if (!resource->buffer.IsValid() || resource->pendingFullUpload) {
return;
}
if (static_cast<VkDeviceSize>(bufferObject.GetSize()) != resource->storageSize) {
resource->pendingFullUpload = true;
return;
}
if (!IsResourceBusy(*resource)) {
if (!resource->buffer.Upload(bufferObject.MappedData() + offset,
static_cast<VkDeviceSize>(size), static_cast<VkDeviceSize>(offset))) {
MGLOG_E("VkBufferManager::OnSubData: host upload failed");
resource->pendingFullUpload = true;
}
return;
}
// Busy partial write: stage + GPU copy preserves GL ordering within the
// frame and leaves bytes outside the range (possibly GPU-written, e.g.
// SSBO) intact. Fall back to a storage swap if staging is unavailable.
if (!StagedRangeCopy(*resource, bufferObject, offset, size)) {
SwapStorageAndUploadAll(*resource, bufferObject);
}
}
void VkBufferManager::OnFlushMappedRange(MG_State::GLState::BufferObject& bufferObject, Range1D range,
Flags<BufferMappingAccessBit> appAccess) {
auto* resource = ResourceOf(bufferObject);
if (!resource) {
return;
}
resource->transientFrameSerial = 0;
if (!resource->buffer.IsValid() || resource->pendingFullUpload) {
return;
}
if (static_cast<VkDeviceSize>(bufferObject.GetSize()) != resource->storageSize) {
resource->pendingFullUpload = true;
return;
}
const SizeT offset = range.start;
const SizeT size = range.end - range.start;
// GL_MAP_UNSYNCHRONIZED_BIT: the app guarantees it does not overwrite
// data the GPU is still reading; honour it with a direct host write.
if ((appAccess & BufferMappingAccessBit::Unsynchronized) || !IsResourceBusy(*resource)) {
if (!resource->buffer.Upload(bufferObject.MappedData() + offset,
static_cast<VkDeviceSize>(size), static_cast<VkDeviceSize>(offset))) {
MGLOG_E("VkBufferManager::OnFlushMappedRange: host upload failed");
resource->pendingFullUpload = true;
}
return;
}
if (!StagedRangeCopy(*resource, bufferObject, offset, size)) {
SwapStorageAndUploadAll(*resource, bufferObject);
}
}
void VkBufferManager::OnResourceDestroyed(SharedPtr<MG_State::GLState::BackendBufferResource>&& resource) {
if (!resource) {
return;
}
auto vkResource = std::static_pointer_cast<VkBufferResource>(std::move(resource));
if (!vkResource->buffer.IsValid()) {
return;
}
if (m_deferredResourceReleases.empty()) {
vkResource->buffer.Destroy();
return;
}
MOBILEGL_ASSERT(m_currentFrameIndex < m_deferredResourceReleases.size(),
"VkBufferManager::OnResourceDestroyed current frame index out of range");
// Keep the whole resource alive until this frame slot's fence has been
// waited, then the storage is destroyed with it.
m_deferredResourceReleases[m_currentFrameIndex].push_back(std::move(vkResource));
}
void* VkBufferManager::AcquirePersistentMap(MG_State::GLState::BufferObject& bufferObject) {
const VkDeviceSize size = static_cast<VkDeviceSize>(bufferObject.GetSize());
if (size == 0) {
return nullptr;
}
auto resource = std::static_pointer_cast<VkBufferResource>(bufferObject.GetBackendResource());
if (!resource) {
resource = MakeShared<VkBufferResource>();
bufferObject.SetBackendResource(resource);
TrackLiveResource(resource);
}
// Idempotent: an already-backed buffer returns the same mapped base.
if (resource->persistentMapped && resource->buffer.IsValid() && resource->storageSize == size) {
return resource->buffer.GetMappedData();
}
// One-time creation of HOST_VISIBLE + HOST_COHERENT, persistently mapped storage
// carrying every usage (never recreated, so the app's pointer never dangles). Seed
// it from the current shadow - MappedData() is still the shadow here because the
// frontend adopts (and drops) the shadow only after this returns.
DeferRelease(std::move(resource->buffer));
if (!CreateResidentStorage(*resource, size, kPersistentBackedUsage, kPersistentBackedRequiredFlags)) {
resource->persistentMapped = false;
resource->storageSize = 0;
resource->usageFlags = 0;
return nullptr;
}
const Uint8* seed = bufferObject.MappedData();
if (seed != nullptr) {
resource->buffer.Upload(seed, size, 0);
}
resource->persistentMapped = true;
resource->pendingFullUpload = false;
resource->storageSize = size;
resource->lastUseSerial = 0;
return resource->buffer.GetMappedData();
}
Bool VkBufferManager::AcquireResidentSlice(BufferKind kind,
const SharedPtr<MG_State::GLState::BufferObject>& bufferObject,
BufferSlice& outSlice) {
const VkBufferUsageFlags requiredUsage = GetVkBufferUsage(kind);
MOBILEGL_ASSERT(requiredUsage != 0, "VkBufferManager::AcquireResidentSlice unsupported buffer kind");
MOBILEGL_ASSERT(bufferObject != nullptr, "VkBufferManager::AcquireResidentSlice requires valid buffer object");
auto resource = GetOrCreateResource(bufferObject);
bufferObject->SyncPersistentMappedRange();
const VkDeviceSize size = static_cast<VkDeviceSize>(bufferObject->GetSize());
if (size == 0) {
MGLOG_E("VkBufferManager::AcquireResidentSlice failed: buffer size is zero");
return false;
}
// Zero-copy persistent buffers already hold the app's live coherent writes in
// host-visible storage carrying every usage; bind directly, no re-upload/staging.
if (resource->persistentMapped && resource->buffer.IsValid() && resource->storageSize == size) {
resource->lastUseSerial = m_frameSerial;
outSlice = resource->buffer.GetSlice(0, size);
return outSlice.IsValid();
}
const Bool needsRecreate = !resource->buffer.IsValid() || resource->storageSize != size ||
((resource->usageFlags & requiredUsage) != requiredUsage) ||
resource->pendingFullUpload;
if (needsRecreate) {
const VkBufferUsageFlags usage = resource->usageFlags | requiredUsage;
DeferRelease(std::move(resource->buffer));
if (!CreateResidentStorage(*resource, size, usage)) {
return false;
}
if (!resource->buffer.Upload(bufferObject->MappedData(), size, 0)) {
MGLOG_E("VkBufferManager::AcquireResidentSlice failed: initial upload failed");
resource->buffer.Destroy();
resource->storageSize = 0;
resource->usageFlags = 0;
return false;
}
resource->pendingFullUpload = false;
}
resource->lastUseSerial = m_frameSerial;
outSlice = resource->buffer.GetSlice(0, size);
return true;
}
Bool VkBufferManager::AcquireStreamedSlice(BufferKind kind,
const SharedPtr<MG_State::GLState::BufferObject>& bufferObject,
BufferSlice& outSlice) {
(void)kind;
MOBILEGL_ASSERT(bufferObject != nullptr, "VkBufferManager::AcquireStreamedSlice requires valid buffer object");
auto resource = GetOrCreateResource(bufferObject);
bufferObject->SyncPersistentMappedRange();
const VkDeviceSize size = static_cast<VkDeviceSize>(bufferObject->GetSize());
if (size == 0) {
MGLOG_E("VkBufferManager::AcquireStreamedSlice failed: buffer size is zero");
return false;
}
const Uint64 changeSerial = bufferObject->GetChangeSerial();
if (resource->transientFrameSerial == m_frameSerial && resource->transientChangeSerial == changeSerial &&
resource->transientSize == size && resource->transientSlice.IsValid()) {
outSlice = resource->transientSlice;
return true;
}
if (!m_transientUploadArena.Upload(m_currentFrameIndex, bufferObject->MappedData(), size, 16,
outSlice)) {
return false;
}
resource->transientSlice = outSlice;
resource->transientFrameSerial = m_frameSerial;
resource->transientChangeSerial = changeSerial;
resource->transientSize = size;
// Streaming path is authoritative now; release resident storage so we do
// not keep a second, stale copy alive (downgrade).
if (resource->buffer.IsValid()) {
DeferRelease(std::move(resource->buffer));
resource->storageSize = 0;
}
return true;
}
void VkBufferManager::DeferRelease(VkBufferObject&& buffer) {
if (!buffer.IsValid()) {
return;
}
if (m_deferredResidentReleases.empty()) {
if (m_deferredBufferReleases.empty()) {
buffer.Destroy();
return;
}
MOBILEGL_ASSERT(m_currentFrameIndex < m_deferredResidentReleases.size(),
"VkBufferManager::DeferResidentRelease current frame index out of range");
m_deferredResidentReleases[m_currentFrameIndex].push_back(std::move(buffer));
MOBILEGL_ASSERT(m_currentFrameIndex < m_deferredBufferReleases.size(),
"VkBufferManager::DeferRelease current frame index out of range");
m_deferredBufferReleases[m_currentFrameIndex].push_back(std::move(buffer));
}
void VkBufferManager::CollectDeferredResidentReleases(Uint32 frameIndex) {
MOBILEGL_ASSERT(frameIndex < m_deferredResidentReleases.size(),
"VkBufferManager::CollectDeferredResidentReleases frame index out of range");
m_deferredResidentReleases[frameIndex].clear();
void VkBufferManager::CollectDeferredReleases(Uint32 frameIndex) {
MOBILEGL_ASSERT(frameIndex < m_deferredBufferReleases.size(),
"VkBufferManager::CollectDeferredReleases frame index out of range");
m_deferredBufferReleases[frameIndex].clear();
m_deferredResourceReleases[frameIndex].clear();
}
VkBufferUsageFlags VkBufferManager::GetVkBufferUsage(BufferKind kind) {
@@ -196,51 +593,32 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// never need to recreate a buffer after it has already been bound.
return VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT;
case BufferKind::Uniform:
return VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT;
case BufferKind::TextureBuffer:
return VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT;
case BufferKind::ShaderStorage:
return VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT;
case BufferKind::Indirect:
return VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT;
default:
return 0;
}
}
void VkBufferManager::CollectResidentGarbageIfNeeded() {
++m_residentGcTick;
if (m_residentGcTick < kResidentBufferGCInterval) {
return;
}
CollectResidentGarbageNow();
m_residentGcTick = 0;
}
void VkBufferManager::CollectResidentGarbageNow() {
Vector<MG_State::GLState::BufferObject*> staleBuffers;
staleBuffers.reserve(m_residentBuffers.size());
for (const auto& [rawBuffer, entry] : m_residentBuffers) {
if (entry.aliveRef.expired()) {
staleBuffers.push_back(rawBuffer);
void VkBufferManager::DestroyAllDeferredReleases() {
for (auto& releases : m_deferredBufferReleases) {
for (auto& buffer : releases) {
buffer.Destroy();
}
releases.clear();
}
for (const auto* rawBuffer : staleBuffers) {
auto it = m_residentBuffers.find(const_cast<MG_State::GLState::BufferObject*>(rawBuffer));
if (it == m_residentBuffers.end()) {
continue;
m_deferredBufferReleases.clear();
for (auto& releases : m_deferredResourceReleases) {
for (auto& resource : releases) {
resource->buffer.Destroy();
}
DeferResidentRelease(std::move(it->second.buffer));
m_residentBuffers.erase(it);
releases.clear();
}
}
void VkBufferManager::DestroyDeferredResidentReleases() {
for (auto& deferredReleases : m_deferredResidentReleases) {
deferredReleases.clear();
}
m_deferredResidentReleases.clear();
}
void VkBufferManager::DestroyResidentBuffers() {
for (auto& [_, entry] : m_residentBuffers) {
entry.buffer.Destroy();
}
m_residentBuffers.clear();
m_deferredResourceReleases.clear();
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -19,6 +19,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Vertex,
Index,
Uniform,
TextureBuffer,
ShaderStorage,
Indirect,
};
struct VkBufferManagerInitInfo {
@@ -30,6 +33,42 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool transientPersistentMapping = false;
};
// The DirectVulkan storage behind one frontend buffer (pipe_resource analogue).
// Owned (refcounted) by the frontend BufferObject; the manager holds only weak
// references (for shutdown) plus strong references on deferred-release lists.
class VkBufferResource : public MG_State::GLState::BackendBufferResource {
public:
~VkBufferResource() override = default;
// Resident storage (may be invalid for streaming-only buffers).
VkBufferObject buffer;
VkDeviceSize storageSize = 0;
VkBufferUsageFlags usageFlags = 0;
// Frame serial of the last GPU reference; drives busy tracking.
Uint64 lastUseSerial = 0;
// Set when an immediate op could not be applied; forces a full re-upload
// on the next AcquireResidentSlice.
Bool pendingFullUpload = false;
// Backs a zero-copy coherent persistent map (PipeResource GPU residency): the
// buffer is HOST_VISIBLE+COHERENT, persistently mapped, carries every usage and is
// never orphaned or recreated. Draw-time acquire binds it directly, no re-upload.
Bool persistentMapped = false;
// Cached transient (streaming) slice for the current frame.
BufferSlice transientSlice{};
Uint64 transientFrameSerial = 0;
Uint64 transientChangeSerial = 0;
VkDeviceSize transientSize = 0;
};
// Supplies a command buffer that is recording and outside any render pass,
// for staged buffer-range copies. Implemented by VulkanRenderer.
class IBufferCopyCommandProvider {
public:
virtual ~IBufferCopyCommandProvider() = default;
virtual VkCommandBuffer AcquireBufferCopyCommandBuffer() = 0;
};
class VkBufferManager {
public:
Bool Initialize(const VkBufferManagerInitInfo& initInfo);
@@ -38,35 +77,77 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Recreate all per-frame transient arenas
Bool RecreateTransientArenas(Uint32 frameCount);
void BeginFrame(Uint32 frameIndex);
// All previously submitted GPU work has completed (vkDeviceWaitIdle).
void NotifyDeviceIdle();
// A frame slot's submission fence has been waited: every serial up to
// and including `serial` is complete. Raises the completed floor so
// GetCompletedSerial reflects real fence progress instead of only the
// frameSerial-minus-frameCount inference.
void NotifyFrameSerialComplete(Uint64 serial);
void SetCopyCommandProvider(IBufferCopyCommandProvider* provider);
Bool UploadTransient(BufferKind kind, Uint32 frameIndex, const void* data, VkDeviceSize size,
VkDeviceSize alignment, BufferSlice& outSlice);
Bool SyncResidentBuffer(BufferKind kind, const SharedPtr<MG_State::GLState::BufferObject>& bufferObject,
BufferSlice& outSlice);
void DowngradeResidentBufferToTransient(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject);
// Draw-time acquire for resident (device-storage) buffers: ensures the
// resource exists and is fully uploaded, marks it used this frame.
Bool AcquireResidentSlice(BufferKind kind, const SharedPtr<MG_State::GLState::BufferObject>& bufferObject,
BufferSlice& outSlice);
// Draw-time acquire for streamed buffers: uploads the whole shadow into
// the per-frame arena (cached by change serial), releasing any resident
// storage the buffer may still own.
Bool AcquireStreamedSlice(BufferKind kind, const SharedPtr<MG_State::GLState::BufferObject>& bufferObject,
BufferSlice& outSlice);
// Zero-copy persistent map (PipeResource GPU residency): create (once) a
// HOST_VISIBLE+COHERENT, persistently mapped resident buffer carrying every usage,
// seed it from the shadow, and return its mapped base for the app to write into
// directly. Idempotent. Returns nullptr on failure (frontend keeps its shadow).
void* AcquirePersistentMap(MG_State::GLState::BufferObject& bufferObject);
// Immediate ops, dispatched from the frontend BufferBackendOps table.
void OnRespecify(MG_State::GLState::BufferObject& bufferObject);
void OnSubData(MG_State::GLState::BufferObject& bufferObject, SizeT offset, SizeT size);
void OnFlushMappedRange(MG_State::GLState::BufferObject& bufferObject, Range1D range,
Flags<BufferMappingAccessBit> appAccess);
void OnResourceDestroyed(SharedPtr<MG_State::GLState::BackendBufferResource>&& resource);
Uint64 GetFrameSerial() const { return m_frameSerial; }
// Highest frame serial whose GPU work is known complete; serials at or
// below it may be considered signaled. Drives IsResourceBusy and the
// backend GL fence objects.
Uint64 GetCompletedSerial() const;
// Busy = potentially referenced by GPU work that has not been fenced yet
// (including commands recorded for the current, unsubmitted frame).
Bool IsResourceBusy(const VkBufferResource& resource) const;
private:
struct ResidentBufferEntry {
WeakPtr<MG_State::GLState::BufferObject> aliveRef;
VkBufferObject buffer;
VkDeviceSize size = 0;
VkBufferUsageFlags usage = 0;
};
Bool InitializeTransientArenas();
static VkBufferUsageFlags GetVkBufferUsage(BufferKind kind);
void DeferResidentRelease(VkBufferObject&& buffer);
void CollectDeferredResidentReleases(Uint32 frameIndex);
void CollectResidentGarbageIfNeeded();
void CollectResidentGarbageNow();
void DestroyDeferredResidentReleases();
void DestroyResidentBuffers();
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);
// Swap storage (conditional orphan) and refill it from the shadow copy.
Bool SwapStorageAndUploadAll(VkBufferResource& resource, MG_State::GLState::BufferObject& bufferObject);
// Record a staging-slice copy into the resident storage, ordered against
// in-flight and already-recorded GPU work.
Bool StagedRangeCopy(VkBufferResource& resource, MG_State::GLState::BufferObject& bufferObject,
SizeT offset, SizeT size);
void DeferRelease(VkBufferObject&& buffer);
void CollectDeferredReleases(Uint32 frameIndex);
void DestroyAllDeferredReleases();
void TrackLiveResource(const SharedPtr<VkBufferResource>& resource);
void ReleaseAllLiveResources();
VkBufferManagerInitInfo m_initInfo{};
BufferArena m_transientUploadArena;
UnorderedMap<MG_State::GLState::BufferObject*, ResidentBufferEntry> m_residentBuffers;
Vector<Vector<VkBufferObject>> m_deferredResidentReleases;
IBufferCopyCommandProvider* m_copyProvider = nullptr;
Vector<Vector<VkBufferObject>> m_deferredBufferReleases;
Vector<Vector<SharedPtr<VkBufferResource>>> m_deferredResourceReleases;
Vector<WeakPtr<VkBufferResource>> m_liveResources;
Uint32 m_currentFrameIndex = 0;
Uint32 m_residentGcTick = 0;
Uint64 m_frameSerial = 1;
Uint64 m_completedSerialFloor = 0;
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -49,11 +49,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
Bool VkBufferObject::Create(const VkBufferObjectDesc& desc) {
return Create(desc.allocator, desc.size, desc.usage, desc.memoryUsage, desc.allocationFlags);
return Create(desc.allocator, desc.size, desc.usage, desc.memoryUsage, desc.allocationFlags,
desc.requiredFlags);
}
Bool VkBufferObject::Create(VmaAllocator allocator, VkDeviceSize size, VkBufferUsageFlags usage,
VmaMemoryUsage memoryUsage, VmaAllocationCreateFlags allocationFlags) {
VmaMemoryUsage memoryUsage, VmaAllocationCreateFlags allocationFlags,
VkMemoryPropertyFlags requiredFlags) {
MOBILEGL_ASSERT(allocator != nullptr, "VkBufferObject::Create requires valid VMA allocator");
MOBILEGL_ASSERT(size > 0, "VkBufferObject::Create requires non-zero size");
@@ -69,6 +71,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VmaAllocationCreateInfo allocationInfo{};
allocationInfo.usage = memoryUsage;
allocationInfo.flags = allocationFlags;
allocationInfo.requiredFlags = requiredFlags;
const VkResult result =
vmaCreateBuffer(m_allocator, &bufferInfo, &allocationInfo, &m_buffer, &m_allocation, nullptr);
@@ -140,12 +143,39 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
Memcpy(static_cast<Uint8*>(mapped) + offset, data, static_cast<SizeT>(size));
const VkResult flushResult = vmaFlushAllocation(m_allocator, m_allocation, offset, size);
if (flushResult != VK_SUCCESS) {
MGLOG_E("VkBufferObject::Upload failed: vmaFlushAllocation returned %d", flushResult);
if (!wasMapped) {
Unmap();
}
return false;
}
if (!wasMapped) {
Unmap();
}
return true;
}
Bool VkBufferObject::Invalidate(VkDeviceSize size, VkDeviceSize offset) {
MOBILEGL_ASSERT(IsValid(), "VkBufferObject::Invalidate called on invalid buffer");
MOBILEGL_ASSERT(IsMapped(), "VkBufferObject::Invalidate requires mapped memory");
MOBILEGL_ASSERT(offset <= m_size, "VkBufferObject::Invalidate offset out of range");
const VkDeviceSize resolvedSize = size == VK_WHOLE_SIZE ? m_size - offset : size;
MOBILEGL_ASSERT(offset + resolvedSize <= m_size, "VkBufferObject::Invalidate range out of bounds");
if (resolvedSize == 0) {
return true;
}
const VkResult result = vmaInvalidateAllocation(m_allocator, m_allocation, offset, resolvedSize);
if (result != VK_SUCCESS) {
MGLOG_E("VkBufferObject::Invalidate failed: vmaInvalidateAllocation returned %d", result);
return false;
}
return true;
}
BufferSlice VkBufferObject::GetSlice(VkDeviceSize offset, VkDeviceSize size) const {
MOBILEGL_ASSERT(offset <= m_size, "VkBufferObject::GetSlice offset out of range");
const VkDeviceSize resolvedSize = (size == VK_WHOLE_SIZE) ? (m_size - offset) : size;
@@ -20,6 +20,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkBufferUsageFlags usage = 0;
VmaMemoryUsage memoryUsage = VMA_MEMORY_USAGE_AUTO;
VmaAllocationCreateFlags allocationFlags = 0;
// Memory property bits the allocation MUST satisfy (e.g. HOST_VISIBLE|HOST_COHERENT
// for a persistently-mapped buffer the app writes into without explicit flushes).
VkMemoryPropertyFlags requiredFlags = 0;
};
class VkBufferObject {
@@ -34,12 +37,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool Create(const VkBufferObjectDesc& desc);
Bool Create(VmaAllocator allocator, VkDeviceSize size, VkBufferUsageFlags usage,
VmaMemoryUsage memoryUsage, VmaAllocationCreateFlags allocationFlags = 0);
VmaMemoryUsage memoryUsage, VmaAllocationCreateFlags allocationFlags = 0,
VkMemoryPropertyFlags requiredFlags = 0);
void Destroy();
void* Map();
void Unmap();
Bool Upload(const void* data, VkDeviceSize size, VkDeviceSize offset = 0);
Bool Invalidate(VkDeviceSize size = VK_WHOLE_SIZE, VkDeviceSize offset = 0);
VkBuffer GetHandle() const { return m_buffer; }
VkDeviceSize GetSize() const { return m_size; }
@@ -12,12 +12,152 @@
#include "MG_Util/Converters/MGToStr/TextureEnumConverter.h"
namespace MobileGL::MG_Backend::DirectVulkan {
static Bool IsCubeMapFaceUploadTarget(TextureUploadTarget target) {
return target >= TextureUploadTarget::CubeMapPositiveX &&
target <= TextureUploadTarget::CubeMapNegativeZ;
}
static Bool PendingClearMatchesTextureIdentity(const PendingClearKey& key, const TextureIdentity& identity) {
return key.texture == identity.texture && key.textureLifetimeId == identity.lifetimeId;
}
static Uint32 ResolveAttachmentBaseArrayLayer(TextureUploadTarget target) {
if (!IsCubeMapFaceUploadTarget(target)) {
return 0;
}
return static_cast<Uint32>(target) - static_cast<Uint32>(TextureUploadTarget::CubeMapPositiveX);
}
static Uint32 ResolveAttachmentBaseArrayLayer(
const MG_State::GLState::FramebufferAttachmentObject& attachment) {
if (attachment.IsLayered()) {
return 0;
}
const TextureUploadTarget uploadTarget = attachment.GetTextureUploadTarget();
if (!IsCubeMapFaceUploadTarget(uploadTarget)) {
return static_cast<Uint32>(std::max(attachment.GetTextureLayer(), 0));
}
return ResolveAttachmentBaseArrayLayer(uploadTarget);
}
static Uint32 ResolveAttachmentLayerCount(
const MG_State::GLState::FramebufferAttachmentObject& attachment) {
if (attachment.IsLayered()) {
return static_cast<Uint32>(std::max(attachment.GetSize().z(), 1));
}
return 1u;
}
static const MG_State::GLState::FramebufferAttachmentObject* GetClearableAttachment(
const MG_State::GLState::FramebufferObject& drawFbo, FramebufferAttachmentType attachmentType) {
if (attachmentType == FramebufferAttachmentType::None) {
return nullptr;
}
const auto& attachment = drawFbo.GetAttachment(attachmentType);
if (!attachment.IsTexture() || attachment.IsRenderbuffer()) {
return nullptr;
}
return &attachment;
}
PendingClearKey VkClearManager::MakePendingClearKey(MG_State::GLState::ITextureObject* texture, Uint32 mipLevel,
Uint32 baseArrayLayer, Uint32 layerCount) {
return PendingClearKey {
.texture = texture,
.textureLifetimeId = texture ? texture->GetLifetimeId() : 0,
.mipLevel = mipLevel,
.baseArrayLayer = baseArrayLayer,
.layerCount = layerCount,
};
}
PendingClearKey VkClearManager::MakePendingClearKey(
const MG_State::GLState::FramebufferAttachmentObject& attachment) {
MOBILEGL_ASSERT(attachment.IsTexture() && !attachment.IsRenderbuffer(),
"MakePendingClearKey requires a texture framebuffer attachment");
auto* texture = attachment.GetTexture().get();
MOBILEGL_ASSERT(texture != nullptr, "MakePendingClearKey: texture attachment resolved to null");
const Uint32 mipLevel = static_cast<Uint32>(std::max(attachment.GetTextureLevel(), 0));
const Uint32 baseArrayLayer = ResolveAttachmentBaseArrayLayer(attachment);
const Uint32 layerCount = ResolveAttachmentLayerCount(attachment);
return MakePendingClearKey(texture, mipLevel, baseArrayLayer, layerCount);
}
Bool VkClearManager::Initialize() {
return true;
}
void VkClearManager::Shutdown() {
const std::lock_guard<std::mutex> lock(m_mutex);
m_pendingClears.clear();
m_aliveObjects.clear();
}
TextureIdentity VkClearManager::MakeTextureIdentity(MG_State::GLState::ITextureObject* texture) {
return TextureIdentity {
.texture = texture,
.lifetimeId = texture ? texture->GetLifetimeId() : 0,
};
}
void VkClearManager::MergeClearPayload(ClearAttachmentPayload& dst, const ClearAttachmentPayload& src) {
dst.mask |= src.mask;
if ((src.mask & GL_COLOR_BUFFER_BIT) != 0) {
dst.color = src.color;
}
if ((src.mask & GL_DEPTH_BUFFER_BIT) != 0) {
dst.depth = src.depth;
}
if ((src.mask & GL_STENCIL_BUFFER_BIT) != 0) {
dst.stencil = src.stencil;
}
}
void VkClearManager::ErasePendingClearsForTextureLocked(const TextureIdentity& identity) {
Vector<PendingClearKey> keysToErase;
keysToErase.reserve(m_pendingClears.size());
for (auto it = m_pendingClears.begin(); it != m_pendingClears.end(); ++it) {
if (PendingClearMatchesTextureIdentity(it->first, identity)) {
keysToErase.emplace_back(it->first);
}
}
for (const auto& key : keysToErase) {
m_pendingClears.erase(key);
}
m_aliveObjects.erase(identity);
}
Bool VkClearManager::LockTextureIdentityLocked(const TextureIdentity& identity,
SharedPtr<MG_State::GLState::ITextureObject>& outTexture) {
outTexture.reset();
if (identity.texture == nullptr) {
return false;
}
auto aliveIt = m_aliveObjects.find(identity);
if (aliveIt == m_aliveObjects.end()) {
ErasePendingClearsForTextureLocked(identity);
return false;
}
outTexture = aliveIt->second.lock();
if (!outTexture || outTexture.get() != identity.texture || outTexture->GetLifetimeId() != identity.lifetimeId) {
ErasePendingClearsForTextureLocked(identity);
outTexture.reset();
return false;
}
return true;
}
Bool VkClearManager::LockTextureLocked(const PendingClearKey& key,
SharedPtr<MG_State::GLState::ITextureObject>& outTexture) {
return LockTextureIdentityLocked(TextureIdentity{
.texture = key.texture,
.lifetimeId = key.textureLifetimeId,
}, outTexture);
}
void VkClearManager::QueueClear(GLbitfield mask, const ClearFramebufferPayload& clearPayload,
@@ -26,93 +166,237 @@ namespace MobileGL::MG_Backend::DirectVulkan {
auto& drawbufs = drawFbo.GetDrawBuffers();
// This should automatically work on default & offscreen FBO
for (auto drawbuf: drawbufs) {
if (drawbuf == FramebufferAttachmentType::None ||
drawFbo.GetAttachment(drawbuf).IsRenderbuffer())
const auto* attachment = GetClearableAttachment(drawFbo, drawbuf);
if (!attachment) {
continue;
}
QueueClear({
.color = clearPayload.color,
.attachmentType = drawbuf
}, drawFbo.GetAttachment(drawbuf).GetTexture());
.mask = GL_COLOR_BUFFER_BIT,
.color = clearPayload.color
}, *attachment);
MGLOG_D("%s: %s (texture %d) - color = (%.2f, %.2f, %.2f, %.2f)", __func__,
MG_Util::ConvertFramebufferAttachmentTypeToString(drawbuf).c_str(),
drawFbo.GetAttachment(drawbuf).GetTexture()->GetExternalIndex(),
attachment->GetTexture()->GetExternalIndex(),
clearPayload.color[0], clearPayload.color[1], clearPayload.color[2], clearPayload.color[3]);
}
}
if (mask & GL_DEPTH_BUFFER_BIT &&
!drawFbo.GetAttachment(FramebufferAttachmentType::Depth).IsRenderbuffer()) {
QueueClear({
.depth = clearPayload.depth,
.attachmentType = FramebufferAttachmentType::Depth,
}, drawFbo.GetAttachment(FramebufferAttachmentType::Depth).GetTexture());
MGLOG_D("%s: Depth (texture %d) - depth = (%.2f)", __func__,
drawFbo.GetAttachment(FramebufferAttachmentType::Depth).GetTexture()->GetExternalIndex(), clearPayload.depth);
if (mask & GL_DEPTH_BUFFER_BIT) {
const auto* attachment = GetClearableAttachment(drawFbo, FramebufferAttachmentType::Depth);
if (attachment) {
QueueClear({
.mask = GL_DEPTH_BUFFER_BIT,
.depth = clearPayload.depth,
}, *attachment);
MGLOG_D("%s: Depth (texture %d) - depth = (%.2f)", __func__,
attachment->GetTexture()->GetExternalIndex(), clearPayload.depth);
}
}
if (mask & GL_STENCIL_BUFFER_BIT &&
!drawFbo.GetAttachment(FramebufferAttachmentType::Stencil).IsRenderbuffer()) {
QueueClear({
.stencil = clearPayload.stencil,
.attachmentType = FramebufferAttachmentType::Stencil,
}, drawFbo.GetAttachment(FramebufferAttachmentType::Stencil).GetTexture());
MGLOG_D("%s: Stencil (texture %d) - stencil = (%u)", __func__,
drawFbo.GetAttachment(FramebufferAttachmentType::Stencil).GetTexture()->GetExternalIndex(), clearPayload.stencil);
if (mask & GL_STENCIL_BUFFER_BIT) {
const auto* attachment = GetClearableAttachment(drawFbo, FramebufferAttachmentType::Stencil);
if (attachment) {
QueueClear({
.mask = GL_STENCIL_BUFFER_BIT,
.stencil = clearPayload.stencil,
}, *attachment);
MGLOG_D("%s: Stencil (texture %d) - stencil = (%u)", __func__,
attachment->GetTexture()->GetExternalIndex(), clearPayload.stencil);
}
}
}
void VkClearManager::QueueClear(const ClearAttachmentPayload& clearPayload,
const SharedPtr<MG_State::GLState::ITextureObject>& texture) {
WeakPtr<MG_State::GLState::ITextureObject> weakTexturePtr = texture;
if (weakTexturePtr.expired())
if (clearPayload.mask == 0 || !texture) {
return;
auto* pTexture = weakTexturePtr.lock().get();
m_aliveObjects[pTexture] = weakTexturePtr;
m_pendingClears[pTexture] = clearPayload;
}
const PendingClearKey key = MakePendingClearKey(texture.get());
const std::lock_guard<std::mutex> lock(m_mutex);
m_aliveObjects[MakeTextureIdentity(texture.get())] = texture;
auto& pending = m_pendingClears[key];
MergeClearPayload(pending, clearPayload);
}
void VkClearManager::QueueClear(const ClearAttachmentPayload& clearPayload,
const MG_State::GLState::FramebufferAttachmentObject& attachment) {
if (clearPayload.mask == 0 || !attachment.IsTexture() || attachment.IsRenderbuffer()) {
return;
}
const auto texture = attachment.GetTexture();
if (!texture) {
return;
}
const PendingClearKey key = MakePendingClearKey(attachment);
const std::lock_guard<std::mutex> lock(m_mutex);
m_aliveObjects[MakeTextureIdentity(texture.get())] = texture;
auto& pending = m_pendingClears[key];
MergeClearPayload(pending, clearPayload);
}
Bool VkClearManager::HasPendingClear(MG_State::GLState::ITextureObject* texture) {
return m_pendingClears.find(texture) != m_pendingClears.end();
}
Bool VkClearManager::GetPendingClear(MG_State::GLState::ITextureObject* texture, ClearAttachmentPayload& outPayload) {
if (m_aliveObjects.find(texture) == m_aliveObjects.end() ||
m_pendingClears.find(texture) == m_pendingClears.end()) {
MGLOG_D("%s: Failed getting pending clear for texture %d", __func__, texture->GetExternalIndex());
if (texture == nullptr) {
return false;
}
outPayload = m_pendingClears[texture];
MGLOG_D("%s: Got pending clear for texture %d (%s), clear value: color = (%.2f, %.2f, %.2f, %.2f), depth = (%.2f), stencil = (%u)", __func__,
texture->GetExternalIndex(),
MG_Util::ConvertTextureInternalFormatToString(texture->GetFormat()).c_str(),
const Uint64 lifetimeId = texture->GetLifetimeId();
const std::lock_guard<std::mutex> lock(m_mutex);
for (auto it = m_pendingClears.begin(); it != m_pendingClears.end(); ++it) {
if (it->first.texture == texture && it->first.textureLifetimeId == lifetimeId) {
SharedPtr<MG_State::GLState::ITextureObject> liveTexture;
return LockTextureLocked(it->first, liveTexture);
}
}
return false;
}
Bool VkClearManager::HasPendingClear(const PendingClearKey& key) {
if (key.texture == nullptr) {
return false;
}
const std::lock_guard<std::mutex> lock(m_mutex);
if (m_pendingClears.find(key) == m_pendingClears.end()) {
return false;
}
SharedPtr<MG_State::GLState::ITextureObject> liveTexture;
return LockTextureLocked(key, liveTexture);
}
Bool VkClearManager::HasPendingClear(const MG_State::GLState::FramebufferAttachmentObject& attachment) {
if (!attachment.IsTexture() || attachment.IsRenderbuffer() || !attachment.GetTexture()) {
return false;
}
return HasPendingClear(MakePendingClearKey(attachment));
}
Bool VkClearManager::GetPendingClear(const PendingClearKey& key, ClearAttachmentPayload& outPayload) {
SharedPtr<MG_State::GLState::ITextureObject> liveTexture;
return GetPendingClear(key, outPayload, liveTexture);
}
Bool VkClearManager::GetPendingClear(const PendingClearKey& key, ClearAttachmentPayload& outPayload,
SharedPtr<MG_State::GLState::ITextureObject>& outTexture) {
if (key.texture == nullptr) {
return false;
}
const std::lock_guard<std::mutex> lock(m_mutex);
if (!LockTextureLocked(key, outTexture)) {
return false;
}
auto it = m_pendingClears.find(key);
if (it == m_pendingClears.end()) {
outTexture.reset();
return false;
}
outPayload = it->second;
MGLOG_D("%s: Got pending clear for texture@%p lifetime=%llu, mip=%u layer=%u count=%u mask=0x%x clear value: color = (%.2f, %.2f, %.2f, %.2f), depth = (%.2f), stencil = (%u)", __func__,
static_cast<void*>(key.texture),
static_cast<unsigned long long>(key.textureLifetimeId),
key.mipLevel, key.baseArrayLayer, key.layerCount,
static_cast<Uint32>(outPayload.mask),
outPayload.color[0], outPayload.color[1], outPayload.color[2], outPayload.color[3],
outPayload.depth,
outPayload.stencil);
return true;
}
Bool VkClearManager::GetPendingClear(const MG_State::GLState::FramebufferAttachmentObject& attachment,
ClearAttachmentPayload& outPayload) {
if (!attachment.IsTexture() || attachment.IsRenderbuffer() || !attachment.GetTexture()) {
MGLOG_D("%s: Failed getting pending clear for non-texture framebuffer attachment", __func__);
return false;
}
return GetPendingClear(MakePendingClearKey(attachment), outPayload);
}
Bool VkClearManager::GetPendingClears(MG_State::GLState::ITextureObject* texture,
Vector<PendingClearEntry>& outEntries) {
outEntries.clear();
if (texture == nullptr) {
return false;
}
const Uint64 lifetimeId = texture->GetLifetimeId();
const std::lock_guard<std::mutex> lock(m_mutex);
SharedPtr<MG_State::GLState::ITextureObject> liveTexture;
if (!LockTextureIdentityLocked(MakeTextureIdentity(texture), liveTexture)) {
return false;
}
for (auto it = m_pendingClears.begin(); it != m_pendingClears.end(); ++it) {
if (it->first.texture == texture && it->first.textureLifetimeId == lifetimeId) {
outEntries.emplace_back(PendingClearEntry{.key = it->first, .payload = it->second});
}
}
return !outEntries.empty();
}
void VkClearManager::PopPendingClear(MG_State::GLState::ITextureObject* texture) {
MGLOG_D("%s: Pop pending clear for texture %d", __func__, texture->GetExternalIndex());
m_aliveObjects.erase(texture);
m_pendingClears.erase(texture);
if (texture == nullptr) {
return;
}
const TextureIdentity identity = MakeTextureIdentity(texture);
MGLOG_D("%s: Pop all pending clears for texture %d", __func__, texture->GetExternalIndex());
const std::lock_guard<std::mutex> lock(m_mutex);
ErasePendingClearsForTextureLocked(identity);
}
void VkClearManager::PopPendingClear(const PendingClearKey& key) {
if (key.texture == nullptr) {
return;
}
{
const std::lock_guard<std::mutex> lock(m_mutex);
auto it = m_pendingClears.find(key);
if (it != m_pendingClears.end()) {
m_pendingClears.erase(it);
}
}
MGLOG_D("%s: Pop pending clear for texture@%p lifetime=%llu mip=%u layer=%u count=%u", __func__,
static_cast<void*>(key.texture), static_cast<unsigned long long>(key.textureLifetimeId),
key.mipLevel, key.baseArrayLayer, key.layerCount);
}
void VkClearManager::PopPendingClear(const MG_State::GLState::FramebufferAttachmentObject& attachment) {
if (!attachment.IsTexture() || attachment.IsRenderbuffer() || !attachment.GetTexture()) {
return;
}
PopPendingClear(MakePendingClearKey(attachment));
}
SizeT VkClearManager::CollectGarbage() {
const std::lock_guard<std::mutex> lock(m_mutex);
m_gcCounter++;
if (m_gcCounter != 0) {
return 0;
}
SizeT count = 0;
for (const auto& [raw, weak]: m_aliveObjects) {
if (weak.expired()) {
count++;
m_pendingClears.erase(raw);
m_aliveObjects.erase(raw);
Vector<TextureIdentity> expiredTextures;
expiredTextures.reserve(m_aliveObjects.size());
for (auto it = m_aliveObjects.begin(); it != m_aliveObjects.end(); ++it) {
if (it->second.expired()) {
expiredTextures.emplace_back(it->first);
}
}
return count;
if (expiredTextures.empty()) {
return 0;
}
for (const auto& identity : expiredTextures) {
ErasePendingClearsForTextureLocked(identity);
}
return expiredTextures.size();
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -14,6 +14,7 @@
#include "MG_Util/Math/VectorTypes.h"
#include <Includes.h>
#include <unordered_map>
namespace MobileGL::MG_Backend::DirectVulkan {
struct ClearFramebufferPayload {
@@ -23,16 +24,70 @@ namespace MobileGL::MG_Backend::DirectVulkan {
};
struct ClearAttachmentPayload {
union {
FloatVec4 color;
Float depth{};
Uint32 stencil;
};
FramebufferAttachmentType attachmentType = FramebufferAttachmentType::Color0;
GLbitfield mask = 0;
FloatVec4 color = FloatVec4(0.0f, 0.0f, 0.0f, 0.0f);
Float depth = 1.0f;
Uint32 stencil = 0;
};
struct PendingClearKey {
MG_State::GLState::ITextureObject* texture = nullptr;
Uint64 textureLifetimeId = 0;
Uint32 mipLevel = 0;
Uint32 baseArrayLayer = 0;
Uint32 layerCount = 1;
Bool operator==(const PendingClearKey& other) const {
return texture == other.texture && textureLifetimeId == other.textureLifetimeId &&
mipLevel == other.mipLevel &&
baseArrayLayer == other.baseArrayLayer && layerCount == other.layerCount;
}
};
struct TextureIdentity {
MG_State::GLState::ITextureObject* texture = nullptr;
Uint64 lifetimeId = 0;
Bool operator==(const TextureIdentity& other) const {
return texture == other.texture && lifetimeId == other.lifetimeId;
}
};
struct PendingClearEntry {
PendingClearKey key{};
ClearAttachmentPayload payload{};
};
struct PendingClearKeyHash {
SizeT operator()(const PendingClearKey& key) const {
const SizeT textureHash = std::hash<MG_State::GLState::ITextureObject*>{}(key.texture);
const SizeT textureLifetimeHash = std::hash<Uint64>{}(key.textureLifetimeId);
const SizeT mipHash = std::hash<Uint32>{}(key.mipLevel);
const SizeT layerHash = std::hash<Uint32>{}(key.baseArrayLayer);
const SizeT layerCountHash = std::hash<Uint32>{}(key.layerCount);
SizeT hash = textureHash;
hash ^= textureLifetimeHash + 0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= mipHash + 0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= layerHash + 0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= layerCountHash + 0x9e3779b9u + (hash << 6) + (hash >> 2);
return hash;
}
};
struct TextureIdentityHash {
SizeT operator()(const TextureIdentity& key) const {
SizeT hash = std::hash<MG_State::GLState::ITextureObject*>{}(key.texture);
hash ^= std::hash<Uint64>{}(key.lifetimeId) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
return hash;
}
};
class VkClearManager {
public:
static PendingClearKey MakePendingClearKey(const MG_State::GLState::FramebufferAttachmentObject& attachment);
static PendingClearKey MakePendingClearKey(MG_State::GLState::ITextureObject* texture, Uint32 mipLevel = 0,
Uint32 baseArrayLayer = 0, Uint32 layerCount = 1);
Bool Initialize();
void Shutdown();
@@ -40,13 +95,33 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void QueueClear(
const ClearAttachmentPayload& clearPayload,
const SharedPtr<MG_State::GLState::ITextureObject>& texture);
void QueueClear(const ClearAttachmentPayload& clearPayload,
const MG_State::GLState::FramebufferAttachmentObject& attachment);
Bool HasPendingClear(MG_State::GLState::ITextureObject* texture);
Bool GetPendingClear(MG_State::GLState::ITextureObject* texture, ClearAttachmentPayload& outPayload);
Bool HasPendingClear(const PendingClearKey& key);
Bool HasPendingClear(const MG_State::GLState::FramebufferAttachmentObject& attachment);
Bool GetPendingClear(const PendingClearKey& key, ClearAttachmentPayload& outPayload);
Bool GetPendingClear(const PendingClearKey& key, ClearAttachmentPayload& outPayload,
SharedPtr<MG_State::GLState::ITextureObject>& outTexture);
Bool GetPendingClear(const MG_State::GLState::FramebufferAttachmentObject& attachment,
ClearAttachmentPayload& outPayload);
Bool GetPendingClears(MG_State::GLState::ITextureObject* texture, Vector<PendingClearEntry>& outEntries);
void PopPendingClear(MG_State::GLState::ITextureObject* texture);
void PopPendingClear(const PendingClearKey& key);
void PopPendingClear(const MG_State::GLState::FramebufferAttachmentObject& attachment);
SizeT CollectGarbage();
private:
static TextureIdentity MakeTextureIdentity(MG_State::GLState::ITextureObject* texture);
static void MergeClearPayload(ClearAttachmentPayload& dst, const ClearAttachmentPayload& src);
void ErasePendingClearsForTextureLocked(const TextureIdentity& identity);
Bool LockTextureIdentityLocked(const TextureIdentity& identity,
SharedPtr<MG_State::GLState::ITextureObject>& outTexture);
Bool LockTextureLocked(const PendingClearKey& key,
SharedPtr<MG_State::GLState::ITextureObject>& outTexture);
Uint8 m_gcCounter = 0;
UnorderedMap<MG_State::GLState::ITextureObject*, ClearAttachmentPayload> m_pendingClears;
UnorderedMap<MG_State::GLState::ITextureObject*, WeakPtr<MG_State::GLState::ITextureObject>> m_aliveObjects;
mutable std::mutex m_mutex;
std::unordered_map<PendingClearKey, ClearAttachmentPayload, PendingClearKeyHash> m_pendingClears;
std::unordered_map<TextureIdentity, WeakPtr<MG_State::GLState::ITextureObject>, TextureIdentityHash> m_aliveObjects;
};
} // namespace MobileGL::MG_Backend::DirectVulkan
File diff suppressed because it is too large Load Diff
@@ -16,26 +16,49 @@
#include "MG_State/GLState/FramebufferState/FramebufferObject.h"
#include <Includes.h>
#include <vk_mem_alloc.h>
namespace MobileGL::MG_Backend::DirectVulkan {
enum class TrackedAttachmentTarget : Uint8 {
Texture,
Renderbuffer,
SwapchainColor,
SwapchainDepthStencil
};
struct PendingClearAttachmentInfo {
// Index into the render pass attachment descriptions (VkRenderPassBeginInfo::pClearValues space).
Uint32 attachmentIndex = 0;
MG_State::GLState::ITextureObject* texture = nullptr;
// 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;
ClearAttachmentPayload inlinePayload{};
};
struct TrackedAttachmentLayoutInfo {
TrackedAttachmentTarget target = TrackedAttachmentTarget::Texture;
MG_State::GLState::ITextureObject* texture = nullptr;
WeakPtr<MG_State::GLState::ITextureObject> texture;
WeakPtr<MG_State::GLState::RenderbufferObject> renderbuffer;
Uint32 textureMipLevel = 0;
Uint32 swapchainImageIndex = 0;
VkImageLayout finalLayout = VK_IMAGE_LAYOUT_UNDEFINED;
};
struct DepthStencilAttachmentLoadInfo {
VkAttachmentLoadOp depthLoadOp = VK_ATTACHMENT_LOAD_OP_LOAD;
VkAttachmentLoadOp stencilLoadOp = VK_ATTACHMENT_LOAD_OP_LOAD;
VkImageLayout initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
};
DepthStencilAttachmentLoadInfo ResolveDepthStencilAttachmentLoadInfo(
VkImageLayout trackedLayout, Bool clearDepth, Bool clearStencil);
IntVec2 ResolveRenderPassFramebufferExtent(Bool isDefaultFbo, const TextureSize& attachmentExtent,
VkExtent2D swapchainExtent);
struct RenderPassEntry {
static inline VkDevice s_device;
static inline Vector<VkTextureManager::TextureResource*> s_textureResourcesScratch;
@@ -46,8 +69,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Vector<PendingClearAttachmentInfo> pendingClearAttachments;
Vector<TrackedAttachmentLayoutInfo> trackedAttachmentLayouts;
Uint32 attachmentCount = 0;
Uint32 colorAttachmentCount = 0;
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;
@@ -59,8 +88,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
std::swap(pendingClearAttachments, that.pendingClearAttachments);
std::swap(trackedAttachmentLayouts, that.trackedAttachmentLayouts);
std::swap(attachmentCount, that.attachmentCount);
std::swap(colorAttachmentCount, that.colorAttachmentCount);
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,
@@ -70,7 +103,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const Vector<PendingClearAttachmentInfo>& pendingClearAttachments,
const Vector<TrackedAttachmentLayoutInfo>& trackedAttachmentLayouts,
Uint32 attachmentCount,
IntVec2 extent, int subpass):
Uint32 colorAttachmentCount,
Bool hasDepthStencilAttachment,
VkSampleCountFlagBits sampleCount,
IntVec2 extent, Uint32 layers):
hash(hash),
renderPass(renderpass),
framebuffer(framebuffer),
@@ -78,8 +114,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
pendingClearAttachments(Move(pendingClearAttachments)),
trackedAttachmentLayouts(Move(trackedAttachmentLayouts)),
attachmentCount(attachmentCount),
colorAttachmentCount(colorAttachmentCount),
hasDepthStencilAttachment(hasDepthStencilAttachment),
sampleCount(sampleCount),
extent(extent),
subpass(subpass)
layers(layers)
{}
~RenderPassEntry() {
@@ -119,8 +158,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
public:
using HashType = Uint64;
VkRenderPassManager(VkDevice device,
const VulkanRendererConfig& config, VkClearManager& clearManager, VkTextureManager& textureManager,
SwapchainObject& swapchainObject);
VkPhysicalDevice physicalDevice, VmaAllocator allocator, const VulkanRendererConfig& config,
VkClearManager& clearManager, VkTextureManager& textureManager, SwapchainObject& swapchainObject);
~VkRenderPassManager();
Bool Initialize();
@@ -129,23 +168,92 @@ namespace MobileGL::MG_Backend::DirectVulkan {
HashType ComputeHash(
const MG_State::GLState::FramebufferObject& fbo,
Uint32 swapchainImageIndex,
Bool includePendingClear = true) const;
Bool includePendingClear = true);
RenderPassEntry& GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo, Uint32 swapchainImageIndex);
void QueueRenderbufferClear(GLbitfield mask, const ClearFramebufferPayload& clearPayload,
const MG_State::GLState::FramebufferObject& drawFbo);
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();
private:
VkDevice m_device = VK_NULL_HANDLE;
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
VmaAllocator m_allocator = nullptr;
const VulkanRendererConfig& m_config;
VkClearManager& m_clearManager;
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
// image recreation.
Uint64 m_renderbufferImageEpoch = 1;
// Per-draw fast-path memo for GetOrCreateRenderPass (dirty-flag state tracking): when the
// framebuffer state is provably unchanged since the last resolution, the active render pass
// is reused WITHOUT recomputing the expensive per-draw hash. Invalidated by FBO switch /
// version change, swapchain rotation, any attachment image recreation (the two epochs),
// or a pending clear. Portable to Vulkan 1.1 (no dynamic_rendering / imageless FB needed).
Bool m_rpFastValid = false;
const MG_State::GLState::FramebufferObject* m_rpFastFbo = nullptr;
Uint16 m_rpFastFboVersion = 0;
Uint32 m_rpFastSwapchainIndex = 0;
Uint64 m_rpFastTexEpoch = 0;
Uint64 m_rpFastRbEpoch = 0;
Uint64 m_rpFastRenderPassHash = 0;
public:
struct RenderbufferResource {
WeakPtr<MG_State::GLState::RenderbufferObject> renderbuffer;
VkImage image = VK_NULL_HANDLE;
VmaAllocation allocation = nullptr;
VkImageView view = VK_NULL_HANDLE;
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
VkFormat format = VK_FORMAT_UNDEFINED;
VkImageAspectFlags aspect = VK_IMAGE_ASPECT_NONE;
VkExtent2D extent = {0, 0};
VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
TextureInternalFormat internalFormat = TextureInternalFormat::Unknown;
Int samples = 0;
void Destroy(VkDevice device, VmaAllocator allocator);
};
// Public so the renderer's blit/copy/readback bindings can source renderbuffer
// attachments the same way texture attachments go through the texture manager.
RenderbufferResource* GetOrCreateRenderbufferResource(
const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer);
Bool GetPendingRenderbufferClear(MG_State::GLState::RenderbufferObject* renderbuffer,
ClearAttachmentPayload& outPayload) const;
private:
struct PendingRenderbufferClear {
WeakPtr<MG_State::GLState::RenderbufferObject> renderbuffer;
ClearAttachmentPayload payload{};
};
UnorderedMap<MG_State::GLState::RenderbufferObject*, RenderbufferResource> m_renderbufferResources;
UnorderedMap<MG_State::GLState::RenderbufferObject*, PendingRenderbufferClear> m_pendingRenderbufferClears;
Bool HasPendingRenderbufferClear(
const MG_State::GLState::FramebufferAttachmentObject& attachment) const;
void CollectRenderbufferGarbage();
static inline XXH64_state_t* m_hashState = XXH64_createState();
static inline ActiveRenderPassInfo s_activeRenderPass{};
static inline Bool s_hasActiveRenderPass = false;
static inline VkClearManager* s_clearManager = nullptr;
static inline VkTextureManager* s_textureManager = nullptr;
static inline SwapchainObject* s_swapchainObject = nullptr;
static inline VkRenderPassManager* s_renderPassManager = nullptr;
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -10,17 +10,74 @@
#include "MG_State/GLState/Core.h"
#include <algorithm>
#include <cmath>
namespace MobileGL::MG_Backend::DirectVulkan {
namespace {
Bool UsesBorderColor(const MG_State::GLState::SamplerObject& sampler) {
return sampler.GetWrapS() == SamplerWrapMode::ClampToBorder ||
sampler.GetWrapT() == SamplerWrapMode::ClampToBorder ||
sampler.GetWrapR() == SamplerWrapMode::ClampToBorder;
}
Bool IsDepthTextureFormat(TextureInternalFormat format) {
switch (format) {
case TextureInternalFormat::DepthComponent:
case TextureInternalFormat::DepthComponent16:
case TextureInternalFormat::DepthComponent24:
case TextureInternalFormat::DepthComponent32:
case TextureInternalFormat::DepthComponent32F:
case TextureInternalFormat::Depth24Stencil8:
case TextureInternalFormat::Depth32FStencil8:
case TextureInternalFormat::DepthStencil:
return true;
default:
return false;
}
}
Bool NearlyEqual(Float lhs, Float rhs) {
return std::fabs(lhs - rhs) <= 1e-6f;
}
Float ResolveEffectiveMaxLod(const MG_State::GLState::SamplerObject& sampler) {
if (sampler.GetMipmapMode() == SamplerMipmapMode::None) {
return 0.0f;
}
return sampler.GetMaxLod();
}
Float ResolveEffectiveMinLod(const MG_State::GLState::SamplerObject& sampler, Float effectiveMaxLod) {
return std::min(sampler.GetMinLod(), effectiveMaxLod);
}
} // namespace
Bool VkSamplerManager::Initialize(const InitInfo& initInfo) {
Shutdown();
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,
Bool forceNearestFiltering) const {
if (!m_samplerAnisotropySupported) return 1.0f;
if (forceNearestFiltering) 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) {
@@ -34,10 +91,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_config = nullptr;
}
Uint64 VkSamplerManager::BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler) const {
Uint64 VkSamplerManager::BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture,
Bool forceNearestFiltering) const {
MOBILEGL_ASSERT(m_config != nullptr, "VkSamplerManager::BuildSamplerKey: m_config is null");
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config->CacheVersion));
XXHASH_VERIFY(XXH64_update(m_hashState, &forceNearestFiltering, sizeof(forceNearestFiltering)));
const auto minFilter = sampler.GetMinFilter();
XXHASH_VERIFY(XXH64_update(m_hashState, &minFilter, sizeof(minFilter)));
const auto magFilter = sampler.GetMagFilter();
@@ -50,21 +111,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
XXHASH_VERIFY(XXH64_update(m_hashState, &wrapT, sizeof(wrapT)));
const auto wrapR = sampler.GetWrapR();
XXHASH_VERIFY(XXH64_update(m_hashState, &wrapR, sizeof(wrapR)));
const auto minLod = sampler.GetMinLod();
const auto maxLod = ResolveEffectiveMaxLod(sampler);
const auto minLod = ResolveEffectiveMinLod(sampler, maxLod);
XXHASH_VERIFY(XXH64_update(m_hashState, &minLod, sizeof(minLod)));
const auto maxLod = sampler.GetMaxLod();
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, forceNearestFiltering);
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 = sampler.GetSamplerCompareFunc();
const auto compareFunc = ResolveCompareFunc(sampler, texture);
XXHASH_VERIFY(XXH64_update(m_hashState, &compareFunc, sizeof(compareFunc)));
const auto borderColor = ResolveVkBorderColor(sampler, texture);
XXHASH_VERIFY(XXH64_update(m_hashState, &borderColor, sizeof(borderColor)));
return XXH64_digest(m_hashState);
}
VkSampler VkSamplerManager::GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler) {
const Uint64 key = BuildSamplerKey(sampler);
VkSampler VkSamplerManager::GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture,
Bool forceNearestFiltering) {
const Uint64 key = BuildSamplerKey(sampler, texture, forceNearestFiltering);
auto it = m_samplers.find(key);
if (it != m_samplers.end()) {
return it->second.handle;
@@ -72,20 +142,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkSamplerCreateInfo samplerInfo{};
samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
samplerInfo.magFilter = ToVkFilter(sampler.GetMagFilter());
samplerInfo.minFilter = ToVkFilter(sampler.GetMinFilter());
samplerInfo.mipmapMode = ToVkMipmapMode(sampler.GetMipmapMode());
samplerInfo.magFilter = forceNearestFiltering ? VK_FILTER_NEAREST : ToVkFilter(sampler.GetMagFilter());
samplerInfo.minFilter = forceNearestFiltering ? VK_FILTER_NEAREST : ToVkFilter(sampler.GetMinFilter());
samplerInfo.mipmapMode = forceNearestFiltering ? VK_SAMPLER_MIPMAP_MODE_NEAREST
: ToVkMipmapMode(sampler.GetMipmapMode());
samplerInfo.addressModeU = ToVkAddressMode(sampler.GetWrapS());
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, forceNearestFiltering);
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(sampler.GetSamplerCompareFunc());
samplerInfo.minLod = sampler.GetMinLod();
samplerInfo.maxLod = sampler.GetMaxLod();
samplerInfo.borderColor = VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK;
samplerInfo.compareOp = ToVkCompareOp(ResolveCompareFunc(sampler, texture));
samplerInfo.maxLod = ResolveEffectiveMaxLod(sampler);
samplerInfo.minLod = ResolveEffectiveMinLod(sampler, samplerInfo.maxLod);
samplerInfo.borderColor = ResolveVkBorderColor(sampler, texture);
samplerInfo.unnormalizedCoordinates = VK_FALSE;
VkSampler vkSampler = VK_NULL_HANDLE;
@@ -153,4 +227,49 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return VK_COMPARE_OP_ALWAYS;
}
}
SamplerCompareFunc VkSamplerManager::ResolveCompareFunc(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture) {
const auto compareFunc = sampler.GetSamplerCompareFunc();
if (sampler.GetCompareMode() == SamplerCompareMode::CompareToTexture &&
IsDepthTextureFormat(texture.GetFormat()) && compareFunc == SamplerCompareFunc::Always) {
return SamplerCompareFunc::LessEqual;
}
return compareFunc;
}
VkBorderColor VkSamplerManager::ResolveVkBorderColor(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture) {
if (!UsesBorderColor(sampler)) {
return VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK;
}
const auto& borderColor = texture.GetBorderColor();
const Bool isDepthTexture = IsDepthTextureFormat(texture.GetFormat());
if (isDepthTexture) {
if (NearlyEqual(borderColor.x(), 1.0f)) {
return VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
}
if (NearlyEqual(borderColor.x(), 0.0f)) {
return VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK;
}
}
const Bool rgbZero = NearlyEqual(borderColor.x(), 0.0f) && NearlyEqual(borderColor.y(), 0.0f) &&
NearlyEqual(borderColor.z(), 0.0f);
if (rgbZero && NearlyEqual(borderColor.w(), 0.0f)) {
return VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK;
}
if (rgbZero && NearlyEqual(borderColor.w(), 1.0f)) {
return VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK;
}
if (NearlyEqual(borderColor.x(), 1.0f) && NearlyEqual(borderColor.y(), 1.0f) &&
NearlyEqual(borderColor.z(), 1.0f) && NearlyEqual(borderColor.w(), 1.0f)) {
return VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
}
return VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK;
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -15,6 +15,7 @@
namespace MobileGL::MG_State::GLState {
class SamplerObject;
class ITextureObject;
}
namespace MobileGL::MG_Backend::DirectVulkan {
@@ -23,12 +24,18 @@ 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);
void Shutdown();
VkSampler GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler);
VkSampler GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture,
Bool forceNearestFiltering = false);
private:
struct SamplerCacheEntry {
@@ -37,14 +44,28 @@ private:
Uint16 version = 0;
};
Uint64 BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler) const;
Uint64 BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture,
Bool forceNearestFiltering) const;
static VkFilter ToVkFilter(SamplerFilterMode mode);
static VkSamplerMipmapMode ToVkMipmapMode(SamplerMipmapMode mode);
static VkSamplerAddressMode ToVkAddressMode(SamplerWrapMode mode);
static VkCompareOp ToVkCompareOp(SamplerCompareFunc func);
static SamplerCompareFunc ResolveCompareFunc(const MG_State::GLState::SamplerObject& sampler,
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,
Bool forceNearestFiltering) 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();
};
File diff suppressed because it is too large Load Diff
@@ -12,35 +12,159 @@
#include <Includes.h>
#include <MG_State/GLState/TextureState/TextureObject.h>
#include <vk_mem_alloc.h>
#include <unordered_map>
#include <unordered_set>
namespace MobileGL::MG_State::GLState {
class ITextureObject;
}
namespace MobileGL::MG_Backend::DirectVulkan {
enum class SamplerNumericDomain : Uint8;
class VkTextureManager {
public:
// Monotonic epoch bumped whenever a texture VkImage is (re)created. The render-pass
// manager keys its per-draw fast path on this so an attachment's image recreation
// invalidates the cached render pass (dirty-flag tracking; portable to Vulkan 1.1).
Uint64 GetTextureImageEpoch() const { return m_textureImageEpoch; }
struct TextureIdentity {
MG_State::GLState::ITextureObject* texture = nullptr;
Uint64 lifetimeId = 0;
Bool operator==(const TextureIdentity& other) const {
return texture == other.texture && lifetimeId == other.lifetimeId;
}
};
struct TextureIdentityHash {
SizeT operator()(const TextureIdentity& key) const {
SizeT hash = std::hash<MG_State::GLState::ITextureObject*>{}(key.texture);
hash ^= std::hash<Uint64>{}(key.lifetimeId) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
return hash;
}
};
struct InitInfo {
VkDevice device = VK_NULL_HANDLE;
VkPhysicalDevice physicalDevice = VK_NULL_HANDLE;
VmaAllocator allocator = nullptr;
VkCommandPool commandPool = VK_NULL_HANDLE;
VkQueue graphicsQueue = VK_NULL_HANDLE;
Uint32 frameCount = 0;
};
struct TextureResource {
struct AttachmentViewKey {
Uint32 mipLevel = 0;
Uint32 baseArrayLayer = 0;
Uint32 layerCount = 1;
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
Bool operator==(const AttachmentViewKey& other) const {
return mipLevel == other.mipLevel &&
baseArrayLayer == other.baseArrayLayer &&
layerCount == other.layerCount &&
viewType == other.viewType;
}
};
struct AttachmentViewKeyHash {
SizeT operator()(const AttachmentViewKey& key) const {
SizeT hash = std::hash<Uint32>{}(key.mipLevel);
hash ^= std::hash<Uint32>{}(key.baseArrayLayer) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= std::hash<Uint32>{}(key.layerCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.viewType)) +
0x9e3779b9u + (hash << 6) + (hash >> 2);
return hash;
}
};
struct StorageImageViewKey {
Uint32 mipLevel = 0;
Uint32 baseArrayLayer = 0;
Uint32 layerCount = 1;
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
VkFormat format = VK_FORMAT_UNDEFINED;
Bool operator==(const StorageImageViewKey& other) const {
return mipLevel == other.mipLevel &&
baseArrayLayer == other.baseArrayLayer &&
layerCount == other.layerCount &&
viewType == other.viewType &&
format == other.format;
}
};
struct SampledImageViewKey {
Uint32 baseMipLevel = 0;
Uint32 levelCount = 1;
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
VkFormat format = VK_FORMAT_UNDEFINED;
Bool operator==(const SampledImageViewKey& other) const {
return baseMipLevel == other.baseMipLevel &&
levelCount == other.levelCount &&
viewType == other.viewType &&
format == other.format;
}
};
struct SampledImageViewKeyHash {
SizeT operator()(const SampledImageViewKey& key) const {
SizeT hash = std::hash<Uint32>{}(key.baseMipLevel);
hash ^= std::hash<Uint32>{}(key.levelCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.viewType)) +
0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.format)) +
0x9e3779b9u + (hash << 6) + (hash >> 2);
return hash;
}
};
struct StorageImageViewKeyHash {
SizeT operator()(const StorageImageViewKey& key) const {
SizeT hash = std::hash<Uint32>{}(key.mipLevel);
hash ^= std::hash<Uint32>{}(key.baseArrayLayer) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= std::hash<Uint32>{}(key.layerCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.viewType)) +
0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.format)) +
0x9e3779b9u + (hash << 6) + (hash >> 2);
return hash;
}
};
VkImage image = VK_NULL_HANDLE;
VmaAllocation allocation = nullptr;
VkImageView fullView = VK_NULL_HANDLE;
VkImageView sampledView = VK_NULL_HANDLE;
Vector<VkImageView> perMipViews;
Vector<VkImageView> perMipSampledViews;
UnorderedMap<AttachmentViewKey, VkImageView, AttachmentViewKeyHash> attachmentViews;
UnorderedMap<SampledImageViewKey, VkImageView, SampledImageViewKeyHash> alternateSampledViews;
UnorderedMap<StorageImageViewKey, VkImageView, StorageImageViewKeyHash> storageImageViews;
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
VkExtent2D extent = {0, 0};
Uint32 depth = 1;
Uint32 arrayLayers = 1;
Uint32 mipLevels = 1;
Uint32 sampledBaseMipLevel = 0;
Uint32 sampledLevelCount = 1;
VkFormat format = VK_FORMAT_UNDEFINED;
VkImageAspectFlags aspect = VK_IMAGE_ASPECT_NONE;
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
VkImageCreateFlags imageCreateFlags = 0;
Uint16 syncedTextureParamsVersion = 0;
// Snapshot of ITextureObject::GetContentVersion() at the last successful sync;
// lets SyncTexture skip the whole re-check/re-upload when content is unchanged.
Uint64 syncedContentVersion = 0;
// Snapshot of the defined mip-level count at the last sync. Folded into the early-out key
// as defense-in-depth: any path that grows the level set (which resizes the sampled view)
// busts the skip even if it failed to bump the content version.
Uint32 syncedMipLevelCount = 0;
TextureResource() = default;
TextureResource(const TextureResource&) = delete;
@@ -48,41 +172,88 @@ public:
std::swap(this->image, that.image);
std::swap(this->allocation, that.allocation);
std::swap(this->fullView, that.fullView);
std::swap(this->sampledView, that.sampledView);
std::swap(this->perMipViews, that.perMipViews);
std::swap(this->perMipSampledViews, that.perMipSampledViews);
std::swap(this->attachmentViews, that.attachmentViews);
std::swap(this->alternateSampledViews, that.alternateSampledViews);
std::swap(this->storageImageViews, that.storageImageViews);
std::swap(this->layout, that.layout);
std::swap(this->extent, that.extent);
std::swap(this->depth, that.depth);
std::swap(this->arrayLayers, that.arrayLayers);
std::swap(this->mipLevels, that.mipLevels);
std::swap(this->sampledBaseMipLevel, that.sampledBaseMipLevel);
std::swap(this->sampledLevelCount, that.sampledLevelCount);
std::swap(this->format, that.format);
std::swap(this->aspect, that.aspect);
std::swap(this->viewType, that.viewType);
std::swap(this->sampleCount, that.sampleCount);
std::swap(this->imageCreateFlags, that.imageCreateFlags);
std::swap(this->syncedTextureParamsVersion, that.syncedTextureParamsVersion);
std::swap(this->syncedContentVersion, that.syncedContentVersion);
std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount);
}
void Reset() {
if (fullView != VK_NULL_HANDLE) {
vkDestroyImageView(s_device, fullView, nullptr);
}
if (sampledView != VK_NULL_HANDLE) {
vkDestroyImageView(s_device, sampledView, nullptr);
}
for (const auto attachmentView : perMipViews) {
if (attachmentView != VK_NULL_HANDLE) {
vkDestroyImageView(s_device, attachmentView, nullptr);
}
}
for (const auto sampledView : perMipSampledViews) {
if (sampledView != VK_NULL_HANDLE) {
vkDestroyImageView(s_device, sampledView, nullptr);
}
}
for (const auto& [_, attachmentView] : attachmentViews) {
if (attachmentView != VK_NULL_HANDLE) {
vkDestroyImageView(s_device, attachmentView, nullptr);
}
}
for (const auto& [_, sampledView] : alternateSampledViews) {
if (sampledView != VK_NULL_HANDLE) {
vkDestroyImageView(s_device, sampledView, nullptr);
}
}
for (const auto& [_, storageImageView] : storageImageViews) {
if (storageImageView != VK_NULL_HANDLE) {
vkDestroyImageView(s_device, storageImageView, nullptr);
}
}
if (image != VK_NULL_HANDLE && allocation != nullptr) {
vmaDestroyImage(s_allocator, image, allocation);
}
fullView = VK_NULL_HANDLE;
sampledView = VK_NULL_HANDLE;
perMipViews.clear();
perMipSampledViews.clear();
attachmentViews.clear();
alternateSampledViews.clear();
storageImageViews.clear();
image = VK_NULL_HANDLE;
allocation = nullptr;
layout = VK_IMAGE_LAYOUT_UNDEFINED;
extent = {0, 0};
depth = 1;
arrayLayers = 1;
mipLevels = 1;
sampledBaseMipLevel = 0;
sampledLevelCount = 1;
format = VK_FORMAT_UNDEFINED;
aspect = VK_IMAGE_ASPECT_NONE;
viewType = VK_IMAGE_VIEW_TYPE_2D;
sampleCount = VK_SAMPLE_COUNT_1_BIT;
imageCreateFlags = 0;
syncedTextureParamsVersion = 0;
syncedContentVersion = 0;
syncedMipLevelCount = 0;
}
~TextureResource() {
@@ -95,21 +266,71 @@ public:
Bool Initialize(const InitInfo& initInfo);
void Shutdown();
void BeginFrame(Uint32 frameIndex);
TextureResource* SyncTextureAndGetDescriptor(
MG_State::GLState::ITextureObject& texture);
VkImageView GetOrCreateViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel);
VkImageView GetOrCreateAttachmentViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel,
Uint32 baseArrayLayer, Uint32 layerCount,
VkImageViewType viewType);
VkImageView GetOrCreateSampledViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel);
VkImageView GetOrCreateSampledImageView(MG_State::GLState::ITextureObject& texture, VkFormat format);
VkImageView GetOrCreateStorageImageView(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel,
VkFormat format, Bool layered, Int32 layer);
void UpdateTrackedImageLayout(MG_State::GLState::ITextureObject* texture, VkImageLayout newLayout);
void UpdateTrackedImageLayoutAfterAttachmentWrite(VkCommandBuffer commandBuffer,
MG_State::GLState::ITextureObject* texture,
Uint32 writtenMipLevel,
VkImageLayout newLayout);
Bool TransitionTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
Bool TransitionTextureForStorageImage(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
// Non-mutating probe for the per-draw storage-image fast path: true when preparing this
// texture as a storage image may need work that is illegal inside a render pass (resource
// creation, dirty-content upload, or a layout transition to GENERAL). Unknown state reports
// true - a false positive merely ends the render pass, a false negative would skip a barrier.
Bool NeedsStorageImagePreparation(MG_State::GLState::ITextureObject& texture) const;
static VkImageAspectFlags ResolveSampledImageViewAspectMask(VkImageAspectFlags imageAspect);
static VkFormat ResolveSampledImageViewFormat(VkFormat imageFormat, SamplerNumericDomain numericDomain);
static Bool AreSampledImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat);
static Bool AreStorageImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat);
static Bool TransitionImageLayout(VkCommandBuffer commandBuffer, VkImage image, VkImageLayout& trackedLayout,
VkImageLayout newLayout, VkPipelineStageFlags srcStageMask,
VkPipelineStageFlags dstStageMask, VkAccessFlags srcAccessMask,
VkAccessFlags dstAccessMask, VkImageAspectFlags aspectMask,
Uint32 baseMipLevel = 0, Uint32 levelCount = 1);
Uint32 baseMipLevel = 0, Uint32 levelCount = 1,
Uint32 layerCount = 1);
SizeT CollectGarbage();
// Per-draw sync memo. Within a single SetupDraw the same sampled texture is
// resolved ~3x (SetupDraw's layout-probe loop, its post-transition loop, and
// again inside ResolveSamplerDescriptor). No GL texture mutation can happen
// mid-SetupDraw, and layout is tracked on the TextureResource independently of
// SyncTexture, so after the first successful sync of a texture in a draw the
// heavy SyncTexture work (mip-completeness/resource/view resync + dirty scan)
// is pure redundancy. BeginDrawSyncScope opens a window in which repeat
// SyncTextureAndGetDescriptor calls short-circuit to the already-synced
// resource; EndDrawSyncScope closes it. Use the RAII DrawSyncScope guard.
void BeginDrawSyncScope();
void EndDrawSyncScope();
// RAII guard that opens/closes a per-draw sync memo window (see above).
class DrawSyncScope {
public:
explicit DrawSyncScope(VkTextureManager& manager) : m_manager(manager) { m_manager.BeginDrawSyncScope(); }
~DrawSyncScope() { m_manager.EndDrawSyncScope(); }
DrawSyncScope(const DrawSyncScope&) = delete;
DrawSyncScope& operator=(const DrawSyncScope&) = delete;
private:
VkTextureManager& m_manager;
};
private:
// Bumped in SyncTextureResource right after vmaCreateImage(texture). See GetTextureImageEpoch().
Uint64 m_textureImageEpoch = 1;
Bool SyncTexture(MG_State::GLState::ITextureObject &texture,
TextureResource &outResource);
@@ -119,7 +340,11 @@ private:
TextureResource &resource);
Bool SyncTextureViews(const MG_State::GLState::ITextureObject& texture, TextureResource& resource);
VkImageView CreateImageView(VkImage image, VkFormat format, VkImageAspectFlags aspect,
Uint32 baseMipLevel, Uint32 levelCount) const;
VkImageViewType viewType, Uint32 baseMipLevel, Uint32 levelCount,
Uint32 baseArrayLayer,
Uint32 layerCount,
const VkComponentMapping* components = nullptr,
VkImageUsageFlags viewUsage = 0) const;
Bool UploadDirtyMipLevels(MG_State::GLState::TextureObjectMipmap &mipmapTexture,
TextureUploadTarget uploadTarget,
TextureResource &outResource);
@@ -132,15 +357,40 @@ private:
static void ResolveViewMipRange(const MG_State::GLState::ITextureObject& texture, Uint32 mipLevels,
Uint32& outBaseMipLevel, Uint32& outLevelCount);
static VkImageAspectFlags GetAspectMaskForFormat(VkFormat format);
void DeferResourceRelease(TextureResource&& resource);
void DeferViewRelease(VkImageView view);
void CollectDeferredReleases(Uint32 frameIndex);
void DestroyDeferredReleases();
static TextureIdentity MakeTextureIdentity(MG_State::GLState::ITextureObject* texture);
void EraseTrackedTexture(const TextureIdentity& identity);
void PruneStaleTextureAliases(MG_State::GLState::ITextureObject* texture);
VkDevice m_device = VK_NULL_HANDLE;
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
VmaAllocator m_allocator = nullptr;
VkCommandPool m_commandPool = VK_NULL_HANDLE;
VkQueue m_graphicsQueue = VK_NULL_HANDLE;
Uint32 m_currentFrameIndex = 0;
Uint8 m_gcCounter = 0;
UnorderedMap<MG_State::GLState::ITextureObject*, WeakPtr<MG_State::GLState::ITextureObject>> m_aliveObjects;
UnorderedMap<MG_State::GLState::ITextureObject*, TextureResource> m_textureResources;
// Active only between BeginDrawSyncScope/EndDrawSyncScope; identities of
// textures already fully synced in the current draw (small N -> flat scan).
Bool m_drawSyncScopeActive = false;
// Per-draw sync memo: the identity plus the resolved resource pointer. The pointer is stable
// across rehash in the node-based m_textureResources and stays valid for the draw (a texture
// synced this draw is alive and is not erased mid-draw), so a repeat sync of the same texture
// returns the resource without re-hashing the identity into m_textureResources.
struct DrawSyncedTexture {
TextureIdentity identity;
TextureResource* resource = nullptr;
};
Vector<DrawSyncedTexture> m_drawSyncedThisDraw;
// Formats whose mutable-image probe failed on this device; their images are created
// without MUTABLE_FORMAT_BIT so repeat syncs neither re-probe nor flag-mismatch.
std::unordered_set<VkFormat> m_mutableFormatUnsupported;
std::unordered_map<TextureIdentity, WeakPtr<MG_State::GLState::ITextureObject>, TextureIdentityHash> m_aliveObjects;
std::unordered_map<TextureIdentity, TextureResource, TextureIdentityHash> m_textureResources;
Vector<Vector<TextureResource>> m_deferredReleases;
Vector<Vector<VkImageView>> m_deferredViewReleases;
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -0,0 +1,179 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/VkTimerQueryManager.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 "VkTimerQueryManager.h"
namespace MobileGL::MG_Backend::DirectVulkan {
Bool VkTimerQueryManager::Initialize(const InitInfo& initInfo) {
Shutdown();
MOBILEGL_ASSERT(initInfo.device != VK_NULL_HANDLE, "VkTimerQueryManager::Initialize requires valid VkDevice");
MOBILEGL_ASSERT(initInfo.frameCount > 0, "VkTimerQueryManager::Initialize requires non-zero frame count");
if (initInfo.timestampValidBits == 0 || initInfo.timestampPeriodNs <= 0.0f || initInfo.slotsPerPool == 0) {
MGLOG_W("VkTimerQueryManager: timestamps unsupported (validBits=%u, period=%f, slots=%u)",
initInfo.timestampValidBits, initInfo.timestampPeriodNs, initInfo.slotsPerPool);
return false;
}
m_device = initInfo.device;
m_timestampPeriodNs = initInfo.timestampPeriodNs;
m_validBitsMask = initInfo.timestampValidBits >= 64
? ~0ull
: ((1ull << initInfo.timestampValidBits) - 1ull);
m_slotsPerPool = initInfo.slotsPerPool;
m_pools.resize(initInfo.frameCount);
VkQueryPoolCreateInfo poolInfo{};
poolInfo.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO;
poolInfo.queryType = VK_QUERY_TYPE_TIMESTAMP;
poolInfo.queryCount = m_slotsPerPool;
for (auto& poolState : m_pools) {
const VkResult result = vkCreateQueryPool(m_device, &poolInfo, nullptr, &poolState.pool);
if (result != VK_SUCCESS) {
MGLOG_E("VkTimerQueryManager: vkCreateQueryPool failed with %s", VkResultToString(result));
Shutdown();
return false;
}
}
return true;
}
void VkTimerQueryManager::Shutdown() {
if (m_device != VK_NULL_HANDLE) {
for (auto& poolState : m_pools) {
if (poolState.pool != VK_NULL_HANDLE) {
vkDestroyQueryPool(m_device, poolState.pool, nullptr);
}
}
}
// Records the frontend still holds simply stay unharvested; their
// results read back as 0.
m_pools.clear();
m_device = VK_NULL_HANDLE;
m_timestampPeriodNs = 0.0f;
m_validBitsMask = 0;
m_slotsPerPool = 0;
}
void VkTimerQueryManager::OnFrameCommandRecordingBegan(VkCommandBuffer commandBuffer, Uint32 frameIndex,
Uint64 frameSerial) {
MOBILEGL_ASSERT(frameIndex < m_pools.size(), "VkTimerQueryManager frame index out of range");
auto& poolState = m_pools[frameIndex];
if (poolState.preparedFrameSerial == frameSerial) {
// Recording re-began within the same frame (mid-frame readback
// submit or the Present layout transition); the pool was already
// harvested and reset for this cycle, and resetting again would
// clobber timestamps written earlier in the frame.
return;
}
// Harvest what the pool's previous cycle left behind. The frame slot's
// fence was waited before re-recording, so every executed query is
// already available and the reads return immediately.
DrainPoolPending(poolState);
vkCmdResetQueryPool(commandBuffer, poolState.pool, 0, m_slotsPerPool);
poolState.cursor = 0;
poolState.exhaustionWarned = false;
poolState.preparedFrameSerial = frameSerial;
}
SharedPtr<VkTimerQueryManager::TimestampRecord> VkTimerQueryManager::WriteTimestamp(VkCommandBuffer commandBuffer,
Uint32 frameIndex,
Uint64 frameSerial) {
MOBILEGL_ASSERT(frameIndex < m_pools.size(), "VkTimerQueryManager frame index out of range");
auto& poolState = m_pools[frameIndex];
if (poolState.cursor >= m_slotsPerPool) {
if (!poolState.exhaustionWarned) {
MGLOG_W("VkTimerQueryManager: frame %u timestamp pool exhausted (%u slots); further timer queries "
"this frame fall back to the frontend path",
frameIndex, m_slotsPerPool);
poolState.exhaustionWarned = true;
}
return nullptr;
}
auto record = MakeShared<TimestampRecord>();
record->poolIndex = frameIndex;
record->slot = poolState.cursor++;
record->frameSerial = frameSerial;
vkCmdWriteTimestamp(commandBuffer, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, poolState.pool, record->slot);
poolState.pendingRecords.push_back(record);
return record;
}
Bool VkTimerQueryManager::TryHarvest(TimestampRecord& record) {
if (record.harvested) {
return true;
}
if (m_device == VK_NULL_HANDLE || record.poolIndex >= m_pools.size()) {
return false;
}
Uint64 resultWithAvailability[2] = {0, 0};
const VkResult result = vkGetQueryPoolResults(
m_device, m_pools[record.poolIndex].pool, record.slot, 1, sizeof(resultWithAvailability),
resultWithAvailability, sizeof(Uint64), VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WITH_AVAILABILITY_BIT);
if (result != VK_SUCCESS && result != VK_NOT_READY) {
MGLOG_E("VkTimerQueryManager: vkGetQueryPoolResults failed with %s", VkResultToString(result));
return false;
}
if (resultWithAvailability[1] == 0) {
return false;
}
record.rawTicks = resultWithAvailability[0];
record.harvested = true;
return true;
}
void VkTimerQueryManager::InvalidatePendingRecords() {
for (auto& poolState : m_pools) {
DrainPoolPending(poolState);
// Force a harvest-free reset cycle the next time this pool's frame
// begins recording.
poolState.preparedFrameSerial = 0;
}
}
void VkTimerQueryManager::DrainPoolPending(PoolState& poolState) {
for (auto& record : poolState.pendingRecords) {
if (record->harvested) {
continue;
}
if (!TryHarvest(*record)) {
// The commands carrying this timestamp never executed (they
// were dropped, e.g. by a swapchain recreation mid-frame).
// Mark the record resolved-as-invalid so waits on it cannot
// hang; its result reads back as 0.
record->harvested = true;
record->valid = false;
}
}
poolState.pendingRecords.clear();
}
Uint64 VkTimerQueryManager::MaskToValidBits(Uint64 ticks) const {
return ticks & m_validBitsMask;
}
Uint64 VkTimerQueryManager::ElapsedNs(const TimestampRecord& begin, const TimestampRecord& end) const {
if (!begin.valid || !end.valid) {
return 0;
}
const Uint64 deltaTicks = MaskToValidBits(end.rawTicks - begin.rawTicks);
return static_cast<Uint64>(static_cast<double>(deltaTicks) * static_cast<double>(m_timestampPeriodNs));
}
Uint64 VkTimerQueryManager::TimestampNs(const TimestampRecord& record) const {
if (!record.valid) {
return 0;
}
return static_cast<Uint64>(static_cast<double>(MaskToValidBits(record.rawTicks)) *
static_cast<double>(m_timestampPeriodNs));
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -0,0 +1,111 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/VkTimerQueryManager.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 "../VkIncludes.h"
#include <Includes.h>
namespace MobileGL::MG_Backend::DirectVulkan {
// GPU timestamp storage backing the GL timer-query frontend (GL_TIME_ELAPSED
// spans and GL_TIMESTAMP one-shots): one VkQueryPool of timestamp slots per
// frame in flight.
//
// Per-frame lifecycle: right after a frame slot's command buffer begins
// recording (and before any render pass, since vkCmdResetQueryPool must be
// recorded outside one), OnFrameCommandRecordingBegan harvests every
// not-yet-read slot of the pool about to be reused (the slot's frame fence
// was waited before re-recording, so the results are already available),
// records a reset of the whole pool, and rewinds the allocation cursor.
class VkTimerQueryManager {
public:
// One vkCmdWriteTimestamp landing spot. Shared (via SharedPtr) between
// the frontend-held query object and the owning pool's pending list, so
// deleting a query while its result is still in flight never leaves the
// pool with a dangling record.
struct TimestampRecord {
Uint32 poolIndex = 0;
Uint32 slot = 0;
// VkBufferManager frame serial current when the timestamp was
// recorded; result availability is bounded by its completion.
Uint64 frameSerial = 0;
Bool harvested = false;
// Cleared when the recorded commands were dropped before they could
// execute (swapchain recreation abandons the in-progress command
// buffer); the result then reads back as 0.
Bool valid = true;
Uint64 rawTicks = 0;
};
struct InitInfo {
VkDevice device = VK_NULL_HANDLE;
Uint32 frameCount = 0;
Uint32 timestampValidBits = 0;
Float timestampPeriodNs = 0.0f; // nanoseconds per timestamp tick
Uint32 slotsPerPool = 128;
};
Bool Initialize(const InitInfo& initInfo);
// The caller guarantees the device is idle (same contract as the other
// DirectVulkan managers' Shutdown paths).
void Shutdown();
// The per-frame hook described in the class comment. Re-begins within
// the same frame serial (mid-frame readback submits, the Present layout
// transition) are skipped so already-written slots survive.
void OnFrameCommandRecordingBegan(VkCommandBuffer commandBuffer, Uint32 frameIndex, Uint64 frameSerial);
// Allocates a slot from the frame's pool and records a bottom-of-pipe
// vkCmdWriteTimestamp (valid both inside and outside a render pass).
// Returns null on pool exhaustion, with one warning per pool cycle; the
// frontend falls back gracefully on a null handle.
SharedPtr<TimestampRecord> WriteTimestamp(VkCommandBuffer commandBuffer, Uint32 frameIndex,
Uint64 frameSerial);
// Non-blocking single-slot read (WITH_AVAILABILITY, no WAIT). Returns
// true once the record holds its raw ticks. Callers gate this on the
// record's frame serial being complete.
Bool TryHarvest(TimestampRecord& record);
// Reads every pending result that is available (the caller guarantees
// the device is idle) and marks the rest invalid. Called when recorded
// but unsubmitted commands are dropped (swapchain recreation), which
// would otherwise leave slots that never become available. Each pool is
// reset lazily on its next OnFrameCommandRecordingBegan.
void InvalidatePendingRecords();
// end - begin using unsigned wrap arithmetic masked to the queue's
// timestampValidBits, converted to nanoseconds. 0 if either record was
// invalidated.
Uint64 ElapsedNs(const TimestampRecord& begin, const TimestampRecord& end) const;
// Raw GPU timestamp converted to nanoseconds. 0 if invalidated.
Uint64 TimestampNs(const TimestampRecord& record) const;
private:
struct PoolState {
VkQueryPool pool = VK_NULL_HANDLE;
Uint32 cursor = 0;
// Frame serial the pool was last harvested + reset for; guards
// against double resets when recording re-begins mid-frame.
Uint64 preparedFrameSerial = 0;
Bool exhaustionWarned = false;
Vector<SharedPtr<TimestampRecord>> pendingRecords;
};
Uint64 MaskToValidBits(Uint64 ticks) const;
// Harvest (or invalidate, when the result never became available)
// every pending record of a pool and clear its pending list.
void DrainPoolPending(PoolState& pool);
VkDevice m_device = VK_NULL_HANDLE;
Float m_timestampPeriodNs = 0.0f;
Uint64 m_validBitsMask = 0;
Uint32 m_slotsPerPool = 0;
Vector<PoolState> m_pools;
};
} // namespace MobileGL::MG_Backend::DirectVulkan
File diff suppressed because it is too large Load Diff
@@ -20,6 +20,7 @@
#include "VkRenderPassManager.h"
#include "VkSamplerManager.h"
#include "VkTextureManager.h"
#include "VkTimerQueryManager.h"
#include "MG_Util/Math/VectorTypes.h"
#include <Includes.h>
#include <vk_mem_alloc.h>
@@ -50,6 +51,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 instanceCount = 1;
Uint32 firstVertex = 0;
Uint32 firstInstance = 0;
// Indexed-draw metadata for bounding vertex-stream conversion. baseVertex is the
// draw's base-vertex offset; indexRangeIsExactView is true only when the draw
// fetches exactly the indices its IndexBufferView describes (direct DrawElements;
// multi/indirect forms leave it false because the CPU cannot bound their ranges).
Int32 baseVertex = 0;
Bool indexRangeIsExactView = false;
};
struct DrawIndexedCmdParam {
@@ -86,6 +93,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
DrawIndexedCmdParam* pParams = nullptr;
};
struct MultiDrawCmd {
GLenum mode = GL_TRIANGLES;
Uint32 drawCount = 0;
DrawCmdParam* pParams = nullptr;
};
struct QueueFamilyIndices {
Int32 graphicsFamily = -1;
Int32 presentFamily = -1;
@@ -101,7 +114,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
};
class VulkanRenderer {
class VulkanRenderer : public IBufferCopyCommandProvider, public FrameContext::IRecordingObserver {
public:
VulkanRenderer(NativeWindowType window, const VulkanRendererConfig& cfg = {});
~VulkanRenderer();
@@ -109,24 +122,144 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void Initialize();
void Shutdown();
// IBufferCopyCommandProvider: recording command buffer, outside any
// render pass, for immediate staged buffer copies.
VkCommandBuffer AcquireBufferCopyCommandBuffer() override;
// FrameContext::IRecordingObserver: prepares the frame's timer-query
// pool (harvest + reset) right after the frame command buffer begins
// recording, before any render pass.
void OnFrameCommandRecordingBegan(VkCommandBuffer commandBuffer) override;
Bool SetupDraw(FrameContext::FrameData& frame, GLenum mode, Flags<DrawSetupAspect> aspects,
const DrawCmdParam& drawParams,
const IndexBufferView* pIndexBufferView = nullptr);
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);
void ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value);
void ClearBufferiv(GLenum buffer, GLint drawbuffer, const GLint* value);
void ClearNamedFramebufferfv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
GLenum buffer, GLint drawbuffer, const GLfloat* value);
void ClearNamedFramebufferfi(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
GLbitfield mask, GLenum filter);
void BlitNamedFramebuffer(const SharedPtr<MG_State::GLState::FramebufferObject>& readFbo,
const SharedPtr<MG_State::GLState::FramebufferObject>& drawFbo,
GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
GLbitfield mask, GLenum filter);
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset,
GLint x, GLint y, GLsizei width, GLsizei height);
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
void GenerateMipmap(GLenum target);
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels);
static SizeT GetReadbackTexelSize(VkFormat sourceFormat);
static Bool ConvertReadbackPixels(const Uint8* sourcePixels, VkFormat sourceFormat,
GLsizei width, GLsizei height, GLenum destinationFormat,
GLenum destinationType, SizeT destinationRowStride,
Uint8* destinationPixels);
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels);
void GetTextureImage(const SharedPtr<MG_State::GLState::ITextureObject>& texture,
TextureUploadTarget uploadTarget, GLint level, GLenum format, GLenum type,
GLsizei bufSize, GLvoid* pixels);
void DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ);
void DispatchComputeIndirect(GLintptr indirect);
void MemoryBarrier(GLbitfield barriers);
static VkMemoryBarrier BuildMemoryBarrierForGlBarriers(GLbitfield barriers);
void DrawArrays(const DrawCmd& payload);
void DrawElements(const DrawIndexedCmd& payload);
void MultiDrawArrays(const MultiDrawCmd& payload);
void MultiDrawElements(const MultiDrawIndexedCmd& payloads);
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount,
GLsizei stride);
void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride);
void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount,
GLsizei maxdrawcount, GLsizei stride);
void Present();
const PhysicalDevice& GetPhysicalDevice() const;
VkInstance GetInstance() const;
Bool IsDrawIndirectCountExtensionEnabled() const;
void RecreateSwapchain();
// GL fence support, expressed in queue-submission indices backed by
// real VkFences. A GL fence captures GetSyncPointSubmitIndex() at
// creation: the index of the submission that will carry the commands
// recorded so far (m_submitCounter + 1 while work is pending, or
// m_submitCounter when nothing has been recorded since the last
// submit). It is signaled once that submission's fence is observed
// signaled - unlike the frame-serial heuristic, this makes fences
// signal as soon as the GPU actually finishes, which MC 1.21.5's
// fence-paced ring buffers rely on to recycle their space.
Uint64 GetSyncPointSubmitIndex() const;
// Non-blocking: polls outstanding submission fences and reports
// whether every submission up to `submitIndex` has completed.
Bool IsSubmitIndexComplete(Uint64 submitIndex);
// Submits the commands recorded so far without waiting (GL flush).
// Recording restarts lazily on a fresh command buffer; the submitted
// one is retired until the frame slot's fence is next waited. Returns
// true when a submission was made.
Bool FlushPendingCommands();
// Flush gated on usefulness: only flushes when `submitIndex` is still
// unsubmitted, so poll loops on already-submitted fences do not split
// the frame's render pass (a full tile load/store on TBDR GPUs).
Bool FlushForSyncPoint(Uint64 submitIndex);
// Blocking wait for a submission index with a nanosecond timeout.
// When the index is still unsubmitted and flushIfPending is set, the
// pending commands are flushed first so the wait can make progress.
Bool WaitForSubmitIndex(Uint64 submitIndex, Uint64 timeoutNs, Bool flushIfPending);
// Frame-serial completion, still used by the timer-query paths (their
// records are bucketed per frame slot).
Bool IsFrameSerialComplete(Uint64 serial) const;
// Blocking wait for a submitted serial. Returns false when the serial
// cannot complete without further submissions (it belongs to the
// current, not-yet-presented frame) or when the wait failed.
Bool WaitForFrameSerial(Uint64 serial, Uint64 timeoutNs);
// GPU timer queries, backing the GL_TIME_ELAPSED / GL_TIMESTAMP
// 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.
SharedPtr<VkTimerQueryManager::TimestampRecord> WriteTimerQueryTimestamp();
// Non-blocking: true once the record's raw ticks are on the CPU
// (harvests the slot once its frame serial has completed).
Bool IsTimerQueryResultReady(VkTimerQueryManager::TimestampRecord& record);
// Blocking wait, mirroring ClientWaitSync's caveat: a record written
// this frame cannot complete until Present submits the commands, so
// this returns false (result reads as 0) instead of deadlocking.
Bool WaitForTimerQueryResult(VkTimerQueryManager::TimestampRecord& record);
Uint64 GetTimerQueryElapsedNs(const VkTimerQueryManager::TimestampRecord& begin,
const VkTimerQueryManager::TimestampRecord& end) const;
Uint64 GetTimerQueryTimestampNs(const VkTimerQueryManager::TimestampRecord& record) const;
void RequestSwapchainResize(Uint32 width, Uint32 height);
// Returns false when the surface is zero-area (minimized/hidden window):
// no new swapchain is installed and presentation must stay suspended.
Bool RecreateSwapchain();
private:
struct BlitUniformData {
@@ -146,8 +279,88 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 samplerBinding = 0;
};
struct DepthMipmapResources {
SharedPtr<MG_State::GLState::ProgramObject> program;
Int srcRectLocation = -1;
Int dstRectLocation = -1;
Int surfaceTransformLocation = -1;
Int srcTexelSizeLocation = -1;
Uint32 samplerBinding = 0;
};
struct DeferredDepthMipmapCleanup {
Vector<VkImageView> imageViews;
Vector<VkFramebuffer> framebuffers;
Vector<VkRenderPass> renderPasses;
Vector<VkPipeline> pipelines;
};
void QueueClearBufferPayload(GLenum buffer, GLint drawbuffer, const ClearAttachmentPayload& clearPayload);
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
// order. Present/readback submissions reference the frame slot's
// fence (not pool-owned); mid-frame flushes use pooled fences that are
// recycled once their submission is observed complete.
// Not thread-safe: like the rest of the renderer, the tracker relies
// on GL calls being serialized (launchers migrate the context across
// threads, but calls never run concurrently), so sync-object polls
// may mutate it without locking.
struct SubmitRecord {
Uint64 submitIndex = 0;
// Buffer-manager frame serial the submission was made under; its
// completion raises the completed-serial floor (timer queries and
// buffer busy-tracking live in frame-serial space).
Uint64 frameSerial = 0;
VkFence fence = VK_NULL_HANDLE;
Bool pooledFence = false;
};
// Registers a submission that vkQueueSubmit just made with `fence`.
// Invariant: every graphics-queue submission that outlives its call
// site must be registered so GL fences observe it. Exempt are the
// texture-upload/preserve submits in VkTextureManager, which
// vkWaitForFences inline before returning.
void RegisterSubmit(VkFence fence, Bool pooledFence);
// Builds the submit packet for the frame's pending command buffer
// (consuming the acquire semaphore on the slot's first submission),
// submits it with `fence`, and registers the submission. On failure
// the frame state is left untouched. Shared by the mid-frame flush
// and the readback path so the semaphore-consumption invariant lives
// in one place.
Bool SubmitPendingCommandBuffer(FrameContext::FrameData& frame, VkFence fence, Bool pooledFence);
// Polls in-flight submission fences (prefix order) and advances the
// completed counter past every fence observed signaled.
void RefreshCompletedSubmits();
// All submissions up to `submitIndex` are known complete (their fence
// was waited or the device was idled); drops their records and
// recycles pooled fences.
void OnSubmitsCompletedUpTo(Uint64 submitIndex);
VkFence AcquirePooledSubmitFence();
void DestroySubmitFencePool();
Bool HasPendingRecordedWork() const;
Vector<SubmitRecord> m_inFlightSubmits;
Vector<VkFence> m_freeSubmitFences;
Uint64 m_submitCounter = 0;
Uint64 m_completedSubmitCounter = 0;
NativeWindowType m_window = 0;
void* m_platformDisplay = nullptr;
void* m_platformLibrary = nullptr;
void* m_platformCloseDisplay = nullptr;
VulkanRendererConfig m_config;
Bool m_swapchainResizeRequested = false;
// Presentation is suspended while the window is zero-area (minimized): the
// swapchain is unusable/out of date, so Present drops frames instead of
// submitting on a signaled fence / presenting never-acquired images.
Bool m_presentSuspended = false;
// Vulkan objects
Bool m_validationLayersEnabled = false;
@@ -164,6 +377,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkQueue m_presentQueue = VK_NULL_HANDLE;
Bool m_drawIndirectCountExtensionEnabled = false;
Bool m_indexTypeUint8ExtensionEnabled = false;
Bool m_logicOpFeatureEnabled = false;
Bool m_multiDrawIndirectFeatureEnabled = false;
Bool m_samplerAnisotropyFeatureEnabled = false;
Bool m_shaderDrawParametersExtensionEnabled = false;
Bool m_shaderDrawParametersFeatureEnabled = false;
Bool m_unformattedFloatStorageImagesEnabled = false;
// fillModeNonSolid gates VK_POLYGON_MODE_LINE/_POINT (glPolygonMode); independentBlend gates
// per-draw-buffer color write masks (glColorMaski). Both are cached at device creation and
// drive a runtime fallback when the device lacks them.
Bool m_fillModeNonSolidFeatureEnabled = false;
Bool m_independentBlendFeatureEnabled = false;
// dualSrcBlend gates GL_SRC1_* blend factors (glBindFragDataLocationIndexed dual-source blend);
// primitiveTopologyListRestart gates primitive restart on *list* topologies (strip/fan restart
// needs no feature). Both cached at device creation and drive a hard-fail-at-draw when absent.
Bool m_dualSrcBlendFeatureEnabled = false;
Bool m_primitiveTopologyListRestartFeatureEnabled = false;
// Cached at device creation from the graphics queue family properties
// and device limits; drives timer-query support.
Uint32 m_timestampValidBits = 0;
Float m_timestampPeriodNs = 0.0f;
Bool m_timerQuerySupported = false;
using PFNDrawIndexedIndirectCountFunc = void(VKAPI_PTR*)(VkCommandBuffer commandBuffer, VkBuffer buffer,
VkDeviceSize offset, VkBuffer countBuffer,
VkDeviceSize countBufferOffset, Uint32 maxDrawCount,
@@ -173,12 +407,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkCommandPool m_commandPool = VK_NULL_HANDLE;
VkBufferManager m_bufferManager;
Vector<const MG_State::GLState::BufferObject*> m_transientVertexIndexBuffersThisFrame;
Uint m_imageIndexAcquired = 0;
FrameContext m_frameContext;
UniquePtr<PipelineFactory> m_pipelineFactory;
// Single-slot "last pipeline" memo: skip the per-draw GetOrCreatePipeline work (state
// gather + synthetic vertex-input rebuild + payload hash + lookup) when the full pipeline
// state is unchanged from the previous draw. The key provably covers every pipeline field.
// Reset per-frame and on pipeline destruction so the cached handle can never dangle.
Bool m_lastPipelineValid = false;
GLenum m_lastPipelineMode = 0;
Uint64 m_lastPipelineProgramHash = 0;
Uint64 m_lastPipelineVertexInputHash = 0;
Uint64 m_lastPipelineRenderPassHash = 0;
Uint m_lastPipelineRenderStateVersion = 0;
ProgramFactory::CompileOptionFlags m_lastPipelineTransformFlags = {};
VkPipeline m_lastPipelineResult = VK_NULL_HANDLE;
UnorderedMap<ProgramFactory::HashType, VkPipeline> m_computePipelines;
UniquePtr<ProgramFactory> m_programFactory;
UniquePtr<UniformManager> m_uniformManager;
UniquePtr<VertexInputStateFactory> m_vertexInputStateFactory;
@@ -186,7 +432,87 @@ namespace MobileGL::MG_Backend::DirectVulkan {
UniquePtr<VkRenderPassManager> m_renderPassManager;
UniquePtr<VkTextureManager> m_textureManager;
UniquePtr<VkSamplerManager> m_samplerManager;
UniquePtr<VkTimerQueryManager> m_timerQueryManager;
BlitResources m_blitResources;
DepthMipmapResources m_depthMipmapResources;
Vector<DeferredDepthMipmapCleanup> m_deferredDepthMipmapCleanup;
// Skip the per-draw CollectSampledTextures walk (~5% of the render thread) when the sampled
// texture SET is provably unchanged from the previous draw: same program (lifetime id +
// backend-state version, which covers sampler-uniform reassignment / relink) and transform
// flags, and no texture bind/unbind/delete since (GetTextureBindGeneration). On a hit,
// m_sampledTexturesScratch still holds the previous draw's list and steps 2-4 (feedback /
// layout probe / transition) re-run on it, so layout correctness is unaffected - only the GL
// walk is skipped. The program lifetime id (never reused, unlike the GL name) and the
// monotonic bind generation make the key ABA-proof; the per-command-buffer reset is a cheap
// belt-and-suspenders.
Bool m_lastSampledSetValid = false;
Uint64 m_lastSampledSetProgramLifetimeId = 0;
Uint32 m_lastSampledSetProgramVersion = 0;
ProgramFactory::CompileOptionFlags m_lastSampledSetTransformFlags = {};
Uint64 m_lastSampledSetBindGeneration = 0;
// Per-draw scratch buffers (clear keeps capacity) — these paths run for every
// draw call and must not allocate.
Vector<MG_State::GLState::ITextureObject*> m_sampledTexturesScratch;
Vector<MG_State::GLState::ITextureObject*> m_storageImageTexturesScratch;
Vector<VkBuffer> m_vertexBuffersScratch;
Vector<VkDeviceSize> m_vertexOffsetsScratch;
Vector<VkVertexInputAttributeDescription> m_patchedAttributesScratch;
Vector<Float> m_vertexConversionScratch;
Vector<Uint8> m_vertexRepackScratch;
struct ConvertedVertexStreamKey {
const MG_State::GLState::BufferObject* buffer = nullptr;
Uint64 changeSerial = 0;
SizeT baseOffset = 0;
Uint32 sourceStride = 0;
DataType type = DataType::Float32;
Int size = 0;
Bool normalized = false;
Bool isInteger = false;
VertexInputStateFactory::VertexStreamConversion conversion =
VertexInputStateFactory::VertexStreamConversion::None;
Bool operator==(const ConvertedVertexStreamKey& other) const {
return buffer == other.buffer && changeSerial == other.changeSerial &&
baseOffset == other.baseOffset && sourceStride == other.sourceStride &&
type == other.type && size == other.size && normalized == other.normalized &&
isInteger == other.isInteger && conversion == other.conversion;
}
};
struct ConvertedVertexStreamKeyHash {
SizeT operator()(const ConvertedVertexStreamKey& key) const {
SizeT hash = std::hash<const void*>{}(key.buffer);
auto combine = [&hash](SizeT value) {
hash ^= value + static_cast<SizeT>(0x9e3779b97f4a7c15ull) + (hash << 6) + (hash >> 2);
};
combine(std::hash<Uint64>{}(key.changeSerial));
combine(std::hash<SizeT>{}(key.baseOffset));
combine(std::hash<Uint32>{}(key.sourceStride));
combine(std::hash<Uint32>{}(static_cast<Uint32>(key.type)));
combine(std::hash<Int>{}(key.size));
combine(std::hash<Bool>{}(key.normalized));
combine(std::hash<Bool>{}(key.isInteger));
combine(std::hash<Uint32>{}(static_cast<Uint32>(key.conversion)));
return hash;
}
};
struct ConvertedVertexStream {
BufferSlice slice;
// Number of source elements the cached slice covers. A draw needing a prefix of
// this range reuses the slice (converted streams are tightly packed); a draw
// needing more reconverts and replaces the entry, so per (buffer, layout) a
// frame converts at most the largest range any draw asked for.
SizeT elementCount = 0;
// Pins the source buffer for the frame so its heap address cannot be reused by
// a new BufferObject while this pointer-keyed entry is alive.
SharedPtr<const MG_State::GLState::BufferObject> sourcePin;
};
UnorderedMap<ConvertedVertexStreamKey, ConvertedVertexStream, ConvertedVertexStreamKeyHash>
m_convertedVertexStreams;
void CreateInstance();
VkResult SetupDebugMessenger();
@@ -203,15 +529,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkPipeline GetOrCreatePipeline(
GLenum mode,
const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj,
ProgramFactory::CompileOptionFlags transformFlags,
const MG_State::GLState::VertexArrayObject& vao,
const RenderPassEntry& renderPassEntry);
VkPipeline GetOrCreateComputePipeline(const ProgramFactory::VkProgramObject& programObj);
void DestroyComputePipelines();
Bool PrepareStorageImageTextures(
VkCommandBuffer commandBuffer,
const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj);
Bool UploadAndBindVertexBuffers(VkCommandBuffer commandBuffer, const MG_State::GLState::VertexArrayObject& vao);
Bool UploadAndBindVertexBuffers(VkCommandBuffer commandBuffer, const MG_State::GLState::VertexArrayObject& vao,
const ProgramFactory::VkProgramObject& programObj,
const DrawCmdParam& drawParams,
const IndexBufferView* pIndexBufferView);
Bool UploadAndBindIndexBuffer(FrameContext::FrameData& frame,
const MG_State::GLState::VertexArrayObject& vao,
const IndexBufferView* pIndexBufferView = nullptr);
Bool InitializeBlitResources();
Bool InitializeDepthMipmapResources();
void ShutdownBlitResources();
void ShutdownDepthMipmapResources();
void CollectDeferredDepthMipmapCleanup(Uint32 frameIndex);
void DestroyDeferredDepthMipmapCleanup();
Bool TryBlitToDefaultFramebufferWithShader(FrameContext::FrameData& frame,
MG_State::GLState::FramebufferObject& readFbo,
MG_State::GLState::FramebufferObject& drawFbo,
@@ -220,7 +561,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
GLenum filter);
Bool MaterializePendingClearForTexture(VkCommandBuffer commandBuffer,
MG_State::GLState::ITextureObject& texture);
Bool MaterializePendingClearForRenderbuffer(
VkCommandBuffer commandBuffer,
const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer);
VkPipeline GetOrCreateBlitPipeline(const RenderPassEntry& renderPassEntry);
Bool GenerateDepthMipmapWithShader(FrameContext::FrameData& frame,
MG_State::GLState::ITextureObject& texture,
VkTextureManager::TextureResource& resource,
Uint32 baseMipLevel,
Uint32 generateMipLevelCount,
const IntVec3& storageBaseTexelSize,
VkImageLayout originalLayout,
VkImageLayout finalLayout);
Bool SubmitReadbackCommandsAndWait(FrameContext::FrameData& frame);
void ShutdownSwapchain();
+49 -4
View File
@@ -10,16 +10,61 @@
#include "VulkanRendererConfig.h"
#define ENUM_STR_CASE(c) case c: return #c;
namespace MobileGL::MG_Backend::DirectVulkan {
inline const char* VkResultToString(VkResult result) {
switch (result) {
ENUM_STR_CASE(VK_SUCCESS)
ENUM_STR_CASE(VK_NOT_READY)
ENUM_STR_CASE(VK_TIMEOUT)
ENUM_STR_CASE(VK_EVENT_SET)
ENUM_STR_CASE(VK_EVENT_RESET)
ENUM_STR_CASE(VK_INCOMPLETE)
ENUM_STR_CASE(VK_ERROR_OUT_OF_HOST_MEMORY)
ENUM_STR_CASE(VK_ERROR_OUT_OF_DEVICE_MEMORY)
ENUM_STR_CASE(VK_ERROR_INITIALIZATION_FAILED)
ENUM_STR_CASE(VK_ERROR_DEVICE_LOST)
ENUM_STR_CASE(VK_ERROR_MEMORY_MAP_FAILED)
ENUM_STR_CASE(VK_ERROR_LAYER_NOT_PRESENT)
ENUM_STR_CASE(VK_ERROR_EXTENSION_NOT_PRESENT)
ENUM_STR_CASE(VK_ERROR_FEATURE_NOT_PRESENT)
ENUM_STR_CASE(VK_ERROR_INCOMPATIBLE_DRIVER)
ENUM_STR_CASE(VK_ERROR_TOO_MANY_OBJECTS)
ENUM_STR_CASE(VK_ERROR_FORMAT_NOT_SUPPORTED)
ENUM_STR_CASE(VK_ERROR_FRAGMENTED_POOL)
ENUM_STR_CASE(VK_ERROR_UNKNOWN)
ENUM_STR_CASE(VK_ERROR_OUT_OF_POOL_MEMORY)
ENUM_STR_CASE(VK_ERROR_INVALID_EXTERNAL_HANDLE)
ENUM_STR_CASE(VK_ERROR_FRAGMENTATION)
ENUM_STR_CASE(VK_ERROR_INVALID_OPAQUE_CAPTURE_ADDRESS)
ENUM_STR_CASE(VK_PIPELINE_COMPILE_REQUIRED)
ENUM_STR_CASE(VK_ERROR_SURFACE_LOST_KHR)
ENUM_STR_CASE(VK_ERROR_NATIVE_WINDOW_IN_USE_KHR)
ENUM_STR_CASE(VK_SUBOPTIMAL_KHR)
ENUM_STR_CASE(VK_ERROR_OUT_OF_DATE_KHR)
ENUM_STR_CASE(VK_ERROR_INCOMPATIBLE_DISPLAY_KHR)
ENUM_STR_CASE(VK_ERROR_VALIDATION_FAILED_EXT)
ENUM_STR_CASE(VK_ERROR_INVALID_SHADER_NV)
default:
return "VK_RESULT_UNKNOWN";
}
}
} // namespace MobileGL::MG_Backend::DirectVulkan
#define VK_VERIFY(expr, ...) \
do { \
VkResult _vk_verify_result = (expr); \
MOBILEGL_ASSERT(_vk_verify_result == VK_SUCCESS, "Vulkan error %d at %s:%d" __VA_OPT__(" - ") __VA_ARGS__, _vk_verify_result, __FILE__, __LINE__); \
if (_vk_verify_result != VK_SUCCESS) { \
MGLOG_F("Vulkan error %s (%d) at %s:%d" __VA_OPT__(" - ") __VA_ARGS__, \
MobileGL::MG_Backend::DirectVulkan::VkResultToString(_vk_verify_result), \
_vk_verify_result, __FILE__, __LINE__); \
} \
MOBILEGL_ASSERT(_vk_verify_result == VK_SUCCESS, "Vulkan error %s (%d) at %s:%d" __VA_OPT__(" - ") __VA_ARGS__, MobileGL::MG_Backend::DirectVulkan::VkResultToString(_vk_verify_result), _vk_verify_result, __FILE__, __LINE__); \
} while (0)
#define ENUM_STR_CASE(c) case c: return #c;
#define XXHASH_VERIFY(expr, ...) \
do { \
XXH_errorcode _xxh_verify_result = (expr); \
MOBILEGL_ASSERT(_xxh_verify_result == XXH_OK, "XXHash error %d at %s:%d" __VA_OPT__(" - ") __VA_ARGS__, _xxh_verify_result, __FILE__, __LINE__); \
} while (0)
} while (0)
@@ -11,10 +11,22 @@
namespace MobileGL::MG_Backend::DirectVulkan {
struct VulkanRendererConfig {
Uint32 MaxFramesInFlight = 2;
// Fallback CPU pipeline depth used when the MOBILEGL_MAGMA_FRAMESINFLIGHT env var is
// unset/invalid. A deeper pipeline lets the CPU run further ahead of the GPU, hiding
// per-frame GPU-completion latency. Whatever value is chosen (env or this fallback) is
// only a request: VulkanRenderer::Initialize clamps it down to the surface's maxImageCount
// (and never below 2), since not every driver allows that many swapchain images.
Uint32 MaxFramesInFlight = 3;
String AppName = "MobileGL-VulkanRenderer";
MobileGL::Version Version = MG_Config::CoreVersion;
Uint64 CacheVersion = MG_Config::CacheVersion;
Uint32 SurfaceWidth = 1;
Uint32 SurfaceHeight = 1;
Bool DisablePipelineCache = false;
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
Bool EnableValidationLayers = true;
#else
Bool EnableValidationLayers = false;
#endif
};
} // namespace MobileGL::MG_Backend::DirectVulkan
+2 -1
View File
@@ -16,4 +16,5 @@ target_link_libraries(
${LINK_LIBRARIES}
)
add_test(NAME BufferBench COMMAND BufferBench --benchmark_counters_tabular=true)
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)
+2 -1
View File
@@ -16,4 +16,5 @@ target_link_libraries(
${LINK_LIBRARIES}
)
add_test(NAME ProgramBench COMMAND ProgramBench --benchmark_counters_tabular=true)
add_test(NAME ProgramBench COMMAND ProgramBench --benchmark_counters_tabular=true)
set_tests_properties(ProgramBench PROPERTIES LABELS benchmark)
+700
View File
@@ -0,0 +1,700 @@
// MobileGL - MobileGL/MG_Impl/CGLImpl/CGLImpl.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 "CGLImpl.h"
#if defined(__APPLE__)
#include "../EGLImpl/EGLImpl.h"
namespace MobileGL::MG_Impl::CGLImpl {
namespace {
struct PixelFormatObject {
Uint32 RetainCount = 1;
Bool DoubleBuffer = true;
GLint ColorSize = 24;
GLint AlphaSize = 8;
GLint DepthSize = 24;
GLint StencilSize = 8;
GLint SampleBuffers = 0;
GLint Samples = 0;
GLint Profile = kCGLOGLPVersion_3_2_Core;
GLint RendererId = 0x4d474c;
};
struct ContextObject {
Uint32 RetainCount = 1;
CGLPixelFormatObj PixelFormat = nullptr;
CGLContextObj Share = nullptr;
EGLDisplay Display = EGL_NO_DISPLAY;
EGLConfig Config = nullptr;
EGLContext Context = EGL_NO_CONTEXT;
EGLSurface Surface = EGL_NO_SURFACE;
void* NSObject = nullptr;
void* View = nullptr;
void* MetalLayer = nullptr;
GLint SwapInterval = 1;
GLint VirtualScreen = 0;
GLint SurfaceBackingSize[2] = {0, 0};
Bool HasDrawable = false;
Bool Locked = false;
};
std::recursive_mutex& RegistryMutex() {
static auto* mutex = new std::recursive_mutex();
return *mutex;
}
Uint64& NextPixelFormatHandle() {
static auto* handle = new Uint64(1);
return *handle;
}
Uint64& NextContextHandle() {
static auto* handle = new Uint64(1);
return *handle;
}
UnorderedMap<CGLPixelFormatObj, PixelFormatObject>& PixelFormats() {
static auto* formats = new UnorderedMap<CGLPixelFormatObj, PixelFormatObject>();
return *formats;
}
UnorderedMap<CGLContextObj, ContextObject>& Contexts() {
static auto* contexts = new UnorderedMap<CGLContextObj, ContextObject>();
return *contexts;
}
UnorderedMap<std::thread::id, CGLContextObj>& CurrentContexts() {
static auto* contexts = new UnorderedMap<std::thread::id, CGLContextObj>();
return *contexts;
}
CGLPixelFormatObj EncodePixelFormat(Uint64 handle) {
return reinterpret_cast<CGLPixelFormatObj>(static_cast<SizeT>(handle));
}
CGLContextObj EncodeContext(Uint64 handle) {
return reinterpret_cast<CGLContextObj>(static_cast<SizeT>(handle));
}
std::thread::id CurrentThreadKey() {
return std::this_thread::get_id();
}
Bool AttributeHasValue(CGLPixelFormatAttribute attrib) {
switch (attrib) {
case kCGLPFAColorSize:
case kCGLPFAAlphaSize:
case kCGLPFADepthSize:
case kCGLPFAStencilSize:
case kCGLPFASampleBuffers:
case kCGLPFASamples:
case kCGLPFARendererID:
case kCGLPFADisplayMask:
case kCGLPFAOpenGLProfile:
return true;
default:
return false;
}
}
void ApplyPixelFormatAttribute(PixelFormatObject& pixelFormat,
CGLPixelFormatAttribute attrib,
GLint value) {
switch (attrib) {
case kCGLPFADoubleBuffer:
pixelFormat.DoubleBuffer = true;
break;
case kCGLPFAColorSize:
pixelFormat.ColorSize = value;
break;
case kCGLPFAAlphaSize:
pixelFormat.AlphaSize = value;
break;
case kCGLPFADepthSize:
pixelFormat.DepthSize = value;
break;
case kCGLPFAStencilSize:
pixelFormat.StencilSize = value;
break;
case kCGLPFASampleBuffers:
pixelFormat.SampleBuffers = value;
break;
case kCGLPFASamples:
pixelFormat.Samples = value;
break;
case kCGLPFAOpenGLProfile:
pixelFormat.Profile = value;
break;
case kCGLPFARendererID:
pixelFormat.RendererId = value;
break;
default:
break;
}
}
Bool InitEGLContext(ContextObject& object, CGLPixelFormatObj pix, CGLContextObj share) {
auto* pixelFormat = [&]() -> PixelFormatObject* {
auto& pixelFormats = PixelFormats();
auto it = pixelFormats.find(pix);
return it == pixelFormats.end() ? nullptr : &it->second;
}();
if (!pixelFormat) {
return false;
}
EGLDisplay display = EGLImpl::GetDisplay(EGL_DEFAULT_DISPLAY);
if (display == EGL_NO_DISPLAY) {
return false;
}
if (!EGLImpl::Initialize(display, nullptr, nullptr)) {
return false;
}
EGLImpl::BindAPI(EGL_OPENGL_API);
const EGLint attribs[] = {
EGL_RED_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_BLUE_SIZE, 8,
EGL_ALPHA_SIZE, std::max(pixelFormat->AlphaSize, 0),
EGL_DEPTH_SIZE, std::max(pixelFormat->DepthSize, 0),
EGL_STENCIL_SIZE, std::max(pixelFormat->StencilSize, 0),
EGL_SURFACE_TYPE, EGL_WINDOW_BIT | EGL_PBUFFER_BIT,
EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT,
EGL_NONE,
};
EGLConfig config = nullptr;
EGLint count = 0;
if (!EGLImpl::ChooseConfig(display, attribs, &config, 1, &count) || count <= 0) {
return false;
}
EGLContext shareContext = EGL_NO_CONTEXT;
if (share != nullptr) {
auto& contexts = Contexts();
auto shareIt = contexts.find(share);
if (shareIt == contexts.end()) {
return false;
}
shareContext = shareIt->second.Context;
}
const EGLint contextAttribs[] = {
EGL_CONTEXT_MAJOR_VERSION, 3,
EGL_CONTEXT_MINOR_VERSION, 3,
EGL_NONE,
};
EGLContext eglContext = EGLImpl::CreateContext(display, config, shareContext, contextAttribs);
if (eglContext == EGL_NO_CONTEXT) {
return false;
}
object.Display = display;
object.Config = config;
object.Context = eglContext;
object.PixelFormat = pix;
object.Share = share;
return true;
}
ContextObject* TryGetContext(CGLContextObj ctx) {
auto& contexts = Contexts();
auto it = contexts.find(ctx);
return it == contexts.end() ? nullptr : &it->second;
}
const ContextObject* TryGetContext(CGLContextObj ctx, const std::lock_guard<std::recursive_mutex>&) {
auto& contexts = Contexts();
auto it = contexts.find(ctx);
return it == contexts.end() ? nullptr : &it->second;
}
PixelFormatObject* TryGetPixelFormat(CGLPixelFormatObj pix) {
auto& pixelFormats = PixelFormats();
auto it = pixelFormats.find(pix);
return it == pixelFormats.end() ? nullptr : &it->second;
}
CGLError MakeCurrentLocked(CGLContextObj ctx, ContextObject& object) {
CurrentContexts()[CurrentThreadKey()] = ctx;
if (!object.HasDrawable || object.Surface == EGL_NO_SURFACE) {
return kCGLNoError;
}
if (!EGLImpl::MakeCurrent(object.Display, object.Surface, object.Surface, object.Context)) {
return kCGLBadState;
}
return kCGLNoError;
}
CGLError RecreateSurfaceLocked(CGLContextObj ctx, ContextObject& object) {
if (!object.MetalLayer) {
return kCGLBadDrawable;
}
if (object.Surface != EGL_NO_SURFACE) {
EGLImpl::DestroySurface(object.Display, object.Surface);
object.Surface = EGL_NO_SURFACE;
}
const EGLAttrib attribs[] = {
EGL_WIDTH, std::max<GLint>(object.SurfaceBackingSize[0], 1),
EGL_HEIGHT, std::max<GLint>(object.SurfaceBackingSize[1], 1),
EGL_NONE,
};
EGLSurface surface = EGLImpl::CreatePlatformWindowSurface(object.Display, object.Config,
object.MetalLayer, attribs);
if (surface == EGL_NO_SURFACE) {
object.HasDrawable = false;
return kCGLBadDrawable;
}
object.Surface = surface;
object.HasDrawable = true;
return GetCurrentContext() == ctx ? MakeCurrentLocked(ctx, object) : kCGLNoError;
}
CGLError ResizeSurfaceLocked(ContextObject& object) {
if (object.Surface == EGL_NO_SURFACE) {
return kCGLBadDrawable;
}
return EGLImpl::ResizePlatformWindowSurface(
object.Display, object.Surface,
std::max<GLint>(object.SurfaceBackingSize[0], 1),
std::max<GLint>(object.SurfaceBackingSize[1], 1))
? kCGLNoError
: kCGLBadDrawable;
}
} // namespace
CGLError ChoosePixelFormat(const CGLPixelFormatAttribute* attribs, CGLPixelFormatObj* pix, GLint* npix) {
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
if (!pix || !npix) {
return kCGLBadAddress;
}
PixelFormatObject object;
if (attribs) {
for (SizeT i = 0; attribs[i] != static_cast<CGLPixelFormatAttribute>(0); ++i) {
const auto attrib = attribs[i];
GLint value = 1;
if (AttributeHasValue(attrib)) {
value = static_cast<GLint>(attribs[++i]);
}
ApplyPixelFormatAttribute(object, attrib, value);
}
}
const auto handle = EncodePixelFormat(NextPixelFormatHandle()++);
PixelFormats()[handle] = object;
*pix = handle;
*npix = 1;
return kCGLNoError;
}
CGLError DestroyPixelFormat(CGLPixelFormatObj pix) {
ReleasePixelFormat(pix);
return kCGLNoError;
}
CGLError DescribePixelFormat(CGLPixelFormatObj pix, GLint pixNum, CGLPixelFormatAttribute attrib, GLint* value) {
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
if (!value) {
return kCGLBadAddress;
}
if (pixNum != 0 && pixNum != 1) {
return kCGLBadValue;
}
auto* pixelFormat = TryGetPixelFormat(pix);
if (!pixelFormat) {
return kCGLBadPixelFormat;
}
switch (attrib) {
case kCGLPFADoubleBuffer:
*value = pixelFormat->DoubleBuffer ? 1 : 0;
return kCGLNoError;
case kCGLPFAAccelerated:
case kCGLPFAAcceleratedCompute:
case kCGLPFASupportsAutomaticGraphicsSwitching:
*value = 1;
return kCGLNoError;
case kCGLPFAColorSize:
*value = pixelFormat->ColorSize;
return kCGLNoError;
case kCGLPFAAlphaSize:
*value = pixelFormat->AlphaSize;
return kCGLNoError;
case kCGLPFADepthSize:
*value = pixelFormat->DepthSize;
return kCGLNoError;
case kCGLPFAStencilSize:
*value = pixelFormat->StencilSize;
return kCGLNoError;
case kCGLPFASampleBuffers:
*value = pixelFormat->SampleBuffers;
return kCGLNoError;
case kCGLPFASamples:
*value = pixelFormat->Samples;
return kCGLNoError;
case kCGLPFARendererID:
*value = pixelFormat->RendererId;
return kCGLNoError;
case kCGLPFAOpenGLProfile:
*value = pixelFormat->Profile;
return kCGLNoError;
case kCGLPFAVirtualScreenCount:
*value = 1;
return kCGLNoError;
default:
*value = 0;
return kCGLNoError;
}
}
void ReleasePixelFormat(CGLPixelFormatObj pix) {
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
auto* pixelFormat = TryGetPixelFormat(pix);
if (!pixelFormat) {
return;
}
if (pixelFormat->RetainCount > 1) {
--pixelFormat->RetainCount;
return;
}
PixelFormats().erase(pix);
}
CGLPixelFormatObj RetainPixelFormat(CGLPixelFormatObj pix) {
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
auto* pixelFormat = TryGetPixelFormat(pix);
if (pixelFormat) {
++pixelFormat->RetainCount;
}
return pix;
}
GLuint GetPixelFormatRetainCount(CGLPixelFormatObj pix) {
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
auto* pixelFormat = TryGetPixelFormat(pix);
return pixelFormat ? pixelFormat->RetainCount : 0;
}
CGLError CreateContext(CGLPixelFormatObj pix, CGLContextObj share, CGLContextObj* ctx) {
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
if (!ctx) {
return kCGLBadAddress;
}
if (!TryGetPixelFormat(pix)) {
return kCGLBadPixelFormat;
}
if (share && !TryGetContext(share)) {
return kCGLBadMatch;
}
ContextObject object;
if (!InitEGLContext(object, pix, share)) {
return kCGLBadAlloc;
}
RetainPixelFormat(pix);
const auto handle = EncodeContext(NextContextHandle()++);
Contexts()[handle] = object;
*ctx = handle;
return kCGLNoError;
}
CGLError DestroyContext(CGLContextObj ctx) {
ReleaseContext(ctx);
return kCGLNoError;
}
CGLContextObj RetainContext(CGLContextObj ctx) {
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
auto* object = TryGetContext(ctx);
if (object) {
++object->RetainCount;
}
return ctx;
}
void ReleaseContext(CGLContextObj ctx) {
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
auto* object = TryGetContext(ctx);
if (!object) {
return;
}
if (object->RetainCount > 1) {
--object->RetainCount;
return;
}
if (object->Surface != EGL_NO_SURFACE) {
EGLImpl::DestroySurface(object->Display, object->Surface);
}
if (object->Context != EGL_NO_CONTEXT) {
EGLImpl::DestroyContext(object->Display, object->Context);
}
ReleasePixelFormat(object->PixelFormat);
auto& currentContexts = CurrentContexts();
for (auto it = currentContexts.begin(); it != currentContexts.end();) {
if (it->second == ctx) {
it = currentContexts.erase(it);
} else {
++it;
}
}
Contexts().erase(ctx);
}
GLuint GetContextRetainCount(CGLContextObj ctx) {
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
auto* object = TryGetContext(ctx);
return object ? object->RetainCount : 0;
}
CGLPixelFormatObj GetPixelFormat(CGLContextObj ctx) {
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
auto* object = TryGetContext(ctx);
return object ? object->PixelFormat : nullptr;
}
CGLError SetCurrentContext(CGLContextObj ctx) {
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
if (!ctx) {
CurrentContexts().erase(CurrentThreadKey());
EGLImpl::MakeCurrent(EGL_NO_DISPLAY, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
return kCGLNoError;
}
auto* object = TryGetContext(ctx);
if (!object) {
return kCGLBadContext;
}
return MakeCurrentLocked(ctx, *object);
}
CGLContextObj GetCurrentContext() {
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
auto& currentContexts = CurrentContexts();
auto it = currentContexts.find(CurrentThreadKey());
return it == currentContexts.end() ? nullptr : it->second;
}
CGLError SetParameter(CGLContextObj ctx, CGLContextParameter pname, const GLint* params) {
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
auto* object = TryGetContext(ctx);
if (!object) {
return kCGLBadContext;
}
if (!params && pname != kCGLCPReclaimResources) {
return kCGLBadAddress;
}
switch (pname) {
case kCGLCPSwapInterval:
object->SwapInterval = params[0];
EGLImpl::SwapInterval(object->Display, object->SwapInterval);
return kCGLNoError;
case kCGLCPSurfaceBackingSize:
{
const GLint width = std::max<GLint>(params[0], 1);
const GLint height = std::max<GLint>(params[1], 1);
if (object->SurfaceBackingSize[0] == width && object->SurfaceBackingSize[1] == height) {
return kCGLNoError;
}
object->SurfaceBackingSize[0] = width;
object->SurfaceBackingSize[1] = height;
if (object->MetalLayer && object->Surface != EGL_NO_SURFACE) {
return ResizeSurfaceLocked(*object);
}
return kCGLNoError;
}
case kCGLCPSurfaceOpacity:
case kCGLCPSurfaceOrder:
case kCGLCPMPSwapsInFlight:
case kCGLCPReclaimResources:
return kCGLNoError;
default:
return kCGLNoError;
}
}
CGLError GetParameter(CGLContextObj ctx, CGLContextParameter pname, GLint* params) {
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
auto* object = TryGetContext(ctx);
if (!object) {
return kCGLBadContext;
}
if (!params) {
return kCGLBadAddress;
}
switch (pname) {
case kCGLCPSwapInterval:
params[0] = object->SwapInterval;
return kCGLNoError;
case kCGLCPSurfaceBackingSize:
params[0] = object->SurfaceBackingSize[0];
params[1] = object->SurfaceBackingSize[1];
return kCGLNoError;
case kCGLCPCurrentRendererID:
params[0] = 0x4d474c;
return kCGLNoError;
case kCGLCPGPUVertexProcessing:
case kCGLCPGPUFragmentProcessing:
case kCGLCPHasDrawable:
params[0] = object->HasDrawable ? 1 : 0;
return kCGLNoError;
case kCGLCPMPSwapsInFlight:
params[0] = 1;
return kCGLNoError;
default:
params[0] = 0;
return kCGLNoError;
}
}
CGLError UpdateContext(CGLContextObj ctx) {
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
return TryGetContext(ctx) ? kCGLNoError : kCGLBadContext;
}
CGLError ClearDrawable(CGLContextObj ctx) {
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
auto* object = TryGetContext(ctx);
if (!object) {
return kCGLBadContext;
}
if (object->Surface != EGL_NO_SURFACE) {
EGLImpl::DestroySurface(object->Display, object->Surface);
}
object->Surface = EGL_NO_SURFACE;
object->View = nullptr;
object->MetalLayer = nullptr;
object->HasDrawable = false;
return kCGLNoError;
}
CGLError FlushDrawable(CGLContextObj ctx) {
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
auto* object = TryGetContext(ctx);
if (!object) {
return kCGLBadContext;
}
if (!object->HasDrawable || object->Surface == EGL_NO_SURFACE) {
return kCGLBadDrawable;
}
const auto currentError = MakeCurrentLocked(ctx, *object);
if (currentError != kCGLNoError) {
return currentError;
}
return EGLImpl::SwapBuffers(object->Display, object->Surface) ? kCGLNoError : kCGLBadDrawable;
}
CGLError LockContext(CGLContextObj ctx) {
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
auto* object = TryGetContext(ctx);
if (!object) {
return kCGLBadContext;
}
object->Locked = true;
return kCGLNoError;
}
CGLError UnlockContext(CGLContextObj ctx) {
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
auto* object = TryGetContext(ctx);
if (!object) {
return kCGLBadContext;
}
object->Locked = false;
return kCGLNoError;
}
void GetVersion(GLint* majorvers, GLint* minorvers) {
if (majorvers) {
*majorvers = 1;
}
if (minorvers) {
*minorvers = 0;
}
}
const char* ErrorString(CGLError error) {
switch (error) {
case kCGLNoError:
return "no error";
case kCGLBadAttribute:
return "invalid pixel format attribute";
case kCGLBadPixelFormat:
return "invalid pixel format";
case kCGLBadContext:
return "invalid context";
case kCGLBadDrawable:
return "invalid drawable";
case kCGLBadState:
return "invalid context state";
case kCGLBadValue:
return "invalid numerical value";
case kCGLBadMatch:
return "invalid share context";
case kCGLBadAddress:
return "invalid pointer";
case kCGLBadAlloc:
return "invalid memory allocation";
default:
return "unknown CGL error";
}
}
CGLError AttachDrawable(CGLContextObj ctx, void* nsView, void* metalLayer, GLint width, GLint height) {
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
auto* object = TryGetContext(ctx);
if (!object) {
return kCGLBadContext;
}
if (!metalLayer) {
return kCGLBadDrawable;
}
width = std::max<GLint>(width, 1);
height = std::max<GLint>(height, 1);
const Bool sameSize = object->SurfaceBackingSize[0] == width && object->SurfaceBackingSize[1] == height;
object->SurfaceBackingSize[0] = width;
object->SurfaceBackingSize[1] = height;
if (object->Surface != EGL_NO_SURFACE && object->MetalLayer == metalLayer && sameSize) {
object->View = nsView;
object->HasDrawable = true;
return kCGLNoError;
}
if (object->Surface != EGL_NO_SURFACE && object->MetalLayer == metalLayer) {
object->View = nsView;
object->HasDrawable = true;
return ResizeSurfaceLocked(*object);
}
if (object->Surface != EGL_NO_SURFACE) {
EGLImpl::DestroySurface(object->Display, object->Surface);
object->Surface = EGL_NO_SURFACE;
}
object->View = nsView;
object->MetalLayer = metalLayer;
const auto recreateError = RecreateSurfaceLocked(ctx, *object);
if (recreateError != kCGLNoError) {
return recreateError;
}
return kCGLNoError;
}
void* GetContextNSObject(CGLContextObj ctx) {
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
auto* object = TryGetContext(ctx);
return object ? object->NSObject : nullptr;
}
void SetContextNSObject(CGLContextObj ctx, void* nsObject) {
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
auto* object = TryGetContext(ctx);
if (object) {
object->NSObject = nsObject;
}
}
} // namespace MobileGL::MG_Impl::CGLImpl
#endif
+49
View File
@@ -0,0 +1,49 @@
// MobileGL - MobileGL/MG_Impl/CGLImpl/CGLImpl.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>
#if defined(__APPLE__)
#ifndef GL_SILENCE_DEPRECATION
#define GL_SILENCE_DEPRECATION
#endif
#include <OpenGL/OpenGL.h>
namespace MobileGL::MG_Impl::CGLImpl {
CGLError ChoosePixelFormat(const CGLPixelFormatAttribute* attribs, CGLPixelFormatObj* pix, GLint* npix);
CGLError DestroyPixelFormat(CGLPixelFormatObj pix);
CGLError DescribePixelFormat(CGLPixelFormatObj pix, GLint pixNum, CGLPixelFormatAttribute attrib, GLint* value);
void ReleasePixelFormat(CGLPixelFormatObj pix);
CGLPixelFormatObj RetainPixelFormat(CGLPixelFormatObj pix);
GLuint GetPixelFormatRetainCount(CGLPixelFormatObj pix);
CGLError CreateContext(CGLPixelFormatObj pix, CGLContextObj share, CGLContextObj* ctx);
CGLError DestroyContext(CGLContextObj ctx);
CGLContextObj RetainContext(CGLContextObj ctx);
void ReleaseContext(CGLContextObj ctx);
GLuint GetContextRetainCount(CGLContextObj ctx);
CGLPixelFormatObj GetPixelFormat(CGLContextObj ctx);
CGLError SetCurrentContext(CGLContextObj ctx);
CGLContextObj GetCurrentContext();
CGLError SetParameter(CGLContextObj ctx, CGLContextParameter pname, const GLint* params);
CGLError GetParameter(CGLContextObj ctx, CGLContextParameter pname, GLint* params);
CGLError UpdateContext(CGLContextObj ctx);
CGLError ClearDrawable(CGLContextObj ctx);
CGLError FlushDrawable(CGLContextObj ctx);
CGLError LockContext(CGLContextObj ctx);
CGLError UnlockContext(CGLContextObj ctx);
void GetVersion(GLint* majorvers, GLint* minorvers);
const char* ErrorString(CGLError error);
CGLError AttachDrawable(CGLContextObj ctx, void* nsView, void* metalLayer, GLint width, GLint height);
void* GetContextNSObject(CGLContextObj ctx);
void SetContextNSObject(CGLContextObj ctx, void* nsObject);
}
#endif
@@ -0,0 +1,110 @@
// MobileGL - MobileGL/MG_Impl/CGLImpl/Exporting/Definitions.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 "../CGLImpl.h"
#if defined(__APPLE__)
MOBILEGL_CGL_API CGLError CGLChoosePixelFormat(const CGLPixelFormatAttribute* attribs,
CGLPixelFormatObj* pix,
GLint* npix) {
return MobileGL::MG_Impl::CGLImpl::ChoosePixelFormat(attribs, pix, npix);
}
MOBILEGL_CGL_API CGLError CGLDestroyPixelFormat(CGLPixelFormatObj pix) {
return MobileGL::MG_Impl::CGLImpl::DestroyPixelFormat(pix);
}
MOBILEGL_CGL_API CGLError CGLDescribePixelFormat(CGLPixelFormatObj pix,
GLint pix_num,
CGLPixelFormatAttribute attrib,
GLint* value) {
return MobileGL::MG_Impl::CGLImpl::DescribePixelFormat(pix, pix_num, attrib, value);
}
MOBILEGL_CGL_API void CGLReleasePixelFormat(CGLPixelFormatObj pix) {
MobileGL::MG_Impl::CGLImpl::ReleasePixelFormat(pix);
}
MOBILEGL_CGL_API CGLPixelFormatObj CGLRetainPixelFormat(CGLPixelFormatObj pix) {
return MobileGL::MG_Impl::CGLImpl::RetainPixelFormat(pix);
}
MOBILEGL_CGL_API GLuint CGLGetPixelFormatRetainCount(CGLPixelFormatObj pix) {
return MobileGL::MG_Impl::CGLImpl::GetPixelFormatRetainCount(pix);
}
MOBILEGL_CGL_API CGLError CGLCreateContext(CGLPixelFormatObj pix, CGLContextObj share, CGLContextObj* ctx) {
return MobileGL::MG_Impl::CGLImpl::CreateContext(pix, share, ctx);
}
MOBILEGL_CGL_API CGLError CGLDestroyContext(CGLContextObj ctx) {
return MobileGL::MG_Impl::CGLImpl::DestroyContext(ctx);
}
MOBILEGL_CGL_API CGLContextObj CGLRetainContext(CGLContextObj ctx) {
return MobileGL::MG_Impl::CGLImpl::RetainContext(ctx);
}
MOBILEGL_CGL_API void CGLReleaseContext(CGLContextObj ctx) {
MobileGL::MG_Impl::CGLImpl::ReleaseContext(ctx);
}
MOBILEGL_CGL_API GLuint CGLGetContextRetainCount(CGLContextObj ctx) {
return MobileGL::MG_Impl::CGLImpl::GetContextRetainCount(ctx);
}
MOBILEGL_CGL_API CGLPixelFormatObj CGLGetPixelFormat(CGLContextObj ctx) {
return MobileGL::MG_Impl::CGLImpl::GetPixelFormat(ctx);
}
MOBILEGL_CGL_API CGLError CGLSetCurrentContext(CGLContextObj ctx) {
return MobileGL::MG_Impl::CGLImpl::SetCurrentContext(ctx);
}
MOBILEGL_CGL_API CGLContextObj CGLGetCurrentContext(void) {
return MobileGL::MG_Impl::CGLImpl::GetCurrentContext();
}
MOBILEGL_CGL_API CGLError CGLSetParameter(CGLContextObj ctx, CGLContextParameter pname, const GLint* params) {
return MobileGL::MG_Impl::CGLImpl::SetParameter(ctx, pname, params);
}
MOBILEGL_CGL_API CGLError CGLGetParameter(CGLContextObj ctx, CGLContextParameter pname, GLint* params) {
return MobileGL::MG_Impl::CGLImpl::GetParameter(ctx, pname, params);
}
MOBILEGL_CGL_API CGLError CGLUpdateContext(CGLContextObj ctx) {
return MobileGL::MG_Impl::CGLImpl::UpdateContext(ctx);
}
MOBILEGL_CGL_API CGLError CGLClearDrawable(CGLContextObj ctx) {
return MobileGL::MG_Impl::CGLImpl::ClearDrawable(ctx);
}
MOBILEGL_CGL_API CGLError CGLFlushDrawable(CGLContextObj ctx) {
return MobileGL::MG_Impl::CGLImpl::FlushDrawable(ctx);
}
MOBILEGL_CGL_API CGLError CGLLockContext(CGLContextObj ctx) {
return MobileGL::MG_Impl::CGLImpl::LockContext(ctx);
}
MOBILEGL_CGL_API CGLError CGLUnlockContext(CGLContextObj ctx) {
return MobileGL::MG_Impl::CGLImpl::UnlockContext(ctx);
}
MOBILEGL_CGL_API void CGLGetVersion(GLint* majorvers, GLint* minorvers) {
MobileGL::MG_Impl::CGLImpl::GetVersion(majorvers, minorvers);
}
MOBILEGL_CGL_API const char* CGLErrorString(CGLError error) {
return MobileGL::MG_Impl::CGLImpl::ErrorString(error);
}
#endif
@@ -0,0 +1,56 @@
// MobileGL - MobileGL/MG_Impl/DyldInterpose/DyldInterpose.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 <Includes.h>
#if defined(__APPLE__)
#include "MG_Impl/GetProcAddress.h"
#include <dlfcn.h>
namespace {
struct DyldInterposeEntry {
const void* Replacement;
const void* Replacee;
};
bool IsGLProcName(const char* name) {
if (name == nullptr) {
return false;
}
if (strncmp(name, "CGL", 3) == 0) {
return true;
}
if (strncmp(name, "gl", 2) != 0) {
return false;
}
// Avoid stealing glfw*/glib*/glX*/global application symbols.
return name[2] >= 'A' && name[2] <= 'Z' && name[2] != 'X';
}
void* MobileGLDlsym(void* handle, const char* symbol) {
if (IsGLProcName(symbol)) {
if (void* proc = MobileGL::MG_Impl::GetProcAddress(symbol)) {
return proc;
}
}
return dlsym(handle, symbol);
}
__attribute__((used)) static const DyldInterposeEntry kMobileGLDyldInterpose[]
__attribute__((section("__DATA,__interpose"))) = {
{reinterpret_cast<const void*>(MobileGLDlsym), reinterpret_cast<const void*>(dlsym)},
};
} // namespace
#endif
+202 -28
View File
@@ -8,8 +8,11 @@
#include "EGLImpl.h"
#include "../GetProcAddress.h"
#include <Init.h>
#include <MG_Backend/BackendObjects.h>
#include <MG_State/EGLState/Core.h>
#include <mutex>
#include <sstream>
#include <type_traits>
namespace MobileGL::MG_Impl::EGLImpl {
@@ -23,6 +26,17 @@ namespace MobileGL::MG_Impl::EGLImpl {
return MG_State::pEGLContext.get();
}
// Entry points that can legitimately be an application's FIRST EGL
// call (display/proc-address/string queries) lazily bring MobileGL
// up here, so the library needs no static constructor and can
// re-initialize after the last eglTerminate tore everything down.
// Teardown-ish entry points keep using GetState() and fail benignly
// when MobileGL is not initialized.
EGLStateContext* GetStateEnsureInitialized() {
MobileGL::EnsureInitialized();
return GetState();
}
MG_Backend::BackendObject* GetBackendObject(EGLStateContext* state) {
auto* backendObject = MG_Backend::pActiveBackendObject.get();
if (!backendObject && state) {
@@ -31,14 +45,55 @@ namespace MobileGL::MG_Impl::EGLImpl {
return backendObject;
}
std::recursive_mutex& EGLOperationMutex() {
static std::recursive_mutex mutex;
return mutex;
}
String CurrentThreadIdString() {
std::ostringstream stream;
stream << std::this_thread::get_id();
return stream.str();
}
MG_Backend::WindowBackend DetectWindowBackend() {
#if defined(ANDROID) || defined(__ANDROID__)
return MG_Backend::WindowBackend::Android;
#elif defined(__APPLE__)
return MG_Backend::WindowBackend::MetalLayer;
#elif defined(_WIN32)
return MG_Backend::WindowBackend::Win32;
#elif defined(__linux__)
return MG_Backend::WindowBackend::X11;
#else
return MG_Backend::WindowBackend::Unknown;
#endif
}
EGLint GetAttribValue(const EGLint* attribList, EGLint attrib, EGLint defaultValue) {
if (!attribList) {
return defaultValue;
}
for (SizeT i = 0; attribList[i] != EGL_NONE; i += 2) {
if (attribList[i] == attrib) {
return attribList[i + 1];
}
}
return defaultValue;
}
EGLint GetAttribValueAttrib(const EGLAttrib* attribList, EGLint attrib, EGLint defaultValue) {
if (!attribList) {
return defaultValue;
}
for (SizeT i = 0; attribList[i] != EGL_NONE; i += 2) {
if (attribList[i] == attrib) {
return static_cast<EGLint>(attribList[i + 1]);
}
}
return defaultValue;
}
template <typename NativeType>
Bool IsNullNativeHandle(NativeType nativeHandle) {
if constexpr (std::is_pointer_v<NativeType>) {
@@ -77,25 +132,35 @@ namespace MobileGL::MG_Impl::EGLImpl {
return EGL_NO_SURFACE;
}
auto* backendObject = GetBackendObject(state);
if (!backendObject) {
MGLOG_E("activeBackendObject not initialized!");
return EGL_NO_SURFACE;
}
const MG_Backend::WindowHandle windowHandle = {
.Backend = DetectWindowBackend(),
.Handle = ToVoidHandle(window),
.Width = static_cast<Uint32>(std::max<EGLint>(GetAttribValue(attrib_list, EGL_WIDTH, 0), 0)),
.Height = static_cast<Uint32>(std::max<EGLint>(GetAttribValue(attrib_list, EGL_HEIGHT, 0), 0)),
};
if (!backendObject->CreateEGLWindowSurface(windowHandle)) {
EGLSurface surface = state->CreateWindowSurface(dpy, config, window, attrib_list);
if (surface == EGL_NO_SURFACE) {
return EGL_NO_SURFACE;
}
auto* backendObject = GetBackendObject(state);
if (!backendObject) {
MGLOG_E("activeBackendObject not initialized!");
state->DestroySurface(dpy, surface);
return EGL_NO_SURFACE;
}
if (!backendObject->CreateEGLWindowSurface(surface, windowHandle)) {
state->DestroySurface(dpy, surface);
state->SetError(EGL_BAD_NATIVE_WINDOW);
return EGL_NO_SURFACE;
}
return state->CreateWindowSurface(dpy, config, window, attrib_list);
return surface;
}
EGLBoolean SwapBuffers(EGLDisplay dpy, EGLSurface draw) {
const std::lock_guard<std::recursive_mutex> operationLock(EGLOperationMutex());
auto* state = GetState();
if (!state) {
return EGL_FALSE;
@@ -111,6 +176,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
return EGL_FALSE;
}
if (!backendObject->SwapEGLBuffers(dpy, draw)) {
MGLOG_E("eglSwapBuffers failed on thread=%s dpy=%p draw=%p", CurrentThreadIdString().c_str(), dpy, draw);
state->SetError(EGL_BAD_SURFACE);
return EGL_FALSE;
}
@@ -135,7 +201,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
}
EGLBoolean Initialize(EGLDisplay dpy, EGLint* major, EGLint* minor) {
auto* state = GetState();
auto* state = GetStateEnsureInitialized();
if (!state) {
return EGL_FALSE;
}
@@ -156,7 +222,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
}
EGLDisplay GetDisplay(NativeDisplayType display) {
auto* state = GetState();
auto* state = GetStateEnsureInitialized();
if (!state) {
return EGL_NO_DISPLAY;
}
@@ -172,6 +238,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
}
EGLBoolean MakeCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) {
const std::lock_guard<std::recursive_mutex> operationLock(EGLOperationMutex());
auto* state = GetState();
if (!state) {
return EGL_FALSE;
@@ -181,17 +248,30 @@ namespace MobileGL::MG_Impl::EGLImpl {
const auto oldDraw = state->GetCurrentSurface(EGL_DRAW);
const auto oldRead = state->GetCurrentSurface(EGL_READ);
const auto oldContext = state->GetCurrentContext();
const String threadId = CurrentThreadIdString();
MGLOG_D("eglMakeCurrent begin thread=%s dpy=%p draw=%p read=%p ctx=%p oldDpy=%p oldDraw=%p oldRead=%p oldCtx=%p",
threadId.c_str(), dpy, draw, read, ctx, oldDisplay, oldDraw, oldRead, oldContext);
if (!state->MakeCurrent(dpy, draw, read, ctx)) {
const EGLint error = state->ConsumeError();
MGLOG_D("eglMakeCurrent rejected by EGLState thread=%s error=0x%04x", threadId.c_str(), error);
state->SetError(error);
return EGL_FALSE;
}
const Bool releaseCurrentRequest =
dpy == EGL_NO_DISPLAY && draw == EGL_NO_SURFACE && read == EGL_NO_SURFACE && ctx == EGL_NO_CONTEXT;
draw == EGL_NO_SURFACE && read == EGL_NO_SURFACE && ctx == EGL_NO_CONTEXT;
if (releaseCurrentRequest) {
if (auto* backendObject = MG_Backend::pActiveBackendObject.get()) {
(void)backendObject->MakeEGLCurrent(dpy, draw, read, ctx);
if (!backendObject->MakeEGLCurrent(dpy, draw, read, ctx)) {
MGLOG_E("eglMakeCurrent release failed in backend thread=%s", threadId.c_str());
state->MakeCurrent(oldDisplay, oldDraw, oldRead, oldContext);
state->SetError(EGL_BAD_ACCESS);
return EGL_FALSE;
}
}
MGLOG_D("eglMakeCurrent release succeeded thread=%s", threadId.c_str());
return EGL_TRUE;
}
@@ -202,10 +282,14 @@ namespace MobileGL::MG_Impl::EGLImpl {
return EGL_FALSE;
}
if (!backendObject->MakeEGLCurrent(dpy, draw, read, ctx)) {
MGLOG_E("eglMakeCurrent backend attach failed thread=%s dpy=%p draw=%p read=%p ctx=%p", threadId.c_str(),
dpy, draw, read, ctx);
state->SetError(EGL_BAD_ACCESS);
state->MakeCurrent(oldDisplay, oldDraw, oldRead, oldContext);
return EGL_FALSE;
}
MGLOG_D("eglMakeCurrent attach succeeded thread=%s dpy=%p draw=%p read=%p ctx=%p", threadId.c_str(), dpy, draw,
read, ctx);
return EGL_TRUE;
}
@@ -218,11 +302,18 @@ namespace MobileGL::MG_Impl::EGLImpl {
}
EGLBoolean DestroySurface(EGLDisplay dpy, EGLSurface surface) {
const std::lock_guard<std::recursive_mutex> operationLock(EGLOperationMutex());
auto* state = GetState();
if (!state) {
return EGL_FALSE;
}
return state->DestroySurface(dpy, surface) ? EGL_TRUE : EGL_FALSE;
if (!state->DestroySurface(dpy, surface)) {
return EGL_FALSE;
}
if (auto* backendObject = MG_Backend::pActiveBackendObject.get()) {
backendObject->ReleaseEGLSurface(surface);
}
return EGL_TRUE;
}
EGLBoolean Terminate(EGLDisplay dpy) {
@@ -230,7 +321,21 @@ namespace MobileGL::MG_Impl::EGLImpl {
if (!state) {
return EGL_FALSE;
}
return state->TerminateDisplay(dpy) ? EGL_TRUE : EGL_FALSE;
if (!state->TerminateDisplay(dpy)) {
return EGL_FALSE;
}
if (auto* backendObject = MG_Backend::pActiveBackendObject.get()) {
backendObject->ReleaseEGLResources();
}
// The last initialized display is gone and nothing is current on any
// thread: tear the whole library down deterministically inside the
// EGL lifecycle (backend, GL/EGL state, glslang). A later EGL call
// re-initializes lazily via GetStateEnsureInitialized(); process exit
// then has nothing left to destroy.
if (!state->HasAnyInitializedDisplay() && !state->HasAnyCurrentContext()) {
MobileGL::Destroy();
}
return EGL_TRUE;
}
EGLBoolean ReleaseThread() {
@@ -238,6 +343,9 @@ namespace MobileGL::MG_Impl::EGLImpl {
if (!state) {
return EGL_FALSE;
}
if (auto* backendObject = MG_Backend::pActiveBackendObject.get()) {
(void)backendObject->MakeEGLCurrent(EGL_NO_DISPLAY, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
}
state->ReleaseThread();
return EGL_TRUE;
}
@@ -259,7 +367,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
}
EGLBoolean BindAPI(EGLenum api) {
auto* state = GetState();
auto* state = GetStateEnsureInitialized();
if (!state) {
return EGL_FALSE;
}
@@ -292,7 +400,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
}
char const* QueryString(EGLDisplay display, EGLint name) {
auto* state = GetState();
auto* state = GetStateEnsureInitialized();
if (!state) {
return nullptr;
}
@@ -310,7 +418,14 @@ namespace MobileGL::MG_Impl::EGLImpl {
case EGL_CLIENT_APIS:
return "OpenGL OpenGL_ES";
case EGL_EXTENSIONS:
return "";
if (display == EGL_NO_DISPLAY) {
return "EGL_EXT_client_extensions "
"EGL_EXT_platform_base "
"EGL_KHR_platform_base "
"EGL_MESA_platform_surfaceless";
}
return "EGL_KHR_create_context "
"EGL_MESA_platform_surfaceless";
default:
state->SetError(EGL_BAD_PARAMETER);
return nullptr;
@@ -322,7 +437,17 @@ namespace MobileGL::MG_Impl::EGLImpl {
if (!state) {
return EGL_FALSE;
}
return state->SwapInterval(dpy, interval) ? EGL_TRUE : EGL_FALSE;
if (!state->SwapInterval(dpy, interval)) {
return EGL_FALSE;
}
// Forward the request to the backend's native presentation path; without this
// the app's vsync setting only ever reaches MobileGL's shadow state and the
// native surface stays at the driver default (interval 1 = always vsynced).
auto* backendObject = GetBackendObject(state);
if (backendObject) {
backendObject->SetEGLSwapInterval(static_cast<Int>(interval));
}
return EGL_TRUE;
}
EGLSurface CreatePbufferSurface(EGLDisplay dpy, EGLConfig config, const EGLint* attrib_list) {
@@ -330,7 +455,24 @@ namespace MobileGL::MG_Impl::EGLImpl {
if (!state) {
return EGL_NO_SURFACE;
}
return state->CreatePbufferSurface(dpy, config, attrib_list);
const EGLint width = GetAttribValue(attrib_list, EGL_WIDTH, 1);
const EGLint height = GetAttribValue(attrib_list, EGL_HEIGHT, 1);
EGLSurface surface = state->CreatePbufferSurface(dpy, config, attrib_list);
if (surface == EGL_NO_SURFACE) {
return EGL_NO_SURFACE;
}
auto* backendObject = GetBackendObject(state);
if (!backendObject) {
return EGL_NO_SURFACE;
}
if (!backendObject->CreateEGLPbufferSurface(surface, width, height)) {
state->DestroySurface(dpy, surface);
state->SetError(EGL_BAD_ALLOC);
return EGL_NO_SURFACE;
}
return surface;
}
EGLBoolean BindTexImage(EGLDisplay dpy, EGLSurface surface, EGLint buffer) {
@@ -521,7 +663,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
EGLDisplay GetPlatformDisplay(EGLenum platform, void* native_display, const EGLAttrib* attrib_list) {
(void)attrib_list;
auto* state = GetState();
auto* state = GetStateEnsureInitialized();
if (!state) {
return EGL_NO_DISPLAY;
}
@@ -547,22 +689,53 @@ namespace MobileGL::MG_Impl::EGLImpl {
return EGL_NO_SURFACE;
}
auto* backendObject = GetBackendObject(state);
if (!backendObject) {
MGLOG_E("activeBackendObject not initialized!");
return EGL_NO_SURFACE;
}
const MG_Backend::WindowHandle windowHandle = {
.Backend = DetectWindowBackend(),
.Handle = native_window,
.Width = static_cast<Uint32>(std::max<EGLint>(GetAttribValueAttrib(attrib_list, EGL_WIDTH, 0), 0)),
.Height = static_cast<Uint32>(std::max<EGLint>(GetAttribValueAttrib(attrib_list, EGL_HEIGHT, 0), 0)),
};
if (!backendObject->CreateEGLWindowSurface(windowHandle)) {
EGLSurface surface = state->CreatePlatformWindowSurface(dpy, config, native_window, attrib_list);
if (surface == EGL_NO_SURFACE) {
return EGL_NO_SURFACE;
}
auto* backendObject = GetBackendObject(state);
if (!backendObject) {
MGLOG_E("activeBackendObject not initialized!");
state->DestroySurface(dpy, surface);
return EGL_NO_SURFACE;
}
if (!backendObject->CreateEGLWindowSurface(surface, windowHandle)) {
state->DestroySurface(dpy, surface);
state->SetError(EGL_BAD_NATIVE_WINDOW);
return EGL_NO_SURFACE;
}
return state->CreatePlatformWindowSurface(dpy, config, native_window, attrib_list);
return surface;
}
EGLBoolean ResizePlatformWindowSurface(EGLDisplay dpy, EGLSurface surface, EGLint width, EGLint height) {
auto* state = GetState();
if (!state) {
return EGL_FALSE;
}
if (!state->ResizeSurface(dpy, surface, width, height)) {
return EGL_FALSE;
}
auto* backendObject = GetBackendObject(state);
if (!backendObject) {
MGLOG_E("activeBackendObject not initialized!");
return EGL_FALSE;
}
width = std::max<EGLint>(width, 1);
height = std::max<EGLint>(height, 1);
if (!backendObject->ResizeEGLWindowSurface(surface, static_cast<Uint32>(width), static_cast<Uint32>(height))) {
state->SetError(EGL_BAD_NATIVE_WINDOW);
return EGL_FALSE;
}
return EGL_TRUE;
}
EGLSurface CreatePlatformPixmapSurface(EGLDisplay dpy, EGLConfig config, void* native_pixmap,
@@ -586,6 +759,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
if (!name) {
return nullptr;
}
MobileGL::EnsureInitialized();
MGLOG_D("eglGetProcAddress(%s)", name);
void* proc = MG_Impl::GetProcAddress(name);
+1
View File
@@ -57,6 +57,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
EGLDisplay GetPlatformDisplay(EGLenum platform, void* native_display, const EGLAttrib* attrib_list);
EGLSurface CreatePlatformWindowSurface(EGLDisplay dpy, EGLConfig config, void* native_window,
const EGLAttrib* attrib_list);
EGLBoolean ResizePlatformWindowSurface(EGLDisplay dpy, EGLSurface surface, EGLint width, EGLint height);
EGLSurface CreatePlatformPixmapSurface(EGLDisplay dpy, EGLConfig config, void* native_pixmap,
const EGLAttrib* attrib_list);
EGLBoolean WaitSync(EGLDisplay dpy, EGLSync sync, EGLint flags);
@@ -235,6 +235,14 @@ MOBILEGL_EGL_API EGLDisplay eglGetPlatformDisplay(EGLenum platform, void* native
return MobileGL::MG_Impl::EGLImpl::GetPlatformDisplay(platform, native_display, attrib_list);
}
MOBILEGL_EGL_API EGLDisplay eglGetPlatformDisplayEXT(EGLenum platform, void* native_display,
const EGLint* attrib_list) {
MGLOG_D("eglGetPlatformDisplayEXT(platform=%u, native_display=%p, attrib_list=%p)", platform, native_display,
attrib_list);
return MobileGL::MG_Impl::EGLImpl::GetPlatformDisplay(
platform, native_display, reinterpret_cast<const EGLAttrib*>(attrib_list));
}
MOBILEGL_EGL_API EGLSurface eglCreatePlatformWindowSurface(EGLDisplay dpy, EGLConfig config, void* native_window,
const EGLAttrib* attrib_list) {
MGLOG_D("eglCreatePlatformWindowSurface(dpy=%p, config=%p, native_window=%p, attrib_list=%p)", dpy, config,
File diff suppressed because it is too large Load Diff
@@ -12,19 +12,42 @@
namespace MobileGL::MG_Impl::GLImpl {
/* @INSERTION_POINT:FUNCTION_DECLARATION@ */
void GetBufferParameteriv(GLenum target, GLenum pname, GLint* params);
void GetBufferParameteri64v(GLenum target, GLenum pname, GLint64* params);
void GetBufferPointerv(GLenum target, GLenum pname, void** params);
GLboolean IsBuffer(GLuint buffer);
void DeleteBuffers(GLsizei n, const GLuint* buffers);
void FlushMappedBufferRange(GLenum target, GLintptr offset, GLsizeiptr length);
GLboolean UnmapBuffer(GLenum target);
void* MapBufferRange(GLenum target, GLintptr offset, GLsizeiptr length, GLbitfield access);
void* MapBuffer(GLenum target, GLenum access);
void BufferStorage(GLenum target, GLsizeiptr size, const void* data, GLbitfield flags);
void CreateBuffers(GLsizei n, GLuint* buffers);
void NamedBufferStorage(GLuint buffer, GLsizeiptr size, const void* data, GLbitfield flags);
void NamedBufferData(GLuint buffer, GLsizeiptr size, const void* data, GLenum usage);
void NamedBufferSubData(GLuint buffer, GLintptr offset, GLsizeiptr size, const void* data);
void CopyNamedBufferSubData(GLuint readBuffer, GLuint writeBuffer, GLintptr readOffset, GLintptr writeOffset,
GLsizeiptr size);
void ClearNamedBufferData(GLuint buffer, GLenum internalformat, GLenum format, GLenum type, const void* data);
void ClearNamedBufferSubData(GLuint buffer, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format,
GLenum type, const void* data);
void* MapNamedBuffer(GLuint buffer, GLenum access);
void* MapNamedBufferRange(GLuint buffer, GLintptr offset, GLsizeiptr length, GLbitfield access);
GLboolean UnmapNamedBuffer(GLuint buffer);
void FlushMappedNamedBufferRange(GLuint buffer, GLintptr offset, GLsizeiptr length);
void GetNamedBufferParameteriv(GLuint buffer, GLenum pname, GLint* params);
void GetNamedBufferParameteri64v(GLuint buffer, GLenum pname, GLint64* params);
void GetNamedBufferPointerv(GLuint buffer, GLenum pname, void** params);
void CopyBufferSubData(GLenum readTarget, GLenum writeTarget, GLintptr readOffset, GLintptr writeOffset,
GLsizeiptr size);
void BufferSubData(GLenum target, GLintptr offset, GLsizeiptr size, const void* data);
void GetBufferSubData(GLenum target, GLintptr offset, GLsizeiptr size, void* data);
void BufferData(GLenum target, GLsizeiptr size, const void* data, GLenum usage);
void BindBuffer(GLenum target, GLuint buffer);
void GenBuffers(GLsizei n, GLuint* buffers);
void BindBufferBase(GLenum target, GLuint index, GLuint buffer);
void BindBufferRange(GLenum target, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size);
void BindBuffersBase(GLenum target, GLuint first, GLsizei count, const GLuint* buffers);
void BindBuffersRange(GLenum target, GLuint first, GLsizei count, const GLuint* buffers, const GLintptr* offsets,
const GLsizeiptr* sizes);
} // namespace MobileGL::MG_Impl::GLImpl
@@ -7,6 +7,7 @@
// End of Source File Header
#include "Validators.h"
#include <MG_Backend/BackendObjects.h>
#include <MG_State/GLState/Core.h>
#include <MG_State/GLState/ErrorState/Error.h>
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
@@ -52,6 +53,26 @@ namespace MobileGL::MG_Impl::GLImpl::BufferImpl {
return true;
}
Bool ValidateBufferBindingPointIndex(BufferTarget target, Uint index) {
SizeT pointCount = MG_State::pGLContext->GetBufferBindingPointCount(target);
if (target == BufferTarget::ShaderStorage && MG_Backend::pActiveBackendObject) {
const Int backendCount =
MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxShaderStorageBufferBindings;
pointCount = std::min(pointCount, static_cast<SizeT>(std::max(backendCount, 0)));
}
if (index < pointCount) {
return true;
}
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl/BufferImpl", "ValidateBufferBindingPointIndex",
std::format("Binding point index {} is out of range for target {}.", index,
MG_Util::ConvertBufferTargetToString(target))));
return false;
}
Bool ValidateBufferName(Uint index, Bool allowZero) {
if (index == 0) {
if (allowZero) return true;
@@ -16,4 +16,5 @@ namespace MobileGL::MG_Impl::GLImpl::BufferImpl {
Bool ValidateBufferUsage(BufferUsage usage);
Bool ValidateBufferMappingAccess(Flags<BufferMappingAccessBit> accessBits);
Bool ValidateBufferBindingPointTarget(BufferTarget target);
Bool ValidateBufferBindingPointIndex(BufferTarget target, Uint index);
} // namespace MobileGL::MG_Impl::GLImpl::BufferImpl
@@ -9,9 +9,75 @@
#include "GL_Drawing.h"
#include <Config.h>
#include <MG_State/GLState/Core.h>
#include <MG_State/EGLState/Core.h>
#include <MG_Backend/BackendObjects.h>
namespace MobileGL::MG_Impl::GLImpl {
static Bool ValidateCurrentProgramForExecution(const char* functionName) {
const auto& currentProgram = MG_State::pGLContext->GetCurrentProgram();
if (!currentProgram) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "There is no current program object."));
return false;
}
if (!currentProgram->GetLinkStatus()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
"The current program object is not linked."));
return false;
}
return true;
}
static Bool ValidateCurrentProgramForCompute(const char* functionName) {
if (!ValidateCurrentProgramForExecution(functionName)) return false;
const auto& currentProgram = MG_State::pGLContext->GetCurrentProgram();
if (currentProgram->GetShaderIndexByStage(ShaderStage::Compute) < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
"The current program object has no compute shader stage."));
return false;
}
return true;
}
static Bool ValidatePrimitiveModeForBackend(const char* functionName, GLenum mode) {
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
if (!activeBackendObject) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "No active backend object."));
return false;
}
if (activeBackendObject->GetBackendType() == BackendType::DirectVulkan && mode == GL_LINE_LOOP) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", functionName,
"Primitive mode GL_LINE_LOOP is not supported by the DirectVulkan backend."));
return false;
}
const auto& vao = MG_State::pGLContext->GetBoundVertexArray();
if (vao && vao->GetExternalIndex() == 0 && !MG_State::IsRelaxedSemanticsActive()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
"Default vertex array object cannot be used for drawing in core profile."));
return false;
}
return true;
}
void Clear_Backend(GLbitfield mask) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
@@ -50,6 +116,13 @@ namespace MobileGL::MG_Impl::GLImpl {
MG_Backend::gBackendFunctionsTable.GL.DrawArrays(mode, first, count);
}
void MultiDrawArrays_Backend(GLenum mode, const GLint* first, const GLsizei* count, GLsizei drawcount) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
MG_Backend::gBackendFunctionsTable.GL.MultiDrawArrays(mode, first, count, drawcount);
}
void DrawElementsBaseVertex_Backend(GLenum mode, GLsizei count, GLenum type, const void* indices,
GLint basevertex) {
#ifdef TRACY_ENABLE
@@ -73,6 +146,24 @@ namespace MobileGL::MG_Impl::GLImpl {
MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirect(mode, indirect, drawcount, stride);
}
void MultiDrawElementsIndirectCount_Backend(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount,
GLsizei maxdrawcount, GLsizei stride) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirectCount(mode, type, indirect, drawcount,
maxdrawcount, stride);
}
void MultiDrawArraysIndirectCount_Backend(GLenum mode, const void* indirect, GLintptr drawcount,
GLsizei maxdrawcount, GLsizei stride) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirectCount(mode, indirect, drawcount, maxdrawcount,
stride);
}
void DrawRangeElementsBaseVertex_Backend(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type,
const void* indices, GLint basevertex) {
#ifdef TRACY_ENABLE
@@ -156,75 +247,193 @@ namespace MobileGL::MG_Impl::GLImpl {
}
/* @INSERTION_POINT:FUNCTION_IMPLEMENTATION@ */
void DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ) {
auto dispatchCompute = MG_Backend::gBackendFunctionsTable.GL.DispatchCompute;
if (!dispatchCompute) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Backend does not support compute dispatch."));
return;
}
if (!ValidateCurrentProgramForCompute(__func__)) return;
dispatchCompute(numGroupsX, numGroupsY, numGroupsZ);
}
void DispatchComputeIndirect(GLintptr indirect) {
auto dispatchComputeIndirect = MG_Backend::gBackendFunctionsTable.GL.DispatchComputeIndirect;
if (!dispatchComputeIndirect) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"Backend does not support indirect compute dispatch."));
return;
}
if (!ValidateCurrentProgramForCompute(__func__)) return;
dispatchComputeIndirect(indirect);
}
void MemoryBarrier(GLbitfield barriers) {
auto memoryBarrier = MG_Backend::gBackendFunctionsTable.GL.MemoryBarrier;
if (!memoryBarrier) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Backend does not support memory barriers."));
return;
}
memoryBarrier(barriers);
}
void MemoryBarrierByRegion(GLbitfield barriers) {
auto memoryBarrierByRegion = MG_Backend::gBackendFunctionsTable.GL.MemoryBarrierByRegion;
if (!memoryBarrierByRegion) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"Backend does not support regional memory barriers."));
return;
}
memoryBarrierByRegion(barriers);
}
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride) {
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
MultiDrawElementsIndirect_Backend(mode, type, indirect, drawcount, stride);
}
void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride) {
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
MultiDrawArraysIndirect_Backend(mode, indirect, drawcount, stride);
}
void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount,
GLsizei maxdrawcount, GLsizei stride) {
auto multiDrawElementsIndirectCount = MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirectCount;
if (!multiDrawElementsIndirectCount) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"Backend does not support indirect-parameter indexed draws."));
return;
}
MultiDrawElementsIndirectCount_Backend(mode, type, indirect, drawcount, maxdrawcount, stride);
}
void MultiDrawArraysIndirectCount(GLenum mode, const void* indirect, GLintptr drawcount,
GLsizei maxdrawcount, GLsizei stride) {
auto multiDrawArraysIndirectCount = MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirectCount;
if (!multiDrawArraysIndirectCount) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"Backend does not support indirect-parameter array draws."));
return;
}
MultiDrawArraysIndirectCount_Backend(mode, indirect, drawcount, maxdrawcount, stride);
}
void DrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type,
const void* indices, GLint basevertex) {
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
DrawRangeElementsBaseVertex_Backend(mode, start, end, count, type, indices, basevertex);
}
void DrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices) {
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
DrawRangeElements_Backend(mode, start, end, count, type, indices);
}
void DrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
GLsizei instancecount, GLint basevertex, GLuint baseinstance) {
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
DrawElementsInstancedBaseVertexBaseInstance_Backend(mode, count, type, indices, instancecount, basevertex,
baseinstance);
}
void DrawElementsInstancedBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices,
GLsizei instancecount, GLint basevertex) {
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
DrawElementsInstancedBaseVertex_Backend(mode, count, type, indices, instancecount, basevertex);
}
void DrawElementsInstancedBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
GLsizei instancecount, GLuint baseinstance) {
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
DrawElementsInstancedBaseInstance_Backend(mode, count, type, indices, instancecount, baseinstance);
}
void DrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount) {
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
DrawElementsInstanced_Backend(mode, count, type, indices, instancecount);
}
void DrawElementsIndirect(GLenum mode, GLenum type, const void* indirect) {
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
DrawElementsIndirect_Backend(mode, type, indirect);
}
void DrawArraysInstancedBaseInstance(GLenum mode, GLint first, GLsizei count, GLsizei instancecount,
GLuint baseinstance) {
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
DrawArraysInstancedBaseInstance_Backend(mode, first, count, instancecount, baseinstance);
}
void DrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei instancecount) {
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
DrawArraysInstanced_Backend(mode, first, count, instancecount);
}
void DrawArraysIndirect(GLenum mode, const void* indirect) {
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
DrawArraysIndirect_Backend(mode, indirect);
}
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices, GLint basevertex) {
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
DrawElementsBaseVertex_Backend(mode, count, type, indices, basevertex);
}
void DrawArrays(GLenum mode, GLint first, GLsizei count) {
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
DrawArrays_Backend(mode, first, count);
}
void MultiDrawArrays(GLenum mode, const GLint* first, const GLsizei* count, GLsizei drawcount) {
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
if (drawcount < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "drawcount must be non-negative."));
return;
}
MultiDrawArrays_Backend(mode, first, count, drawcount);
}
void MultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, const void* const* indices,
GLsizei drawcount) {
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
MultiDrawElements_Backend(mode, count, type, indices, drawcount);
}
void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, const void* const* indices,
GLsizei drawcount, const GLint* basevertex) {
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
MultiDrawElementsBaseVertex_Backend(mode, count, type, indices, drawcount, basevertex);
}
@@ -233,6 +442,8 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices) {
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
DrawElements_Backend(mode, count, type, indices);
}
@@ -11,8 +11,16 @@
namespace MobileGL::MG_Impl::GLImpl {
/* @INSERTION_POINT:FUNCTION_DECLARATION@ */
void DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ);
void DispatchComputeIndirect(GLintptr indirect);
void MemoryBarrier(GLbitfield barriers);
void MemoryBarrierByRegion(GLbitfield barriers);
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride);
void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride);
void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount,
GLsizei maxdrawcount, GLsizei stride);
void MultiDrawArraysIndirectCount(GLenum mode, const void* indirect, GLintptr drawcount,
GLsizei maxdrawcount, GLsizei stride);
void DrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type,
const void* indices, GLint basevertex);
void DrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices);
@@ -30,6 +38,7 @@ namespace MobileGL::MG_Impl::GLImpl {
void DrawArraysIndirect(GLenum mode, const void* indirect);
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices, GLint basevertex);
void DrawArrays(GLenum mode, GLint first, GLsizei count);
void MultiDrawArrays(GLenum mode, const GLint* first, const GLsizei* count, GLsizei drawcount);
void MultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, const void* const* indices,
GLsizei drawcount);
void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, const void* const* indices,
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -24,19 +24,43 @@ namespace MobileGL::MG_Impl::GLImpl {
GLboolean IsRenderbuffer(GLuint renderbuffer);
void GetRenderbufferParameteriv(GLenum target, GLenum pname, GLint* params);
void GenRenderbuffers(GLsizei n, GLuint* renderbuffers);
void CreateRenderbuffers(GLsizei n, GLuint* renderbuffers);
void NamedRenderbufferStorage(GLuint renderbuffer, GLenum internalformat, GLsizei width, GLsizei height);
void NamedRenderbufferStorageMultisample(GLuint renderbuffer, GLsizei samples, GLenum internalformat,
GLsizei width, GLsizei height);
void GetNamedRenderbufferParameteriv(GLuint renderbuffer, GLenum pname, GLint* params);
void FramebufferRenderbuffer(GLenum target, GLenum attachment, GLenum renderbuffertarget, GLuint renderbuffer);
void NamedFramebufferRenderbuffer(GLuint framebuffer, GLenum attachment, GLenum renderbuffertarget,
GLuint renderbuffer);
void DeleteRenderbuffers(GLsizei n, const GLuint* renderbuffers);
void BindRenderbuffer(GLenum target, GLuint renderbuffer);
void SampleMaski(GLuint maskNumber, GLbitfield mask);
GLboolean IsFramebuffer(GLuint framebuffer);
void GetFramebufferAttachmentParameteriv(GLenum target, GLenum attachment, GLenum pname, GLint* params);
void GenFramebuffers(GLsizei n, GLuint* framebuffers);
void CreateFramebuffers(GLsizei n, GLuint* framebuffers);
void FramebufferTextureLayer(GLenum target, GLenum attachment, GLuint texture, GLint level, GLint layer);
void FramebufferTexture3D(GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level,
GLint zoffset);
void FramebufferTexture2D(GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level);
void FramebufferTexture1D(GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level);
void FramebufferTexture(GLenum target, GLenum attachment, GLuint texture, GLint level);
void NamedFramebufferTexture(GLuint framebuffer, GLenum attachment, GLuint texture, GLint level);
void NamedFramebufferTexture1D(GLuint framebuffer, GLenum attachment, GLenum textarget, GLuint texture, GLint level);
void NamedFramebufferTexture2D(GLuint framebuffer, GLenum attachment, GLenum textarget, GLuint texture, GLint level);
void NamedFramebufferTexture3D(GLuint framebuffer, GLenum attachment, GLenum textarget, GLuint texture, GLint level,
GLint zoffset);
void NamedFramebufferTextureLayer(GLuint framebuffer, GLenum attachment, GLuint texture, GLint level, GLint layer);
void NamedFramebufferDrawBuffer(GLuint framebuffer, GLenum buf);
void NamedFramebufferDrawBuffers(GLuint framebuffer, GLsizei n, const GLenum* bufs);
void NamedFramebufferReadBuffer(GLuint framebuffer, GLenum src);
void ClearNamedFramebufferfv(GLuint framebuffer, GLenum buffer, GLint drawbuffer, const GLfloat* value);
void ClearNamedFramebufferfi(GLuint framebuffer, GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
GLenum CheckNamedFramebufferStatus(GLuint framebuffer, GLenum target);
void GetNamedFramebufferAttachmentParameteriv(GLuint framebuffer, GLenum attachment, GLenum pname, GLint* params);
void BlitNamedFramebuffer(GLuint readFramebuffer, GLuint drawFramebuffer, GLint srcX0, GLint srcY0, GLint srcX1,
GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask,
GLenum filter);
void DrawBuffer(GLenum buf);
void DrawBuffers(GLsizei n, const GLenum* bufs);
void ReadBuffer(GLenum src);
@@ -54,6 +78,6 @@ namespace MobileGL::MG_Impl::GLImpl {
SharedPtr<MG_State::GLState::ITextureObject> stencilAttachment;
};
extern UniquePtr<DefaultFramebufferInfo> pDefaultFramebufferInfo;
extern UniquePtr<DefaultFramebufferInfo>& pDefaultFramebufferInfo;
} // namespace FramebufferImpl
} // namespace MobileGL::MG_Impl::GLImpl
@@ -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",
File diff suppressed because it is too large Load Diff
@@ -13,6 +13,13 @@ namespace MobileGL::MG_Impl::GLImpl {
/* @INSERTION_POINT:FUNCTION_DECLARATION@ */
const GLubyte* GetString(GLenum name);
const GLubyte* GetStringi(GLenum name, GLuint index);
void GetBooleanv(GLenum pname, GLboolean* params);
void GetFloatv(GLenum pname, GLfloat* params);
void GetDoublev(GLenum pname, GLdouble* params);
void GetIntegerv(GLenum pname, GLint* params);
void GetInteger64v(GLenum pname, GLint64* params);
void GetIntegeri_v(GLenum target, GLuint index, GLint* data);
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data);
GLenum GetError();
GLenum GetGraphicsResetStatus();
} // namespace MobileGL::MG_Impl::GLImpl
File diff suppressed because it is too large Load Diff
@@ -22,6 +22,12 @@ namespace MobileGL::MG_Impl::GLImpl {
GLchar* name);
void GetActiveUniform(GLuint program, GLuint index, GLsizei bufSize, GLsizei* length, GLint* size, GLenum* type,
GLchar* name);
void GetActiveUniformName(GLuint program, GLuint uniformIndex, GLsizei bufSize, GLsizei* length,
GLchar* uniformName);
void GetUniformIndices(GLuint program, GLsizei uniformCount, const GLchar* const* uniformNames,
GLuint* uniformIndices);
void GetActiveUniformsiv(GLuint program, GLsizei uniformCount, const GLuint* uniformIndices, GLenum pname,
GLint* params);
void GetAttachedShaders(GLuint program, GLsizei maxCount, GLsizei* count, GLuint* shaders);
GLint GetAttribLocation(GLuint program, const GLchar* name);
void GetProgramiv(GLuint program, GLenum pname, GLint* params);
@@ -32,6 +38,7 @@ namespace MobileGL::MG_Impl::GLImpl {
GLint GetUniformLocation(GLuint program, const GLchar* name);
void GetUniformfv(GLuint program, GLint location, GLfloat* params);
void GetUniformiv(GLuint program, GLint location, GLint* params);
void GetUniformuiv(GLuint program, GLint location, GLuint* params);
GLboolean IsProgram(GLuint program);
GLboolean IsShader(GLuint shader);
void LinkProgram(GLuint program);
@@ -45,6 +52,10 @@ namespace MobileGL::MG_Impl::GLImpl {
void Uniform2i(GLint location, GLint v0, GLint v1);
void Uniform3i(GLint location, GLint v0, GLint v1, GLint v2);
void Uniform4i(GLint location, GLint v0, GLint v1, GLint v2, GLint v3);
void Uniform1ui(GLint location, GLuint v0);
void Uniform2ui(GLint location, GLuint v0, GLuint v1);
void Uniform3ui(GLint location, GLuint v0, GLuint v1, GLuint v2);
void Uniform4ui(GLint location, GLuint v0, GLuint v1, GLuint v2, GLuint v3);
void Uniform1fv(GLint location, GLsizei count, const GLfloat* value);
void Uniform2fv(GLint location, GLsizei count, const GLfloat* value);
void Uniform3fv(GLint location, GLsizei count, const GLfloat* value);
@@ -53,15 +64,78 @@ namespace MobileGL::MG_Impl::GLImpl {
void Uniform2iv(GLint location, GLsizei count, const GLint* value);
void Uniform3iv(GLint location, GLsizei count, const GLint* value);
void Uniform4iv(GLint location, GLsizei count, const GLint* value);
void Uniform1uiv(GLint location, GLsizei count, const GLuint* value);
void Uniform2uiv(GLint location, GLsizei count, const GLuint* value);
void Uniform3uiv(GLint location, GLsizei count, const GLuint* value);
void Uniform4uiv(GLint location, GLsizei count, const GLuint* value);
void UniformMatrix2fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value);
void UniformMatrix3fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value);
void UniformMatrix4fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value);
void ProgramUniform1f(GLuint program, GLint location, GLfloat v0);
void ProgramUniform2f(GLuint program, GLint location, GLfloat v0, GLfloat v1);
void ProgramUniform3f(GLuint program, GLint location, GLfloat v0, GLfloat v1, GLfloat v2);
void ProgramUniform4f(GLuint program, GLint location, GLfloat v0, GLfloat v1, GLfloat v2, GLfloat v3);
void ProgramUniform1i(GLuint program, GLint location, GLint v0);
void ProgramUniform2i(GLuint program, GLint location, GLint v0, GLint v1);
void ProgramUniform3i(GLuint program, GLint location, GLint v0, GLint v1, GLint v2);
void ProgramUniform4i(GLuint program, GLint location, GLint v0, GLint v1, GLint v2, GLint v3);
void ProgramUniform1ui(GLuint program, GLint location, GLuint v0);
void ProgramUniform2ui(GLuint program, GLint location, GLuint v0, GLuint v1);
void ProgramUniform3ui(GLuint program, GLint location, GLuint v0, GLuint v1, GLuint v2);
void ProgramUniform4ui(GLuint program, GLint location, GLuint v0, GLuint v1, GLuint v2, GLuint v3);
void ProgramUniform1fv(GLuint program, GLint location, GLsizei count, const GLfloat* value);
void ProgramUniform2fv(GLuint program, GLint location, GLsizei count, const GLfloat* value);
void ProgramUniform3fv(GLuint program, GLint location, GLsizei count, const GLfloat* value);
void ProgramUniform4fv(GLuint program, GLint location, GLsizei count, const GLfloat* value);
void ProgramUniform1iv(GLuint program, GLint location, GLsizei count, const GLint* value);
void ProgramUniform2iv(GLuint program, GLint location, GLsizei count, const GLint* value);
void ProgramUniform3iv(GLuint program, GLint location, GLsizei count, const GLint* value);
void ProgramUniform4iv(GLuint program, GLint location, GLsizei count, const GLint* value);
void ProgramUniform1uiv(GLuint program, GLint location, GLsizei count, const GLuint* value);
void ProgramUniform2uiv(GLuint program, GLint location, GLsizei count, const GLuint* value);
void ProgramUniform3uiv(GLuint program, GLint location, GLsizei count, const GLuint* value);
void ProgramUniform4uiv(GLuint program, GLint location, GLsizei count, const GLuint* value);
void ProgramUniformMatrix2fv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLfloat* value);
void ProgramUniformMatrix3fv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLfloat* value);
void ProgramUniformMatrix4fv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLfloat* value);
void UniformMatrix2x3fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value);
void UniformMatrix3x2fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value);
void UniformMatrix2x4fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value);
void UniformMatrix4x2fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value);
void UniformMatrix3x4fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value);
void UniformMatrix4x3fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value);
void ProgramUniformMatrix2x3fv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLfloat* value);
void ProgramUniformMatrix3x2fv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLfloat* value);
void ProgramUniformMatrix2x4fv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLfloat* value);
void ProgramUniformMatrix4x2fv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLfloat* value);
void ProgramUniformMatrix3x4fv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLfloat* value);
void ProgramUniformMatrix4x3fv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLfloat* value);
GLuint GetUniformBlockIndex(GLuint program, const GLchar* uniformBlockName);
void UniformBlockBinding(GLuint program, GLuint uniformBlockIndex, GLuint uniformBlockBinding);
void GetActiveUniformBlockiv(GLuint program, GLuint uniformBlockIndex, GLenum pname, GLint* params);
void GetActiveUniformBlockName(GLuint program, GLuint uniformBlockIndex, GLsizei bufSize, GLsizei* length,
GLchar* uniformBlockName);
void BindFragDataLocation(GLuint program, GLuint colorNumber, const char* name);
void BindFragDataLocationIndexed(GLuint program, GLuint colorNumber, GLuint index, const char* name);
GLint GetFragDataLocation(GLuint program, const char* name);
GLint GetFragDataIndex(GLuint program, const char* name);
void GetProgramInterfaceiv(GLuint program, GLenum programInterface, GLenum pname, GLint* params);
GLuint GetProgramResourceIndex(GLuint program, GLenum programInterface, const GLchar* name);
void GetProgramResourceName(GLuint program, GLenum programInterface, GLuint index, GLsizei bufSize,
GLsizei* length, GLchar* name);
void GetProgramResourceiv(GLuint program, GLenum programInterface, GLuint index, GLsizei propCount,
const GLenum* props, GLsizei bufSize, GLsizei* length, GLint* params);
GLint GetProgramResourceLocation(GLuint program, GLenum programInterface, const GLchar* name);
GLint GetProgramResourceLocationIndex(GLuint program, GLenum programInterface, const GLchar* name);
void ShaderStorageBlockBinding(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding);
void ValidateProgram(GLuint program);
} // namespace MobileGL::MG_Impl::GLImpl
+362
View File
@@ -0,0 +1,362 @@
// MobileGL - MobileGL/MG_Impl/GLImpl/Query/GL_Query.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 "GL_Query.h"
#include <Config.h>
#include <MG_Backend/BackendObjects.h>
#include <MG_State/GLState/Core.h>
#include <MG_State/GLState/ErrorState/ErrorInfo.h>
namespace MobileGL::MG_Impl::GLImpl {
namespace {
// Frontend query object (GL_ARB_timer_query): wraps an optional backend
// timer-query handle. A null backend handle (backend has no timer-query
// support, timer queries are disabled by config, or the backend could
// not create a query at call time) keeps a graceful fallback: the query
// result is immediately available and reads as zero.
struct QueryObject {
GLuint id = 0;
GLenum target = 0; // 0 = gen'd but never used with BeginQuery/QueryCounter
MG_Backend::BackendQueryHandle backendHandle = nullptr;
Bool active = false;
Bool ended = false;
Bool resultCached = false;
Uint64 cachedResult = 0;
};
// Query calls may arrive from any thread (launchers migrate the context
// across JVM threads), so the live-object registry is mutex-guarded,
// like the sync-object registry in GL_Sync.cpp. Entries left at process
// shutdown are simply dropped; their backend handles die with the
// backend.
std::mutex g_queryObjectsMutex;
UnorderedMap<GLuint, QueryObject*> g_liveQueryObjects;
// Monotonically increasing id allocator; ids are valid query objects
// immediately after GenQueries.
GLuint g_nextQueryId = 1;
// Id of the query currently active on GL_TIME_ELAPSED (0 = none).
GLuint g_activeTimeElapsedQueryId = 0;
Bool TimerQueryDisabled() {
return MG_Config::Features.DisableTimerQuery;
}
void RecordQueryError(ErrorCode code, const char* function, const char* message) {
MG_State::pGLContext->RecordError(code,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", function, message));
}
// Callers must hold g_queryObjectsMutex.
QueryObject* FindQueryObjectLocked(GLuint id) {
const auto it = g_liveQueryObjects.find(id);
return it != g_liveQueryObjects.end() ? it->second : nullptr;
}
// Callers must hold g_queryObjectsMutex. Releases the backend handle
// (if any) and clears any cached result, so the object can be reused.
void ResetQueryObjectLocked(QueryObject* queryObject) {
if (queryObject->backendHandle) {
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
deleteBackendQuery(queryObject->backendHandle);
}
queryObject->backendHandle = nullptr;
}
queryObject->active = false;
queryObject->ended = false;
queryObject->resultCached = false;
queryObject->cachedResult = 0;
}
// Callers must hold g_queryObjectsMutex.
void EndTimeElapsedQueryLocked(QueryObject* queryObject) {
const auto endTimeElapsedQuery = MG_Backend::gBackendFunctionsTable.GL.EndTimeElapsedQuery;
if (endTimeElapsedQuery && queryObject->backendHandle) {
endTimeElapsedQuery(queryObject->backendHandle);
}
queryObject->active = false;
queryObject->ended = true;
g_activeTimeElapsedQueryId = 0;
}
// Shared GetQueryObject* implementation. Returns false when an error
// was recorded and no value should be written back.
Bool GetQueryObjectValue(GLuint id, GLenum pname, const char* function, Uint64& outValue) {
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
auto* queryObject = FindQueryObjectLocked(id);
if (!queryObject) {
RecordQueryError(ErrorCode::InvalidOperation, function, "Query object does not exist.");
return false;
}
if (queryObject->active) {
RecordQueryError(ErrorCode::InvalidOperation, function, "Query object is still active.");
return false;
}
switch (pname) {
case GL_QUERY_RESULT_AVAILABLE: {
if (queryObject->resultCached || !queryObject->backendHandle) {
outValue = 1;
return true;
}
const auto isQueryResultAvailable = MG_Backend::gBackendFunctionsTable.GL.IsQueryResultAvailable;
outValue = (!isQueryResultAvailable || isQueryResultAvailable(queryObject->backendHandle)) ? 1 : 0;
return true;
}
case GL_QUERY_RESULT: {
if (queryObject->resultCached) {
outValue = queryObject->cachedResult;
return true;
}
Uint64 result = 0;
if (queryObject->backendHandle) {
const auto getQueryResult64 = MG_Backend::gBackendFunctionsTable.GL.GetQueryResult64;
if (getQueryResult64 &&
!getQueryResult64(queryObject->backendHandle, /*wait=*/true, &result)) {
// The backend could not produce the result YET (e.g. a
// Vulkan wait refusing to block on a not-yet-submitted
// frame serial). Per the documented no-stall tradeoff
// this call reads 0, but the value is NOT cached and
// the backend handle is kept, so a later AVAILABLE
// poll / RESULT read still produces the real value.
outValue = 0;
return true;
}
// Final value produced (or no GetQueryResult64 hook: the
// query degrades to a zero result); the backend handle is
// consumed and the value cached for later reads.
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
deleteBackendQuery(queryObject->backendHandle);
}
queryObject->backendHandle = nullptr;
}
queryObject->cachedResult = result;
queryObject->resultCached = true;
outValue = result;
return true;
}
default:
RecordQueryError(ErrorCode::InvalidEnum, function, "Unsupported query object parameter.");
return false;
}
}
} // namespace
void GenQueries(GLsizei n, GLuint* ids) {
if (n < 0) {
RecordQueryError(ErrorCode::InvalidValue, __FUNCTION__, "n cannot be negative.");
return;
}
if (!ids) {
return;
}
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
for (GLsizei i = 0; i < n; ++i) {
const GLuint id = g_nextQueryId++;
auto* queryObject = new QueryObject;
queryObject->id = id;
g_liveQueryObjects[id] = queryObject;
ids[i] = id;
}
}
void DeleteQueries(GLsizei n, const GLuint* ids) {
if (n < 0) {
RecordQueryError(ErrorCode::InvalidValue, __FUNCTION__, "n cannot be negative.");
return;
}
if (!ids) {
return;
}
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
for (GLsizei i = 0; i < n; ++i) {
const auto it = g_liveQueryObjects.find(ids[i]);
if (it == g_liveQueryObjects.end()) {
continue; // unknown ids are silently ignored
}
QueryObject* queryObject = it->second;
if (queryObject->active) {
EndTimeElapsedQueryLocked(queryObject); // implicitly end before deletion
}
if (queryObject->backendHandle) {
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
deleteBackendQuery(queryObject->backendHandle);
}
queryObject->backendHandle = nullptr;
}
g_liveQueryObjects.erase(it);
delete queryObject;
}
}
GLboolean IsQuery(GLuint id) {
if (id == 0) {
return GL_FALSE;
}
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
// Gen'd ids count as query objects here: the registry creates live
// objects at GenQueries time.
return FindQueryObjectLocked(id) != nullptr ? GL_TRUE : GL_FALSE;
}
void BeginQuery(GLenum target, GLuint id) {
if (target != GL_TIME_ELAPSED) {
// Only GL_TIME_ELAPSED timer queries are implemented (occlusion and
// primitive queries remain stubs); GL_TIMESTAMP is not a valid
// BeginQuery target either.
RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "Query target is not supported.");
return;
}
if (id == 0) {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "Query id 0 cannot be used.");
return;
}
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
auto* queryObject = FindQueryObjectLocked(id);
if (!queryObject) {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "Query object does not exist.");
return;
}
if (g_activeTimeElapsedQueryId != 0) {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__,
"A query is already active on GL_TIME_ELAPSED.");
return;
}
if (queryObject->active) {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "Query object is already active.");
return;
}
if (queryObject->target != 0 && queryObject->target != target) {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__,
"Query object was already used with a different target.");
return;
}
ResetQueryObjectLocked(queryObject); // discard any previous result
queryObject->target = target;
queryObject->active = true;
const auto beginTimeElapsedQuery = MG_Backend::gBackendFunctionsTable.GL.BeginTimeElapsedQuery;
queryObject->backendHandle =
(!TimerQueryDisabled() && beginTimeElapsedQuery) ? beginTimeElapsedQuery() : nullptr;
g_activeTimeElapsedQueryId = id;
}
void EndQuery(GLenum target) {
if (target != GL_TIME_ELAPSED) {
RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "Query target is not supported.");
return;
}
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
if (g_activeTimeElapsedQueryId == 0) {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "No query is active on GL_TIME_ELAPSED.");
return;
}
auto* queryObject = FindQueryObjectLocked(g_activeTimeElapsedQueryId);
if (!queryObject) {
g_activeTimeElapsedQueryId = 0; // should not happen; keep state consistent
return;
}
EndTimeElapsedQueryLocked(queryObject);
}
void QueryCounter(GLuint id, GLenum target) {
if (target != GL_TIMESTAMP) {
RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "QueryCounter target must be GL_TIMESTAMP.");
return;
}
if (id == 0) {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "Query id 0 cannot be used.");
return;
}
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
auto* queryObject = FindQueryObjectLocked(id);
if (!queryObject) {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "Query object does not exist.");
return;
}
if (queryObject->active) {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "Query object is currently active.");
return;
}
if (queryObject->target != 0 && queryObject->target != target) {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__,
"Query object was already used with a different target.");
return;
}
ResetQueryObjectLocked(queryObject); // discard any previous result
queryObject->target = target;
const auto queryCounterTimestamp = MG_Backend::gBackendFunctionsTable.GL.QueryCounterTimestamp;
queryObject->backendHandle =
(!TimerQueryDisabled() && queryCounterTimestamp) ? queryCounterTimestamp() : nullptr;
queryObject->ended = true;
}
void GetQueryiv(GLenum target, GLenum pname, GLint* params) {
if (!params) {
return;
}
switch (pname) {
case GL_CURRENT_QUERY: {
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
// Only GL_TIME_ELAPSED queries can be active; GL_TIMESTAMP queries
// never are, and other targets remain unimplemented.
*params = target == GL_TIME_ELAPSED ? static_cast<GLint>(g_activeTimeElapsedQueryId) : 0;
return;
}
case GL_QUERY_COUNTER_BITS: {
// 64 bits are advertised only while the live backend can actually
// time: IsTimerQuerySupported is the dynamic truth (extension /
// entry points / timestamp valid bits at call time, not at table
// init), and the MOBILEGL_DISABLE_TIMERQUERY kill switch always
// wins. Non-timer targets remain unimplemented and report 0.
const Bool timerTarget = target == GL_TIME_ELAPSED || target == GL_TIMESTAMP;
const auto isTimerQuerySupported = MG_Backend::gBackendFunctionsTable.GL.IsTimerQuerySupported;
const Bool supported =
timerTarget && !TimerQueryDisabled() && isTimerQuerySupported && isTimerQuerySupported();
*params = supported ? 64 : 0;
return;
}
default:
RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "Unsupported query parameter.");
return;
}
}
void GetQueryObjectiv(GLuint id, GLenum pname, GLint* params) {
Uint64 value = 0;
if (!GetQueryObjectValue(id, pname, __FUNCTION__, value) || !params) {
return;
}
constexpr Uint64 kMaxInt = static_cast<Uint64>(INT_MAX);
*params = value > kMaxInt ? INT_MAX : static_cast<GLint>(value);
}
void GetQueryObjectuiv(GLuint id, GLenum pname, GLuint* params) {
Uint64 value = 0;
if (!GetQueryObjectValue(id, pname, __FUNCTION__, value) || !params) {
return;
}
*params = static_cast<GLuint>(value & 0xFFFFFFFFull);
}
void GetQueryObjecti64v(GLuint id, GLenum pname, GLint64* params) {
Uint64 value = 0;
if (!GetQueryObjectValue(id, pname, __FUNCTION__, value) || !params) {
return;
}
*params = static_cast<GLint64>(value);
}
void GetQueryObjectui64v(GLuint id, GLenum pname, GLuint64* params) {
Uint64 value = 0;
if (!GetQueryObjectValue(id, pname, __FUNCTION__, value) || !params) {
return;
}
*params = static_cast<GLuint64>(value);
}
} // namespace MobileGL::MG_Impl::GLImpl
+24
View File
@@ -0,0 +1,24 @@
// MobileGL - MobileGL/MG_Impl/GLImpl/Query/GL_Query.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_Impl::GLImpl {
void GenQueries(GLsizei n, GLuint* ids);
void DeleteQueries(GLsizei n, const GLuint* ids);
GLboolean IsQuery(GLuint id);
void BeginQuery(GLenum target, GLuint id);
void EndQuery(GLenum target);
void GetQueryiv(GLenum target, GLenum pname, GLint* params);
void GetQueryObjectiv(GLuint id, GLenum pname, GLint* params);
void GetQueryObjectuiv(GLuint id, GLenum pname, GLuint* params);
void GetQueryObjecti64v(GLuint id, GLenum pname, GLint64* params);
void GetQueryObjectui64v(GLuint id, GLenum pname, GLuint64* params);
void QueryCounter(GLuint id, GLenum target);
} // namespace MobileGL::MG_Impl::GLImpl
@@ -7,6 +7,7 @@
// End of Source File Header
#include "GL_RenderState.h"
#include <cmath>
#include <MG_State/GLState/Core.h>
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
#include <MG_Util/Converters/GLToMG/RenderStateEnumConverter.h>
@@ -14,6 +15,82 @@
#include <MG_Util/Converters/MGToStr/RenderStateEnumConverter.h>
namespace MobileGL::MG_Impl::GLImpl {
static Float ClampUnitFloat(GLfloat value) {
return std::clamp(static_cast<Float>(value), 0.0f, 1.0f);
}
static Bool ValidateIndexedBlendCapability(GLenum target, GLuint index, const char* functionName) {
if (target != GL_BLEND) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
"Only GL_BLEND is supported for indexed capability state."));
return false;
}
if (index >= MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", functionName,
"Buffer index " + std::to_string(index) + " is out of range. Max supported is " +
std::to_string(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS - 1) + "."));
return false;
}
return true;
}
static Bool TryConvertBlendEquation(GLenum mode, const char* functionName,
::MobileGL::BlendEquation& outEquation) {
outEquation = MG_Util::ConvertGLEnumToBlendEquation(mode);
if (outEquation != ::MobileGL::BlendEquation::Unknown) return true;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
"Blend equation enum " + MG_Util::ConvertGLEnumToString(mode) +
" is not supported."));
return false;
}
static Bool TryDecodeStencilFace(GLenum face, const char* functionName, Bool& applyFront, Bool& applyBack) {
switch (face) {
case GL_FRONT:
applyFront = true;
applyBack = false;
return true;
case GL_BACK:
applyFront = false;
applyBack = true;
return true;
case GL_FRONT_AND_BACK:
applyFront = true;
applyBack = true;
return true;
default:
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
"Stencil face enum " + MG_Util::ConvertGLEnumToString(face) +
" is not supported."));
return false;
}
}
static Bool TryConvertStencilOperation(GLenum value, const char* functionName, const char* paramName,
StencilOperation& outOperation) {
outOperation = MG_Util::ConvertGLEnumToStencilOperation(value);
if (outOperation != StencilOperation::Unknown) return true;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
String(paramName) + " enum " + MG_Util::ConvertGLEnumToString(value) +
" is not supported."));
return false;
}
void Viewport_State(GLint x, GLint y, GLsizei width, GLsizei height) {
if (width < 0 || height < 0) {
MG_State::pGLContext->RecordError(ErrorCode::InvalidValue,
@@ -26,27 +103,74 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void StencilOpSeparate_State(GLenum face, GLenum sfail, GLenum dpfail, GLenum dppass) {
// TODO: implement
Bool applyFront = false;
Bool applyBack = false;
if (!TryDecodeStencilFace(face, "StencilOpSeparate_State", applyFront, applyBack)) return;
StencilOperation failOp = StencilOperation::Unknown;
StencilOperation depthFailOp = StencilOperation::Unknown;
StencilOperation depthPassOp = StencilOperation::Unknown;
if (!TryConvertStencilOperation(sfail, "StencilOpSeparate_State", "sfail", failOp) ||
!TryConvertStencilOperation(dpfail, "StencilOpSeparate_State", "dpfail", depthFailOp) ||
!TryConvertStencilOperation(dppass, "StencilOpSeparate_State", "dppass", depthPassOp)) {
return;
}
if (applyFront) {
MG_State::pGLContext->SetStencilOp(StencilFace::Front, failOp, depthFailOp, depthPassOp);
}
if (applyBack) {
MG_State::pGLContext->SetStencilOp(StencilFace::Back, failOp, depthFailOp, depthPassOp);
}
}
void StencilOp_State(GLenum fail, GLenum zfail, GLenum zpass) {
// TODO: implement
StencilOpSeparate_State(GL_FRONT_AND_BACK, fail, zfail, zpass);
}
void StencilMaskSeparate_State(GLenum face, GLuint mask) {
// TODO: implement
Bool applyFront = false;
Bool applyBack = false;
if (!TryDecodeStencilFace(face, "StencilMaskSeparate_State", applyFront, applyBack)) return;
if (applyFront) {
MG_State::pGLContext->SetStencilMask(StencilFace::Front, mask);
}
if (applyBack) {
MG_State::pGLContext->SetStencilMask(StencilFace::Back, mask);
}
}
void StencilMask_State(GLuint mask) {
// TODO: implement
StencilMaskSeparate_State(GL_FRONT_AND_BACK, mask);
}
void StencilFuncSeparate_State(GLenum face, GLenum func, GLint ref, GLuint mask) {
// TODO: implement
Bool applyFront = false;
Bool applyBack = false;
if (!TryDecodeStencilFace(face, "StencilFuncSeparate_State", applyFront, applyBack)) return;
DepthTestFunc depthFunc = MG_Util::ConvertGLEnumToDepthTestFunc(func);
if (depthFunc == DepthTestFunc::Unknown) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "StencilFuncSeparate_State",
"Stencil func enum " + MG_Util::ConvertGLEnumToString(func) +
" is not supported."));
return;
}
const Int clampedRef = std::max(ref, 0);
if (applyFront) {
MG_State::pGLContext->SetStencilFunc(StencilFace::Front, depthFunc, clampedRef, mask);
}
if (applyBack) {
MG_State::pGLContext->SetStencilFunc(StencilFace::Back, depthFunc, clampedRef, mask);
}
}
void StencilFunc_State(GLenum func, GLint ref, GLuint mask) {
// TODO: implement
StencilFuncSeparate_State(GL_FRONT_AND_BACK, func, ref, mask);
}
void Scissor_State(GLint x, GLint y, GLsizei width, GLsizei height) {
@@ -61,27 +185,111 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void SampleCoverage_State(GLfloat value, GLboolean invert) {
// TODO: implement
MG_State::pGLContext->SetSampleCoverage(std::clamp(static_cast<Float>(value), 0.0f, 1.0f), invert == GL_TRUE);
}
void PolygonOffset_State(GLfloat factor, GLfloat units) {
// TODO: implement
MG_State::pGLContext->SetPolygonOffset(static_cast<Float>(factor), static_cast<Float>(units));
}
void PolygonMode_State(GLenum face, GLenum mode) {
// TODO: implement
// GL 3.3 core: separate front/back polygon modes were removed in 3.1, so the only legal
// face is GL_FRONT_AND_BACK. GL_FRONT / GL_BACK must be rejected (some desktop drivers
// leniently accept them, but that is non-conformant). Both errors are GL_INVALID_ENUM and
// leave state untouched.
if (face != GL_FRONT_AND_BACK) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"glPolygonMode face must be GL_FRONT_AND_BACK in the core profile; got " +
MG_Util::ConvertGLEnumToString(face) + "."));
return;
}
if (mode != GL_POINT && mode != GL_LINE && mode != GL_FILL) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"glPolygonMode mode must be GL_POINT, GL_LINE, or GL_FILL; got " +
MG_Util::ConvertGLEnumToString(mode) + "."));
return;
}
// Core sets both faces together; keep two slots so GL_POLYGON_MODE round-trips its two values.
MG_State::pGLContext->SetPolygonMode(mode, mode);
}
void PointSize_State(GLfloat size) {
// TODO: implement
if (size <= 0.0f) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "PointSize_State",
"Point size must be greater than zero."));
return;
}
MG_State::pGLContext->SetPointSize(static_cast<Float>(size));
}
void PointParameterf_State(GLenum pname, GLfloat param) {
// TODO: implement
// Single funnel for all four glPointParameter forms. The two GL 3.3 core pnames both carry one
// component, so the *v forms pass params[0], and the integer forms widen to float. The
// GL_POINT_SPRITE_COORD_ORIGIN value is an enum passed as a float (36001.0/36002.0 are exact).
void PointParameter_State(GLenum pname, GLfloat value) {
switch (pname) {
case GL_POINT_FADE_THRESHOLD_SIZE:
if (value < 0.0f) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"GL_POINT_FADE_THRESHOLD_SIZE must be non-negative."));
return;
}
MG_State::pGLContext->SetPointFadeThresholdSize(value);
return;
case GL_POINT_SPRITE_COORD_ORIGIN: {
const GLenum origin = static_cast<GLenum>(std::lround(value));
if (origin != GL_LOWER_LEFT && origin != GL_UPPER_LEFT) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"GL_POINT_SPRITE_COORD_ORIGIN must be GL_LOWER_LEFT or "
"GL_UPPER_LEFT."));
return;
}
MG_State::pGLContext->SetPointSpriteCoordOrigin(origin);
return;
}
default:
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"Unsupported point parameter pname: " + std::to_string(pname)));
return;
}
}
void PointParameterf_State(GLenum pname, GLfloat param) { PointParameter_State(pname, param); }
void PointParameteri_State(GLenum pname, GLint param) {
// TODO: implement
PointParameter_State(pname, static_cast<GLfloat>(param));
}
void PointParameterfv_State(GLenum pname, const GLfloat* params) {
if (!params) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "params pointer cannot be null."));
return;
}
PointParameter_State(pname, params[0]);
}
void PointParameteriv_State(GLenum pname, const GLint* params) {
if (!params) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "params pointer cannot be null."));
return;
}
PointParameter_State(pname, static_cast<GLfloat>(params[0]));
}
void PixelStorei_State(GLenum pname, GLint param) {
@@ -101,14 +309,36 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void LogicOp_State(GLenum opcode) {
// TODO: implement
LogicOperation logicOp = MG_Util::ConvertGLEnumToLogicOperation(opcode);
if (logicOp == LogicOperation::Unknown) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "LogicOp_State",
"Logic op enum " + MG_Util::ConvertGLEnumToString(opcode) +
" is not supported."));
return;
}
MG_State::pGLContext->SetLogicOp(logicOp);
}
void LineWidth_State(GLfloat width) {
// TODO: implement
if (width <= 0.0f) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "LineWidth_State",
"Line width must be greater than zero."));
return;
}
MG_State::pGLContext->SetLineWidth(static_cast<Float>(width));
}
GLboolean IsEnabledi_State(GLenum target, GLuint index) {
if (!ValidateIndexedBlendCapability(target, index, "IsEnabledi_State")) {
return GL_FALSE;
}
CapabilityInput capInput = MG_Util::ConvertGLEnumToCapabilityInput(target);
if (capInput == CapabilityInput::Unknown) {
MG_State::pGLContext->RecordError(
@@ -122,6 +352,37 @@ namespace MobileGL::MG_Impl::GLImpl {
return MG_State::pGLContext->IsCapabilityEnabledIndexed(capInput, index) ? GL_TRUE : GL_FALSE;
}
void GetBooleani_v_State(GLenum target, GLuint index, GLboolean* data) {
if (!data) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetBooleani_v_State",
"data pointer cannot be null."));
return;
}
if (target == GL_COLOR_WRITEMASK) {
// Indexed per-draw-buffer color writemask writes 4 booleans (R,G,B,A) for draw buffer
// `index`. The non-indexed glGetBooleanv(GL_COLOR_WRITEMASK) reports draw buffer 0.
if (index >= MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetBooleani_v_State",
"Color writemask draw buffer index " + std::to_string(index) +
" is out of range."));
return;
}
const BoolVec4 mask = MG_State::pGLContext->GetColorMaskIndexed(index);
data[0] = mask.x() ? GL_TRUE : GL_FALSE;
data[1] = mask.y() ? GL_TRUE : GL_FALSE;
data[2] = mask.z() ? GL_TRUE : GL_FALSE;
data[3] = mask.w() ? GL_TRUE : GL_FALSE;
return;
}
*data = IsEnabledi_State(target, index);
}
GLboolean IsEnabled_State(GLenum cap) {
CapabilityInput capInput = MG_Util::ConvertGLEnumToCapabilityInput(cap);
if (capInput == CapabilityInput::Unknown) {
@@ -137,14 +398,70 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void Hint_State(GLenum target, GLenum mode) {
// TODO: implement
switch (target) {
case GL_LINE_SMOOTH_HINT:
case GL_POLYGON_SMOOTH_HINT:
case GL_TEXTURE_COMPRESSION_HINT:
case GL_FRAGMENT_SHADER_DERIVATIVE_HINT:
break;
default:
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"Unsupported hint target: " + std::to_string(target)));
return;
}
if (mode != GL_FASTEST && mode != GL_NICEST && mode != GL_DONT_CARE) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"Hint mode must be GL_FASTEST, GL_NICEST or GL_DONT_CARE."));
return;
}
MG_State::pGLContext->SetHint(target, mode);
}
void FrontFace_State(GLenum mode) {
// TODO: implement
FrontFaceMode frontFaceMode = MG_Util::ConvertGLEnumToFrontFaceMode(mode);
if (frontFaceMode == FrontFaceMode::Unknown) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "FrontFace_State",
"Front face mode enum " +
MG_Util::ConvertFrontFaceModeToString(frontFaceMode) + "(" +
MG_Util::ConvertGLEnumToString(mode) + ") is not supported."));
return;
}
MG_State::pGLContext->SetFrontFaceMode(frontFaceMode);
}
void ProvokingVertex_State(GLenum mode) {
ProvokingVertexMode provokingVertexMode = MG_Util::ConvertGLEnumToProvokingVertexMode(mode);
if (provokingVertexMode == ProvokingVertexMode::Unknown) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ProvokingVertex_State",
"Provoking vertex mode enum " +
MG_Util::ConvertProvokingVertexModeToString(provokingVertexMode) +
"(" + MG_Util::ConvertGLEnumToString(mode) + ") is not supported."));
return;
}
MG_State::pGLContext->SetProvokingVertexMode(provokingVertexMode);
}
void Enable_State(GLenum cap) {
switch (cap) {
case GL_TEXTURE_1D:
case GL_TEXTURE_2D:
case GL_TEXTURE_3D:
case GL_TEXTURE_CUBE_MAP:
return;
default:
break;
}
CapabilityInput capInput = MG_Util::ConvertGLEnumToCapabilityInput(cap);
if (capInput == CapabilityInput::Unknown) {
MG_State::pGLContext->RecordError(
@@ -159,6 +476,16 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void Disable_State(GLenum cap) {
switch (cap) {
case GL_TEXTURE_1D:
case GL_TEXTURE_2D:
case GL_TEXTURE_3D:
case GL_TEXTURE_CUBE_MAP:
return;
default:
break;
}
CapabilityInput capInput = MG_Util::ConvertGLEnumToCapabilityInput(cap);
if (capInput == CapabilityInput::Unknown) {
MG_State::pGLContext->RecordError(
@@ -173,7 +500,8 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void DepthRange_State(GLclampd near_val, GLclampd far_val) {
// TODO: implement
MG_State::pGLContext->SetDepthRange(
FloatVec2(ClampUnitFloat(static_cast<GLfloat>(near_val)), ClampUnitFloat(static_cast<GLfloat>(far_val))));
}
void DepthMask_State(GLboolean flag) {
@@ -210,12 +538,59 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void ColorMask_State(GLboolean red, GLboolean green, GLboolean blue, GLboolean alpha) {
// glColorMask broadcasts to every draw buffer. GLboolean coercion: any nonzero value enables
// the component; only exactly GL_FALSE disables it.
MG_State::pGLContext->SetColorMask(
BoolVec4(red == GL_TRUE, green == GL_TRUE, blue == GL_TRUE, alpha == GL_TRUE));
BoolVec4(red != GL_FALSE, green != GL_FALSE, blue != GL_FALSE, alpha != GL_FALSE));
}
void ColorMaski_State(GLuint buf, GLboolean red, GLboolean green, GLboolean blue, GLboolean alpha) {
// Indexed color writemask for the single draw buffer `buf`. buf is a GLuint index, never an
// enum, so the only error is GL_INVALID_VALUE when it is out of range (mirrors the indexed
// blend entry points, which bound against MAX_DRAW_BUFFERS).
if (buf >= MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", __func__,
"glColorMaski buffer index " + std::to_string(buf) + " is out of range. Max supported is " +
std::to_string(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS - 1) + "."));
return;
}
MG_State::pGLContext->SetColorMaskIndexed(
buf, BoolVec4(red != GL_FALSE, green != GL_FALSE, blue != GL_FALSE, alpha != GL_FALSE));
}
void PrimitiveRestartIndex_State(GLuint index) {
// glPrimitiveRestartIndex accepts any GLuint and generates no error.
MG_State::pGLContext->SetPrimitiveRestartIndex(index);
}
void ClampColor_State(GLenum target, GLenum clamp) {
// TODO: implement
// GL 3.3 core: the only legal target is GL_CLAMP_READ_COLOR. The compatibility-only
// GL_CLAMP_VERTEX_COLOR / GL_CLAMP_FRAGMENT_COLOR were removed from the core profile and
// must be rejected.
if (target != GL_CLAMP_READ_COLOR) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"glClampColor target must be GL_CLAMP_READ_COLOR in the core profile; "
"got " +
MG_Util::ConvertGLEnumToString(target) + "."));
return;
}
// clamp must be one of GL_TRUE, GL_FALSE, or GL_FIXED_ONLY. NOTE: the Khronos man page's
// Errors section wrongly omits GL_FIXED_ONLY, but the spec lists it as legal AND it is the
// default value, so it must be accepted here.
if (clamp != GL_TRUE && clamp != GL_FALSE && clamp != GL_FIXED_ONLY) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"glClampColor clamp must be GL_TRUE, GL_FALSE, or GL_FIXED_ONLY; got " +
MG_Util::ConvertGLEnumToString(clamp) + "."));
return;
}
MG_State::pGLContext->SetClampReadColor(clamp);
}
void BlendFuncSeparate_State(GLenum sfactorRGB, GLenum dfactorRGB, GLenum sfactorAlpha, GLenum dfactorAlpha) {
@@ -249,11 +624,24 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void BlendEquation_State(GLenum mode) {
// TODO: implement
::MobileGL::BlendEquation blendEquation = ::MobileGL::BlendEquation::Unknown;
if (!TryConvertBlendEquation(mode, "BlendEquation_State", blendEquation)) return;
MG_State::pGLContext->SetBlendEquation(blendEquation, blendEquation);
}
void BlendEquationSeparate_State(GLenum modeRGB, GLenum modeAlpha) {
::MobileGL::BlendEquation colorEquation = ::MobileGL::BlendEquation::Unknown;
if (!TryConvertBlendEquation(modeRGB, "BlendEquationSeparate_State", colorEquation)) return;
::MobileGL::BlendEquation alphaEquation = ::MobileGL::BlendEquation::Unknown;
if (!TryConvertBlendEquation(modeAlpha, "BlendEquationSeparate_State", alphaEquation)) return;
MG_State::pGLContext->SetBlendEquation(colorEquation, alphaEquation);
}
void BlendColor_State(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha) {
// TODO: implement
MG_State::pGLContext->SetBlendColor(
FloatVec4(ClampUnitFloat(red), ClampUnitFloat(green), ClampUnitFloat(blue), ClampUnitFloat(alpha)));
}
void ClearStencil_State(GLint s) {
@@ -261,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) {
@@ -283,10 +673,50 @@ namespace MobileGL::MG_Impl::GLImpl {
BlendFactor dstRGBM = MG_Util::ConvertGLEnumToBlendFactor(dstRGB);
BlendFactor srcAlphaM = MG_Util::ConvertGLEnumToBlendFactor(srcAlpha);
BlendFactor dstAlphaM = MG_Util::ConvertGLEnumToBlendFactor(dstAlpha);
if (srcRGBM == BlendFactor::Unknown || dstRGBM == BlendFactor::Unknown ||
srcAlphaM == BlendFactor::Unknown || dstAlphaM == BlendFactor::Unknown) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "BlendFuncSeparatei_State",
"One of the indexed blend factor enums is not supported."));
return;
}
MG_State::pGLContext->SetBlendFuncIndexed(buf, srcRGBM, dstRGBM, srcAlphaM, dstAlphaM);
}
void BlendFunci_State(GLuint buf, GLenum src, GLenum dst) {
BlendFuncSeparatei_State(buf, src, dst, src, dst);
}
void BlendEquationSeparatei_State(GLuint buf, GLenum modeRGB, GLenum modeAlpha) {
if (buf >= MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", "BlendEquationSeparatei_State",
"Buffer index " + std::to_string(buf) + " is out of range. Max supported is " +
std::to_string(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS - 1) + "."));
return;
}
::MobileGL::BlendEquation colorEquation = ::MobileGL::BlendEquation::Unknown;
if (!TryConvertBlendEquation(modeRGB, "BlendEquationSeparatei_State", colorEquation)) return;
::MobileGL::BlendEquation alphaEquation = ::MobileGL::BlendEquation::Unknown;
if (!TryConvertBlendEquation(modeAlpha, "BlendEquationSeparatei_State", alphaEquation)) return;
MG_State::pGLContext->SetBlendEquationIndexed(buf, colorEquation, alphaEquation);
}
void BlendEquationi_State(GLuint buf, GLenum mode) {
BlendEquationSeparatei_State(buf, mode, mode);
}
void Disablei_State(GLenum target, GLuint index) {
if (!ValidateIndexedBlendCapability(target, index, "Disablei_State")) {
return;
}
auto capInput = MG_Util::ConvertGLEnumToCapabilityInput(target);
if (capInput == CapabilityInput::Unknown) {
MG_State::pGLContext->RecordError(
@@ -301,6 +731,10 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void Enablei_State(GLenum target, GLuint index) {
if (!ValidateIndexedBlendCapability(target, index, "Enablei_State")) {
return;
}
auto capInput = MG_Util::ConvertGLEnumToCapabilityInput(target);
if (capInput == CapabilityInput::Unknown) {
MG_State::pGLContext->RecordError(
@@ -315,10 +749,26 @@ namespace MobileGL::MG_Impl::GLImpl {
}
/* @INSERTION_POINT:FUNCTION_IMPLEMENTATION@ */
void BlendEquationi(GLuint buf, GLenum mode) {
BlendEquationi_State(buf, mode);
}
void BlendEquationSeparatei(GLuint buf, GLenum modeRGB, GLenum modeAlpha) {
BlendEquationSeparatei_State(buf, modeRGB, modeAlpha);
}
void BlendFunci(GLuint buf, GLenum src, GLenum dst) {
BlendFunci_State(buf, src, dst);
}
void BlendFuncSeparatei(GLuint buf, GLenum srcRGB, GLenum dstRGB, GLenum srcAlpha, GLenum dstAlpha) {
BlendFuncSeparatei_State(buf, srcRGB, dstRGB, srcAlpha, dstAlpha);
}
void GetBooleani_v(GLenum target, GLuint index, GLboolean* data) {
GetBooleani_v_State(target, index, data);
}
void Disablei(GLenum target, GLuint index) {
Disablei_State(target, index);
}
@@ -387,10 +837,36 @@ namespace MobileGL::MG_Impl::GLImpl {
PointParameteri_State(pname, param);
}
void PointParameterfv(GLenum pname, const GLfloat* params) {
PointParameterfv_State(pname, params);
}
void PointParameteriv(GLenum pname, const GLint* params) {
PointParameteriv_State(pname, params);
}
void PixelStorei(GLenum pname, GLint param) {
PixelStorei_State(pname, param);
}
void PixelStoref(GLenum pname, GLfloat param) {
// Boolean pixel-store pnames convert by a zero-test (0.4 -> TRUE); integer pnames round to
// nearest. Branch before converting so a fractional value cannot round a true flag to false.
GLint intParam;
switch (pname) {
case GL_PACK_SWAP_BYTES:
case GL_UNPACK_SWAP_BYTES:
case GL_PACK_LSB_FIRST:
case GL_UNPACK_LSB_FIRST:
intParam = (param != 0.0f) ? 1 : 0;
break;
default:
intParam = static_cast<GLint>(std::lround(param));
break;
}
PixelStorei_State(pname, intParam);
}
void LogicOp(GLenum opcode) {
LogicOp_State(opcode);
}
@@ -415,6 +891,10 @@ namespace MobileGL::MG_Impl::GLImpl {
FrontFace_State(mode);
}
void ProvokingVertex(GLenum mode) {
ProvokingVertex_State(mode);
}
void Enable(GLenum cap) {
Enable_State(cap);
}
@@ -443,10 +923,18 @@ namespace MobileGL::MG_Impl::GLImpl {
ColorMask_State(red, green, blue, alpha);
}
void ColorMaski(GLuint index, GLboolean red, GLboolean green, GLboolean blue, GLboolean alpha) {
ColorMaski_State(index, red, green, blue, alpha);
}
void ClampColor(GLenum target, GLenum clamp) {
ClampColor_State(target, clamp);
}
void PrimitiveRestartIndex(GLuint index) {
PrimitiveRestartIndex_State(index);
}
void BlendFuncSeparate(GLenum sfactorRGB, GLenum dfactorRGB, GLenum sfactorAlpha, GLenum dfactorAlpha) {
BlendFuncSeparate_State(sfactorRGB, dfactorRGB, sfactorAlpha, dfactorAlpha);
}
@@ -455,6 +943,10 @@ namespace MobileGL::MG_Impl::GLImpl {
BlendEquation_State(mode);
}
void BlendEquationSeparate(GLenum modeRGB, GLenum modeAlpha) {
BlendEquationSeparate_State(modeRGB, modeAlpha);
}
void BlendColor(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha) {
BlendColor_State(red, green, blue, alpha);
}
@@ -11,7 +11,11 @@
namespace MobileGL::MG_Impl::GLImpl {
/* @INSERTION_POINT:FUNCTION_DECLARATION@ */
void BlendEquationi(GLuint buf, GLenum mode);
void BlendEquationSeparatei(GLuint buf, GLenum modeRGB, GLenum modeAlpha);
void BlendFunci(GLuint buf, GLenum src, GLenum dst);
void BlendFuncSeparatei(GLuint buf, GLenum srcRGB, GLenum dstRGB, GLenum srcAlpha, GLenum dstAlpha);
void GetBooleani_v(GLenum target, GLuint index, GLboolean* data);
void Disablei(GLenum target, GLuint index);
void Enablei(GLenum target, GLuint index);
void BlendFunc(GLenum sfactor, GLenum dfactor);
@@ -29,13 +33,17 @@ namespace MobileGL::MG_Impl::GLImpl {
void PointSize(GLfloat size);
void PointParameterf(GLenum pname, GLfloat param);
void PointParameteri(GLenum pname, GLint param);
void PointParameterfv(GLenum pname, const GLfloat* params);
void PointParameteriv(GLenum pname, const GLint* params);
void PixelStorei(GLenum pname, GLint param);
void PixelStoref(GLenum pname, GLfloat param);
void LogicOp(GLenum opcode);
void LineWidth(GLfloat width);
GLboolean IsEnabledi(GLenum target, GLuint index);
GLboolean IsEnabled(GLenum cap);
void Hint(GLenum target, GLenum mode);
void FrontFace(GLenum mode);
void ProvokingVertex(GLenum mode);
void Enable(GLenum cap);
void Disable(GLenum cap);
void DepthRange(GLclampd near_val, GLclampd far_val);
@@ -43,9 +51,12 @@ namespace MobileGL::MG_Impl::GLImpl {
void DepthFunc(GLenum func);
void CullFace(GLenum mode);
void ColorMask(GLboolean red, GLboolean green, GLboolean blue, GLboolean alpha);
void ColorMaski(GLuint index, GLboolean red, GLboolean green, GLboolean blue, GLboolean alpha);
void ClampColor(GLenum target, GLenum clamp);
void PrimitiveRestartIndex(GLuint index);
void BlendFuncSeparate(GLenum sfactorRGB, GLenum dfactorRGB, GLenum sfactorAlpha, GLenum dfactorAlpha);
void BlendEquation(GLenum mode);
void BlendEquationSeparate(GLenum modeRGB, GLenum modeAlpha);
void BlendColor(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha);
void ClearStencil(GLint s);
void ClearDepth(GLclampd depth);
+69 -5
View File
@@ -13,7 +13,45 @@
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
namespace MobileGL::MG_Impl::GLImpl {
void SetSamplerParam_State(GLuint sampler, GLenum pname, const void* param, bool isFloat, bool isInteger) {
namespace {
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) {
case GL_TEXTURE_MIN_LOD:
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;
}
if (isFloat) {
return SamplerImpl::ValidateSamplerFloatParam(pname, *(const GLfloat*)param);
}
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 isUnsignedInteger) {
if (param == nullptr) return;
if (!SamplerImpl::ValidateSamplerName(sampler)) return;
Bool doesSamplerObjectCreated = MG_State::pGLContext->ValidateSamplerObject(sampler);
@@ -23,6 +61,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
auto& samplerObj = MG_State::pGLContext->GetSamplerObject(sampler);
if (!SamplerImpl::ValidateSamplerObject(sampler)) return;
if (!ValidateSamplerParameterValue(pname, param, isFloat, isUnsignedInteger)) return;
using namespace MG_Util;
switch (pname) {
@@ -51,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;
@@ -64,7 +106,9 @@ 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;
Bool doesSamplerObjectCreated = MG_State::pGLContext->ValidateSamplerObject(sampler);
@@ -103,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;
@@ -167,6 +220,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void BindSampler_State(GLuint unit, GLuint sampler) {
MGLOG_D("BindSampler_State: unit = %u, sampler = %u", unit, sampler);
if (unit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
@@ -175,10 +229,20 @@ namespace MobileGL::MG_Impl::GLImpl {
}
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject((Int)unit);
MG_State::pGLContext->NoteTextureUnitTouched((Int)unit);
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);
@@ -212,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) {
@@ -240,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));
}
+115 -22
View File
@@ -7,37 +7,130 @@
// End of Source File Header
#include "GL_Sync.h"
#include "MG_State/GLState/Core.h"
#include <MG_Backend/BackendObjects.h>
namespace MobileGL::MG_Impl::GLImpl {
GLsync FenceSync_Backend(GLenum condition, GLbitfield flags) {
return 0;
}
namespace {
// Frontend sync object: wraps an optional backend fence handle. A null
// backend handle (backend has no fence support, or could not create a
// fence at call time) keeps the legacy always-signaled behavior.
struct SyncObject {
MG_Backend::BackendSyncHandle backendHandle = nullptr;
GLenum condition = GL_SYNC_GPU_COMMANDS_COMPLETE;
GLbitfield flags = 0;
};
GLenum ClientWaitSync_Backend(GLsync sync, GLbitfield flags, GLuint64 timeout) {
return 0;
}
// Sync calls may arrive from any thread (launchers migrate the context
// across JVM threads), so the live-object registry is mutex-guarded.
// Entries left at process shutdown are simply dropped; their backend
// handles die with the backend.
std::mutex g_syncObjectsMutex;
UnorderedMap<GLsync, SyncObject*> g_liveSyncObjects;
void DeleteSync_Backend(GLsync sync) {}
GLsync FenceSync_State(GLenum condition, GLbitfield flags) {
return 0;
}
GLenum ClientWaitSync_State(GLsync sync, GLbitfield flags, GLuint64 timeout) {
return 0;
}
void DeleteSync_State(GLsync sync) {}
SyncObject* FindSyncObject(GLsync sync) {
const std::lock_guard<std::mutex> lock(g_syncObjectsMutex);
const auto it = g_liveSyncObjects.find(sync);
return it != g_liveSyncObjects.end() ? it->second : nullptr;
}
} // namespace
GLsync FenceSync(GLenum condition, GLbitfield flags) {
return 0;
auto* syncObject = new SyncObject;
syncObject->condition = condition;
syncObject->flags = flags;
if (const auto backendFenceSync = MG_Backend::gBackendFunctionsTable.GL.FenceSync) {
syncObject->backendHandle = backendFenceSync();
}
const GLsync handle = reinterpret_cast<GLsync>(syncObject);
const std::lock_guard<std::mutex> lock(g_syncObjectsMutex);
g_liveSyncObjects[handle] = syncObject;
return handle;
}
GLboolean IsSync(GLsync sync) {
return FindSyncObject(sync) != nullptr ? GL_TRUE : GL_FALSE;
}
GLenum ClientWaitSync(GLsync sync, GLbitfield flags, GLuint64 timeout) {
return 0;
const auto* syncObject = FindSyncObject(sync);
if (!syncObject) {
return GL_WAIT_FAILED;
}
const auto backendClientWaitSync = MG_Backend::gBackendFunctionsTable.GL.ClientWaitSync;
if (!backendClientWaitSync || !syncObject->backendHandle) {
return GL_ALREADY_SIGNALED; // legacy always-signaled fallback
}
return backendClientWaitSync(syncObject->backendHandle, flags, timeout);
}
void DeleteSync(GLsync sync) {}
void WaitSync(GLsync sync, GLbitfield flags, GLuint64 timeout) {
const auto* syncObject = FindSyncObject(sync);
if (!syncObject) {
return;
}
const auto backendWaitSync = MG_Backend::gBackendFunctionsTable.GL.WaitSync;
if (backendWaitSync && syncObject->backendHandle) {
backendWaitSync(syncObject->backendHandle, flags, timeout);
}
}
void DeleteSync(GLsync sync) {
if (sync == nullptr) {
return; // glDeleteSync(0) is silently ignored
}
SyncObject* syncObject = nullptr;
{
const std::lock_guard<std::mutex> lock(g_syncObjectsMutex);
const auto it = g_liveSyncObjects.find(sync);
if (it == g_liveSyncObjects.end()) {
return;
}
syncObject = it->second;
g_liveSyncObjects.erase(it);
}
const auto backendDeleteSync = MG_Backend::gBackendFunctionsTable.GL.DeleteSync;
if (backendDeleteSync && syncObject->backendHandle) {
backendDeleteSync(syncObject->backendHandle);
}
delete syncObject;
}
void GetSynciv(GLsync sync, GLenum pname, GLsizei bufSize, GLsizei* length, GLint* values) {
const auto* syncObject = FindSyncObject(sync);
if (!syncObject) {
if (length) {
*length = 0;
}
return;
}
GLint value = 0;
switch (pname) {
case GL_OBJECT_TYPE:
value = GL_SYNC_FENCE;
break;
case GL_SYNC_STATUS: {
const auto backendGetSyncStatus = MG_Backend::gBackendFunctionsTable.GL.GetSyncStatus;
const Bool signaled = !backendGetSyncStatus || !syncObject->backendHandle ||
backendGetSyncStatus(syncObject->backendHandle);
value = signaled ? GL_SIGNALED : GL_UNSIGNALED;
break;
}
case GL_SYNC_CONDITION:
value = static_cast<GLint>(syncObject->condition);
break;
case GL_SYNC_FLAGS:
value = static_cast<GLint>(syncObject->flags);
break;
default:
break;
}
if (length) {
*length = bufSize > 0 && values ? 1 : 0;
}
if (bufSize > 0 && values) {
values[0] = value;
}
}
} // namespace MobileGL::MG_Impl::GLImpl
+3
View File
@@ -11,6 +11,9 @@
namespace MobileGL::MG_Impl::GLImpl {
GLsync FenceSync(GLenum condition, GLbitfield flags);
GLboolean IsSync(GLsync sync);
GLenum ClientWaitSync(GLsync sync, GLbitfield flags, GLuint64 timeout);
void WaitSync(GLsync sync, GLbitfield flags, GLuint64 timeout);
void DeleteSync(GLsync sync);
void GetSynciv(GLsync sync, GLenum pname, GLsizei bufSize, GLsizei* length, GLint* values);
} // namespace MobileGL::MG_Impl::GLImpl
File diff suppressed because it is too large Load Diff
@@ -11,8 +11,53 @@
namespace MobileGL::MG_Impl::GLImpl {
/* @INSERTION_POINT:FUNCTION_DECLARATION@ */
void ClearTexImage(GLuint texture, GLint level, GLenum format, GLenum type, const void* data);
void ClearTexSubImage(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width,
GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* data);
void BindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access,
GLenum format);
void GenerateMipmap(GLenum target);
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels);
void CreateTextures(GLenum target, GLsizei n, GLuint* textures);
void TextureStorage1D(GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width);
void TextureStorage2D(GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height);
void TextureStorage3D(GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height,
GLsizei depth);
void TextureStorage2DMultisample(GLuint texture, GLsizei samples, GLenum internalformat, GLsizei width,
GLsizei height, GLboolean fixedsamplelocations);
void TextureStorage3DMultisample(GLuint texture, GLsizei samples, GLenum internalformat, GLsizei width,
GLsizei height, GLsizei depth, GLboolean fixedsamplelocations);
void TextureSubImage1D(GLuint texture, GLint level, GLint xoffset, GLsizei width, GLenum format, GLenum type,
const void* pixels);
void TextureSubImage2D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height,
GLenum format, GLenum type, const void* pixels);
void TextureSubImage3D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width,
GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* pixels);
void TextureParameterf(GLuint texture, GLenum pname, GLfloat param);
void TextureParameterfv(GLuint texture, GLenum pname, const GLfloat* params);
void TextureParameteri(GLuint texture, GLenum pname, GLint param);
void TextureParameterIiv(GLuint texture, GLenum pname, const GLint* params);
void TextureParameterIuiv(GLuint texture, GLenum pname, const GLuint* params);
void TextureParameteriv(GLuint texture, GLenum pname, const GLint* params);
void GenerateTextureMipmap(GLuint texture);
void BindTextureUnit(GLuint unit, GLuint texture);
void GetTextureImage(GLuint texture, GLint level, GLenum format, GLenum type, GLsizei bufSize, void* pixels);
void GetTextureSubImage(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width,
GLsizei height, GLsizei depth, GLenum format, GLenum type, GLsizei bufSize, void* pixels);
void GetTextureParameterfv(GLuint texture, GLenum pname, GLfloat* params);
void GetTextureParameterIiv(GLuint texture, GLenum pname, GLint* params);
void GetTextureParameterIuiv(GLuint texture, GLenum pname, GLuint* params);
void GetTextureParameteriv(GLuint texture, GLenum pname, GLint* params);
void GetTextureLevelParameterfv(GLuint texture, GLint level, GLenum pname, GLfloat* params);
void GetTextureLevelParameteriv(GLuint texture, GLint level, GLenum pname, GLint* params);
void TexStorage1D(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width);
void TexStorage2D(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height);
void TexStorage3D(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height,
GLsizei depth);
void TexStorage2DMultisample(GLenum target, GLsizei samples, GLenum internalformat, GLsizei width,
GLsizei height, GLboolean fixedsamplelocations);
void TexStorage3DMultisample(GLenum target, GLsizei samples, GLenum internalformat, GLsizei width,
GLsizei height, GLsizei depth, GLboolean fixedsamplelocations);
void TexSubImage3D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width,
GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* pixels);
void TexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height,
@@ -44,6 +89,8 @@ namespace MobileGL::MG_Impl::GLImpl {
void GetTexParameterfv(GLenum target, GLenum pname, GLfloat* params);
void GetTexLevelParameteriv(GLenum target, GLint level, GLenum pname, GLint* params);
void GetTexLevelParameterfv(GLenum target, GLint level, GLenum pname, GLfloat* params);
void GetMultisamplefv(GLenum pname, GLuint index, GLfloat* val);
void GetInternalformativ(GLenum target, GLenum internalformat, GLenum pname, GLsizei bufSize, GLint* params);
void GetCompressedTexImage(GLenum target, GLint level, void* img);
void GenTextures(GLsizei n, GLuint* textures);
void DeleteTextures(GLsizei n, const GLuint* textures);
@@ -51,11 +98,16 @@ namespace MobileGL::MG_Impl::GLImpl {
GLsizei width, GLsizei height);
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
GLsizei height);
void CopyTextureSubImage2D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y,
GLsizei width, GLsizei height);
void CopyTexSubImage1D(GLenum target, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width);
void CopyTexImage2D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width,
GLsizei height, GLint border);
void CopyTexImage1D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width,
GLint border);
void CopyImageSubData(GLuint srcName, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
GLuint dstName, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
void CompressedTexSubImage3D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width,
GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* data);
void CompressedTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width,
@@ -7,10 +7,15 @@
// End of Source File Header
#include "ProxyTexture.h"
#include <MG_State/GLState/TextureState/TextureObject1D.h>
#include <MG_State/GLState/TextureState/TextureObject2D.h>
#include <MG_State/GLState/TextureState/TextureObject2DCube.h>
#include <MG_State/GLState/TextureState/TextureObject3D.h>
#include <MG_State/GLState/TextureState/TextureObjectStubs.h>
namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
UniquePtr<ProxyTextureManager> pProxyTextureManager;
// Leak-at-exit storage; see GlobalObjects.cpp.
UniquePtr<ProxyTextureManager>& pProxyTextureManager = *new UniquePtr<ProxyTextureManager>();
Bool IsProxyTextureTarget(TextureUploadTarget target) {
switch (target) {
@@ -37,7 +42,39 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
m_proxyTexturesMap.erase(it);
}
auto& obj = m_proxyTexturesMap[target];
obj = MakeShared<MG_State::GLState::TextureObject2D>(0);
switch (target) {
case TextureUploadTarget::ProxyTexture1D:
case TextureUploadTarget::ProxyTexture1DArray:
obj = MakeShared<MG_State::GLState::TextureObject1D>(0);
break;
case TextureUploadTarget::ProxyTexture2D:
obj = MakeShared<MG_State::GLState::TextureObject2D>(0);
break;
case TextureUploadTarget::ProxyTexture3D:
obj = MakeShared<MG_State::GLState::TextureObject3D>(0);
break;
case TextureUploadTarget::ProxyCubeMap:
obj = MakeShared<MG_State::GLState::TextureObject2DCube>(0);
break;
case TextureUploadTarget::ProxyTextureRectangle:
obj = MakeShared<MG_State::GLState::TextureObjectRectangle>(0);
break;
case TextureUploadTarget::ProxyTexture2DArray:
obj = MakeShared<MG_State::GLState::TextureObject2DArray>(0);
break;
case TextureUploadTarget::ProxyCubeMapArray:
obj = MakeShared<MG_State::GLState::TextureObjectCubeMapArray>(0);
break;
case TextureUploadTarget::ProxyTexture2DMultisample:
obj = MakeShared<MG_State::GLState::TextureObject2DMultisample>(0);
break;
case TextureUploadTarget::ProxyTexture2DMultisampleArray:
obj = MakeShared<MG_State::GLState::TextureObject2DMultisampleArray>(0);
break;
default:
obj = MakeShared<MG_State::GLState::TextureObject2D>(0);
break;
}
return obj;
}
@@ -23,5 +23,5 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
UnorderedMap<TextureUploadTarget, SharedPtr<MG_State::GLState::ITextureObject>> m_proxyTexturesMap;
};
extern UniquePtr<ProxyTextureManager> pProxyTextureManager;
extern UniquePtr<ProxyTextureManager>& pProxyTextureManager;
} // namespace MobileGL::MG_Impl::GLImpl::TextureImpl
+170 -40
View File
@@ -7,6 +7,7 @@
// End of Source File Header
#include "Validators.h"
#include <MG_Backend/BackendObjects.h>
#include <MG_State/GLState/Core.h>
#include <MG_State/GLState/ErrorState/Error.h>
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
@@ -83,8 +84,20 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
return false;
}
// TODO: GL_INVALID_VALUE may be generated if level is greater than log2(max), where max is the returned
// value of GL_MAX_TEXTURE_SIZE.
Int maxTextureSize = MG_Backend::DynamicBackendParameters{}.MaxTextureSize;
if (MG_Backend::pActiveBackendObject) {
maxTextureSize = MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxTextureSize;
}
Int maxLevel = 0;
for (Int size = std::max(maxTextureSize, 1); size > 1; size >>= 1) {
++maxLevel;
}
if (level > maxLevel) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateTextureLevelNumber",
"Texture level exceeds GL_MAX_TEXTURE_SIZE"));
return false;
}
return true;
}
@@ -129,7 +142,7 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
return true;
}
Bool ValidateTextureSizeRange(SizeT width, SizeT height, SizeT depth) {
Bool ValidateTextureSizeRange(Int width, Int height, Int depth) {
if (width < 0 || height < 0 || depth < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateTextureSizeRange",
@@ -162,61 +175,153 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
return true;
}
Bool ValidateTextureInternalFormatCompatibleWithInput(TextureInputFormat format,
TextureInternalFormat internalFormat,
TexturePixelDataType type) {
if (type == TexturePixelDataType::UnsignedByte332 || type == TexturePixelDataType::UnsignedByte233Rev ||
type == TexturePixelDataType::UnsignedShort565 || type == TexturePixelDataType::UnsignedShort565Rev ||
type == TexturePixelDataType::UnsignedInt101111Rev) {
if (format != TextureInputFormat::RGB) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateTextureInternalFormatCompatibleWithInput",
"Invalid format for the given type"));
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::GreenInteger || format == TextureInputFormat::BlueInteger ||
format == TextureInputFormat::AlphaInteger;
}
Bool IsIntegerColorInternalFormat(TextureInternalFormat internalFormat) {
switch (internalFormat) {
case TextureInternalFormat::R8I:
case TextureInternalFormat::R8UI:
case TextureInternalFormat::R16I:
case TextureInternalFormat::R16UI:
case TextureInternalFormat::R32I:
case TextureInternalFormat::R32UI:
case TextureInternalFormat::RG8I:
case TextureInternalFormat::RG8UI:
case TextureInternalFormat::RG16I:
case TextureInternalFormat::RG16UI:
case TextureInternalFormat::RG32I:
case TextureInternalFormat::RG32UI:
case TextureInternalFormat::RGB8I:
case TextureInternalFormat::RGB8UI:
case TextureInternalFormat::RGB16I:
case TextureInternalFormat::RGB16UI:
case TextureInternalFormat::RGB32I:
case TextureInternalFormat::RGB32UI:
case TextureInternalFormat::RGBA8I:
case TextureInternalFormat::RGBA8UI:
case TextureInternalFormat::RGBA16I:
case TextureInternalFormat::RGBA16UI:
case TextureInternalFormat::RGBA32I:
case TextureInternalFormat::RGBA32UI:
case TextureInternalFormat::RGB10A2UI:
return true;
default:
return false;
}
}
static Bool IsDepthLikeInternalFormat(TextureInternalFormat internalFormat) {
switch (internalFormat) {
case TextureInternalFormat::DepthComponent:
case TextureInternalFormat::DepthComponent16:
case TextureInternalFormat::DepthComponent24:
case TextureInternalFormat::DepthComponent32: // not core, kept for Minecraft 1.21.5+
case TextureInternalFormat::DepthComponent32F:
case TextureInternalFormat::Depth24Stencil8:
case TextureInternalFormat::Depth32FStencil8:
case TextureInternalFormat::DepthStencil:
return true;
default:
return false;
}
}
static Bool IsDepthLikeInputFormat(TextureInputFormat format) {
return format == TextureInputFormat::DepthComponent || format == TextureInputFormat::DepthStencil ||
format == TextureInputFormat::StencilIndex;
}
// 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", "ValidateClientFormatTypePairing", message));
return false;
};
if (type == TexturePixelDataType::UnsignedByte332 || type == TexturePixelDataType::UnsignedByte233Rev ||
type == TexturePixelDataType::UnsignedShort565 || type == TexturePixelDataType::UnsignedShort565Rev) {
if (format != TextureInputFormat::RGB && format != TextureInputFormat::RGBInteger) {
return recordInvalidOperation("Packed RGB type requires RGB or RGB_INTEGER format");
}
}
if (type == TexturePixelDataType::UnsignedInt101111Rev || type == TexturePixelDataType::UnsignedInt5999Rev) {
if (format != TextureInputFormat::RGB) {
return recordInvalidOperation("Packed float RGB type requires RGB format");
}
}
if (type == TexturePixelDataType::UnsignedShort4444 || type == TexturePixelDataType::UnsignedShort4444Rev ||
type == TexturePixelDataType::UnsignedShort5551 || type == TexturePixelDataType::UnsignedShort1555Rev ||
type == TexturePixelDataType::UnsignedInt8888 || type == TexturePixelDataType::UnsignedInt8888Rev ||
type == TexturePixelDataType::UnsignedInt1010102 || type == TexturePixelDataType::UnsignedInt2101010Rev ||
type == TexturePixelDataType::UnsignedInt5999Rev) {
if (format != TextureInputFormat::RGBA && format != TextureInputFormat::BGRA) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateTextureInternalFormatCompatibleWithInput",
"Invalid format for the given type"));
return false;
type == TexturePixelDataType::UnsignedInt1010102 || type == TexturePixelDataType::UnsignedInt2101010Rev) {
if (format != TextureInputFormat::RGBA && format != TextureInputFormat::BGRA &&
format != TextureInputFormat::RGBAInteger && format != TextureInputFormat::BGRAInteger) {
return recordInvalidOperation("Packed RGBA type requires RGBA/BGRA (integer) format");
}
}
if (internalFormat == TextureInternalFormat::DepthComponent ||
internalFormat == TextureInternalFormat::DepthComponent16 ||
internalFormat == TextureInternalFormat::DepthComponent24 ||
internalFormat == TextureInternalFormat::DepthComponent32F) {
if (format != TextureInputFormat::DepthComponent) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateTextureInternalFormatCompatibleWithInput",
"Invalid format for depth component internal format"));
return false;
if (type == TexturePixelDataType::UnsignedInt248 || type == TexturePixelDataType::Float32UnsignedInt248Rev) {
if (format != TextureInputFormat::DepthStencil) {
return recordInvalidOperation("Packed depth-stencil type requires DEPTH_STENCIL format");
}
}
if (format == TextureInputFormat::DepthComponent &&
(internalFormat != TextureInternalFormat::DepthComponent &&
internalFormat != TextureInternalFormat::DepthComponent16 &&
internalFormat != TextureInternalFormat::DepthComponent24 &&
internalFormat != TextureInternalFormat::DepthComponent32F &&
internalFormat != TextureInternalFormat::DepthComponent32 // workaround for Minecraft 1.21.5+
)) {
if (format == TextureInputFormat::DepthStencil && type != TexturePixelDataType::UnsignedInt248 &&
type != TexturePixelDataType::Float32UnsignedInt248Rev) {
return recordInvalidOperation("DEPTH_STENCIL format requires a packed depth-stencil type");
}
if (IsIntegerColorInputFormat(format) &&
(type == TexturePixelDataType::Float || type == TexturePixelDataType::HalfFloat)) {
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",
"Invalid internal format for depth component format"));
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");
}
if (IsDepthLikeInputFormat(format) != IsDepthLikeInternalFormat(internalFormat)) {
return recordInvalidOperation("Depth/stencil-ness of format and internal format must match");
}
if (IsIntegerColorInputFormat(format) != IsIntegerColorInternalFormat(internalFormat)) {
return recordInvalidOperation("Integer-ness of format and internal format must match");
}
return true;
}
@@ -230,6 +335,18 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
return false;
}
}
if (target == TextureUploadTarget::Texture2DMultisample ||
target == TextureUploadTarget::ProxyTexture2DMultisample ||
target == TextureUploadTarget::Texture2DMultisampleArray ||
target == TextureUploadTarget::ProxyTexture2DMultisampleArray) {
if (level != 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateTextureLevelWithUploadTarget",
"Level must be zero for multisample textures"));
return false;
}
}
return true;
}
@@ -243,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

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