Compare commits

...
Author SHA1 Message Date
swung0x48 2eafce6e28 [Fix] (MG_Util/ShaderTranspiler): normalize legacy desktop shaders to GLSL 330 2026-07-16 17:18:07 -04:00
swung0x48 9b7414c0d9 [Fix] (MG_Impl/Texture): support anisotropic sampler parameters 2026-07-16 12:28:05 -04:00
swung0x48 7acfd19582 [Fix] (MG_Backend/DirectVulkan): fix ERROR-level vertex stream build 2026-07-16 12:26:38 -04:00
swung0x48 aa45936f33 [Fix] (MG_Impl/Texture): preserve generated names before first bind 2026-07-16 09:40:21 -04:00
swung0x48 1146188ed4 [Fix] (MG_Util/Texture): infer RGBA8 for packed RGBA uploads 2026-07-16 03:35:50 -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 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 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 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 37a7f35a27 [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 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 7464179249 [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 e8d9a913d8 [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 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
BZLZHH 30c6f55c2d [Fix] (MG_Backend/DirectVulkan): Add namespace qualifier to Version type. 2026-05-09 08:28:46 +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
BZLZHH b373535d6e [Fix] (MG_Impl/Texture): Fix proxy texture object handling. 2026-04-06 02:35:04 +08:00
swung0x48 4a3a44924a [Refactor] (MG_Backend/DirectVulkan/ProgramFactory): refactor uniform reflection 2026-04-03 16:45:11 +08:00
swung0x48 1b2a7989af [Fix] (MG_Util/ShaderTranspiler/SpvcSession): use proper usage bit to select the right code path. Fix bugs along the way 2026-04-01 14:57:00 +08:00
swung0x48 83d1ce177e [Optimization] (MG_Util/ShaderTranspiler/SpvcSession): use SPIRV-Reflect to avoid full AST parse, speeding up reflection 2026-04-01 10:28:37 +08:00
swung0x48 4321c4a827 [Submodule] (3rdparty/SPIRV-Reflect): add SPIRV-Reflect as dependency 2026-03-29 00:03:57 +08:00
swung0x48 a039ce0987 [Chore] (MG_State/ProgramState): some renames 2026-03-26 09:49:05 +08:00
swung0x48 accfaab720 [Refactor] (MG_Backend/DirectVulkan): get rid of junk, and rename some symbols 2026-03-24 14:20:21 +08:00
swung0x48 35658eb998 [Refactor] (MG_Backend/DirectVulkan): move uniform reflection from UniformDescriptorBinder to ProgramFactory 2026-03-23 16:36:17 +08:00
swung0x48 ccbde0196e [Fix] (MG_Test/ProgramTest): fix wrong decomp source output 2026-03-21 23:07:27 +08:00
swung0x48 0209c5461f [Feat] (MG_Backend/DirectVulkan): implement naive transient/resident buffer, and heuristics to downgrade resident buffer to transient 2026-03-20 17:39:10 +08:00
swung0x48 810b4886c7 [Feat] (MG_Backend/DirectVulkan): supports uint8 index buffer by VK_KHR_index_type_uint8 / VK_EXT_index_type_uint8 2026-03-20 15:27:57 +08:00
swung0x48 7dfc2149d8 [Feat] (MG_Backend/DirectVulkan): unified transient buffer arena 2026-03-20 13:56:12 +08:00
swung0x48 9d1e8bd7cd [Feat] (MG_Backend/DirectVulkan): VkBufferManager transient upload now includes uniform 2026-03-20 13:31:51 +08:00
swung0x48 bfa4049ac1 [Chore] (MG_Backend/DirectVulkan): unwrap frame context initialization 2026-03-20 09:55:22 +08:00
swung0x48 87c56ce3d5 [Fix] (MG_Backend/DirectVulkan): Fix VkBufferManager initialization assertion failure 2026-03-20 09:36:37 +08:00
swung0x48 f2ab50b84e [Feat] (MG_Backend/DirectVulkan): VkBufferManager 2026-03-19 17:44:35 +08:00
swung0x48 b6ac6c182e [Feat] (MG_Backend/DirectVulkan): Buffer arena, buffer slice 2026-03-19 17:23:52 +08:00
swung0x48 5e9f0f0c70 [Chore] (MG_Backend/DirectVulkan): remove some unused stuff 2026-03-19 08:59:34 +08:00
swung0x48 e4455aed9a [Feat] (MG_Backend/DirectVulkan): add draw cmd MultiDrawIndexedCmd 2026-03-18 16:08:00 +08:00
swung0x48 bc018c9513 [Chore] (MG_Backend/DirectVulkan): add indexBufferView field for draw cmds 2026-03-18 13:25:10 +08:00
swung0x48 2ecba4d70c [Fix] (MG_Test/VertexArrayTest): fix VertexArrayTest compilation error 2026-03-18 13:07:57 +08:00
swung0x48 1e11a5950e [Fix] (MG_Test/BufferTest): fix BufferTest compilation error 2026-03-18 11:07:56 +08:00
swung0x48 b324363db0 [Chore] (MG_Backend/DirectVulkan): hard assert SetupDraw failure 2026-03-18 11:01:00 +08:00
swung0x48 a654b15190 [Feat] (MG_Backend/DirectVulkan): naively implement DrawElementsBaseVertex and MultiDrawElementsBaseVertex 2026-03-18 10:43:30 +08:00
swung0x48 75ea7f8c0c [Chore] (MG_Backend/DirectVulkan): include index buffer upload into SetupDraw 2026-03-17 16:09:02 +08:00
swung0x48 09aeee4c03 [Chore] (MG_State/VertexArrayState): add const getter to index buffer binding slot 2026-03-17 15:55:16 +08:00
swung0x48 87122973be [Chore] (MG_Backend/DirectVulkan): refactor draw cmds 2026-03-17 15:10:17 +08:00
swung0x48 8b9adc5121 [Fix] (MG_Backend/DirectVulkan/VkRenderPassManager): avoid reusing active render pass when draw FBO attachments still have pending clears 2026-03-17 14:34:50 +08:00
swung0x48 9e42e40705 [Chore] (MG_Backend/DirectVulkan): notes for MaterializePendingClearForTexture 2026-03-09 17:13:56 +08:00
swung0x48 8a91225eb1 [Chore] (MG_Backend/DirectVulkan): debug log for clear manager 2026-03-09 16:22:51 +08:00
swung0x48 e82815802e [Feat] (MG_Backend/DirectVulkan): implement texture mipmap 2026-03-09 15:09:05 +08:00
swung0x48 67069d4a12 [Fix]: compilation error after merge 2026-03-08 11:06:27 +08:00
swung0x48 4a38e7224e Merge branch 'dev' into Feat/Backend-Direct-Vulkan 2026-03-08 10:31:44 +08:00
swung0x48 645f2f748f [Fix] (MG_Backend/DirectVulkan): fix missing clear when (texture-FBO attach -> clear -> detach) occurs 2026-03-08 00:24:30 +08:00
swung0x48 5652f9ae5f [Chore] (MG_Backend/DirectVulkan): implements BlitFramebuffer using backend manager APIs 2026-03-07 19:29:33 +08:00
swung0x48 f3b0b242dd [Feat] (MG_Backend/DirectVulkan): implements BlitFramebuffer 2026-03-07 18:22:38 +08:00
swung0x48 876cb7bcd3 [Chore] (MG_Backend/DirectVulkan): asserts fbo existence 2026-03-07 09:33:05 +08:00
swung0x48 ee9a121c6f [Fix] (MG_Impl/Init): bind default fbo to draw/read slot at init 2026-03-07 09:31:42 +08:00
swung0x48 00439c5c4a [Fix] (MG_Backend/DirectVulkan): Fix sampled-texture/render-pass layout hazard 2026-03-06 21:53:50 +08:00
swung0x48 d7286a5832 [Fix] (MG_Backend/DirectVulkan): some transition fixes 2026-03-06 18:25:33 +08:00
swung0x48 972804fdc7 [Fix] (MG_Backend/DirectVulkan): fixing some layout mismatch 2026-03-06 17:16:55 +08:00
swung0x48 bca7328421 [Fix] (MG_Backend/DirectVulkan): use hash to save active render pass 2026-03-06 16:17:56 +08:00
swung0x48 8097c0d7b1 [Fix] (MG_Backend/DirectVulkan): add untreated aspect member in TextureResource move ctor 2026-03-06 15:45:19 +08:00
swung0x48 641f54e1b7 [Feat] (MG_Backend/DirectVulkan): Transition texture for sampling 2026-03-06 15:37:14 +08:00
swung0x48 c36bfe0843 [Fix] (MG_Backend/DirectVulkan): track image layout naively 2026-03-06 14:53:43 +08:00
swung0x48 8ae66521e3 [Fix] (MG_Backend/DirectVulkan/VkRenderPassManager): early return when there's an active render pass that is compatible 2026-03-06 14:46:51 +08:00
swung0x48 6e21fd9a35 [Chore] (MG_Backend/DirectVulkan): a lotta assertions & transition uploaded texture directly to VK_ACCESS_SHADER_READ_BIT 2026-03-05 23:17:27 +08:00
swung0x48 a2e8faafe5 [Fix] (MG_Backend/DirectVulkan): upload buffer using fence, without vkQueueWaitIdle 2026-03-05 18:02:28 +08:00
swung0x48 668fa3033e [Optimize] (MG_Backend/DirectVulkan): store textureResources in permanent scratch space rather than temp memory 2026-03-05 09:44:03 +08:00
swung0x48 139f3f978d [Chore] (MG_Backend/DirectVulkan): get rid of unnecessary checks 2026-03-04 16:19:35 +08:00
swung0x48 09f96e5ba5 [Fix] (MG_Backend/DirectVulkan): Properly handle front face state. save VkDevice as static member 2026-03-04 15:08:23 +08:00
swung0x48 592a69b1c4 [Feat] (MG_Backend/DirectVulkan): integrating cullmode and frontface render state into vk pipeline 2026-03-03 17:01:02 +08:00
swung0x48 9211341ae7 [Chore] (MG_Backend/DirectVulkan): remove unused functions 2026-03-03 15:16:27 +08:00
swung0x48 5f9d354adb [Fix] (CMakeLists): add absent source file 2026-03-03 15:05:33 +08:00
swung0x48 0ae36b71f5 [Chore] (MG_Backend/DirectVulkan): move ClearAttachmentsOnActiveRenderPass to VulkanRenderer 2026-03-03 14:59:03 +08:00
swung0x48 20e2768f27 [Chore] (MG_Backend/DirectVulkan): move enum conversion to MG_Utils 2026-03-03 14:17:28 +08:00
swung0x48 00c8b5ae3a [Chore] (MG_Backend/DirectVulkan): get rid of unused functions 2026-03-03 13:38:06 +08:00
swung0x48 7687c19b33 [Fix] (MG_Backend/DirectVulkan): hook up TextureManager to TextureEnumConverter 2026-03-03 13:30:33 +08:00
swung0x48 fc3a7cd5f1 [Chore] (Config): Bump version to 26.03 2026-03-03 10:50:24 +08:00
swung0x48 0b7f906533 [Fix] (MG_Backend/DirectVulkan): use proper scissor state to fix menu 2026-03-03 09:43:25 +08:00
swung0x48 c78a94ed09 [Chore] (MG_Backend/DirectVulkan): use vkCmdClearAttachments to implement clear semantics when there's one compatible render pass in flight (instead of interrupting it) 2026-03-02 17:16:18 +08:00
swung0x48 debcb171ce [Chore] (MG_Backend/DirectVulkan): relaxing byteSize checks for textures in VkTextureManager 2026-03-02 14:55:55 +08:00
swung0x48 282468c9d5 [Fix] (MG_Backend/DirectVulkan): query clear color/state inside of RenderPassManager 2026-03-02 10:13:59 +08:00
swung0x48 34b06da83b [Fix] (MG_Backend/DirectVulkan): get ImageView from swapchain when creating render pass for default FBO 2026-03-02 09:49:32 +08:00
swung0x48 3d55f38dab [Fix] (ConfigLoader): still defaults to DirectGLES backend 2026-03-02 08:58:15 +08:00
swung0x48 16426db244 [Chore] (MG_Backend/DirectVulkan): manage active renderpass inside RenderPassManager static functions 2026-03-01 23:02:54 +08:00
swung0x48 6f8c76f06c [Fix] (MG_Backend/DirectVulkan): fixing Vulkan backend initialization 2026-03-01 22:39:30 +08:00
swung0x48 43f2043478 [Fix] (MG_Backend/DirectVulkan): Vulkan backend default framebuffer initialization 2026-03-01 21:21:32 +08:00
swung0x48 f61f068e28 [Fix] (MG_Backend/DirectVulkan): refactor VkRenderPassManager and hook it up (this commit only compiles and does not work) 2026-03-01 18:17:34 +08:00
swung0x48 e234d471fe [Fix] (MG_Backend/DirectVulkan): uninitialized vars in VkRenderPassManager 2026-02-28 18:00:31 +08:00
swung0x48 6ff97a9bb1 [Feat] (MG_Backend/DirectVulkan): new VkRenderPassManager and a bunch of changes accordingly 2026-02-28 17:37:45 +08:00
swung0x48 8adf1d3b8a [Chore] (MG_Backend/DirectVulkan): get rid of junk 2026-02-27 17:36:57 +08:00
swung0x48 2935279603 [Chore] (MG_Backend/DirectVulkan): remove "transition" stuff in VkRenderTargetManager 2026-02-27 16:34:09 +08:00
swung0x48 918b39bb63 [Fix] (MG_Backend/DirectVulkan): remove weird texture binding fallback 2026-02-27 16:22:19 +08:00
swung0x48 963e88943c [Chore] (MG_Backend/DirectVulkan): manage default depth buffer in SwapchainObject 2026-02-27 15:33:01 +08:00
swung0x48 1a75bcba5e [Fix] (MG_Backend/DirectVulkan): prevent operation in assert getting compiled out in release 2026-02-27 15:05:10 +08:00
swung0x48 b64309edfc [Chore] (MG_Backend/DirectVulkan): get rid of junk 2026-02-27 14:14:11 +08:00
swung0x48 e3f1db0a9a [Chore] (MG_Backend/DirectVulkan): use VkTextureManager::TransitionImageLayout more 2026-02-27 13:59:39 +08:00
swung0x48 2c848ab44b [Chore] (MG_Backend/DirectVulkan): move TransitionImageLayout to VkTextureManager 2026-02-27 09:44:23 +08:00
swung0x48 9b23594415 [Fix] (MG_Backend/DirectVulkan): misc fixes 2026-02-25 16:49:58 +08:00
swung0x48 e2e52965df [Feat] (MG_Backend/DirectVulkan): VkClearManager 2026-02-25 16:12:33 +08:00
swung0x48 6e7907f183 [Chore] (MG_Backend/DirectVulkan): get rid of more junk 2026-02-25 12:46:47 +08:00
swung0x48 b4be1292b8 [Feat] (MG_Backend/DirectVulkan): TextureManager mipmap completeness check 2026-02-25 11:04:28 +08:00
swung0x48 db58f4c214 [Feat] (MG_Backend/DirectVulkan): TextureManager supports mipmap 2026-02-25 10:52:54 +08:00
swung0x48 59bad77176 [Chore] (MG_Backend/DirectVulkan): rename functions in VkTextureManager 2026-02-25 09:52:15 +08:00
BZLZHH 3504fca8da Merge branch 'dev' into Perf/Improvement1 2026-02-24 22:48:23 +08:00
BZLZHH e61817ff6a [Fix] (MG_Backend/DirectGLES): Improve iteration safety in CollectGarbage. 2026-02-24 22:47:18 +08:00
BZLZHH b9df28838c Merge pull request #5 from Perf/Improvement1
Merge Perf/improvement1 to dev
2026-02-24 22:09:38 +08:00
BZLZHH fff00e65c4 [Perf] (Texture|State): Replace dynamic_cast hot paths with static_cast. 2026-02-24 15:40:06 +08:00
BZLZHH 8320b4f456 [Feat|Fix] (MG_Backend/DirectGLES): Unify state-backend registries with weak-ref GC. 2026-02-24 15:27:28 +08:00
swung0x48 a757669df5 [Improvement] (MG_Backend/DirectVulkan): split TextureSamplerManager into TextureManager and SamplerManager 2026-02-24 15:19:34 +08:00
swung0x48 e16c0645c0 [Improvement] (MG_Backend/DirectVulkan): use VMA to allocate texture images 2026-02-24 14:21:24 +08:00
swung0x48 8d37763fd3 [Improvement] (MG_Backend/DirectVulkan): use VMA to allocate RT images 2026-02-24 13:09:26 +08:00
swung0x48 e0d0551e7b [Chore] (MG_Test/Backend/DirectVulkan/TestExec): FramebufferManager -> RenderTargetManager 2026-02-24 13:07:50 +08:00
swung0x48 caa814e8f9 [Chore] (MG_Test/Backend/DirectVulkan/TestExec): get rid of now useless default renderpass 2026-02-24 12:25:04 +08:00
swung0x48 2502d32a2e [Chore] (MG_Test/Backend/DirectVulkan/TestExec): get rid of necessary check 2026-02-24 12:10:44 +08:00
swung0x48 e57dd3529e [Chore] (MG_Test/Backend/DirectVulkan/TestExec): get rid of junk 2026-02-24 10:19:21 +08:00
swung0x48 4b8d809237 [Fix] (MG_Test/Backend/DirectVulkan/TestExec): initialize MobileGL first then EGL stuff 2026-02-24 09:26:46 +08:00
swung0x48 d42182befc [Fix] (MG_Backend/DirectVulkan): swap swapchain width/height on 90/270 rotation 2026-02-23 23:12:22 +08:00
swung0x48 5efdb1016e [Fix] (MG_Backend/DirectVulkan): make vk_enum_string_helper.h available on Android NDK 2026-02-23 22:11:30 +08:00
BZLZHH 15e24cda78 [Perf|Improvement] (All): Improve performance & optimize code. 2026-02-23 16:00:38 +08:00
swung0x48 5013108a7f [Chore] (MG_Backend/DirectVulkan): get rid of unnecessary checks 2026-02-22 13:17:22 +08:00
swung0x48 359ba1c572 [Fix] (MG_Backend/DirectVulkan): properly compute hash for samplers 2026-02-22 12:51:00 +08:00
swung0x48 733523011b [Chore] (MG_Backend/DirectVulkan): more hard assertions 2026-02-22 12:31:18 +08:00
swung0x48 14fc13c34d [Chore] (MG_Backend/DirectVulkan): get rid of fallback image/sampler/imageview 2026-02-22 12:18:49 +08:00
swung0x48 e9fee1358d [Chore] (MG_Backend/DirectVulkan): hard assert instead of silently fallback 2026-02-22 12:08:31 +08:00
swung0x48 dae3dd9996 [Chore] (MG_Backend/DirectVulkan): unifying framebuffer manager Transition* functiosn 2026-02-22 11:08:43 +08:00
swung0x48 556db8b02e [Chore] (MG_Backend/DirectVulkan): get rid of redundant guardrails (cont.) 2026-02-22 10:35:10 +08:00
swung0x48 96e993eecf [Chore] (MG_Backend/DirectVulkan): get rid of redundant guardrails 2026-02-22 10:19:44 +08:00
swung0x48 fb3e2123cc [Fix] (MG_Backend/DirectVulkan): dynamically retrieve Vulkan functions in loader, fixing compilation error on lower NDK versions 2026-02-22 10:11:14 +08:00
373 changed files with 70364 additions and 15259 deletions
-2
View File
@@ -14,7 +14,6 @@ bugprone-forwarding-reference-overload,
bugprone-inaccurate-erase,
bugprone-incorrect-roundings,
bugprone-integer-division,
bugprone-lambda-function-name,
bugprone-macro-parentheses,
bugprone-macro-repeated-side-effects,
bugprone-misplaced-operator-in-strlen-in-alloc,
@@ -63,7 +62,6 @@ cert-str34-c,
cppcoreguidelines-interfaces-global-init,
cppcoreguidelines-narrowing-conversions,
cppcoreguidelines-pro-type-member-init,
cppcoreguidelines-pro-type-static-cast-downcast,
cppcoreguidelines-slicing,
google-default-arguments,
google-runtime-operator,
+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
@@ -0,0 +1,67 @@
#!/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}"
mirror_base="${MOBILEGL_TRACE_FIXTURE_MIRROR_BASE:-https://repo.miawa.cn/mgl/tools/trace_replay/fixtures}"
if ! command -v "${python_bin}" >/dev/null 2>&1 && command -v python >/dev/null 2>&1; then
python_bin=python
fi
fixture_list="$("${python_bin}" tools/trace_replay/trace_cases.py \
--format fixture-files \
--case "${case_name}" \
--fixture-root "${fixture_dir}")"
mapfile -t files <<< "${fixture_list}"
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
fetch_from_mirror() {
mkdir -p "${fixture_dir}"
for file in "${files[@]}"; do
local name
local url
name="$(basename "${file}")"
url="${mirror_base%/}/${name}"
echo "Fetching trace fixture from mirror: ${url}"
if ! curl -L --fail --retry 3 --retry-delay 2 -o "${file}.tmp" "${url}"; then
rm -f "${file}.tmp"
return 1
fi
mv "${file}.tmp" "${file}"
done
}
if fetch_from_mirror; then
echo "Fetched trace fixture files for ${case_name} from mirror: ${include}"
else
echo "Mirror fetch 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
test -s "${file}"
if head -n 1 "${file}" | grep -q "version https://git-lfs.github.com/spec/v1"; then
echo "failed to hydrate LFS fixture: ${file}" >&2
exit 1
fi
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.'
+532
View File
@@ -0,0 +1,532 @@
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
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 }}
/usr/local/lib/android/sdk/emulator
/usr/local/lib/android/sdk/platform-tools
/usr/local/lib/android/sdk/platforms/android-35
/usr/local/lib/android/sdk/system-images/android-35/google_apis/x86_64
key: ${{ runner.os }}-mobilegl-avd-api35-google_apis-x86_64-pixel_6-v1-${{ hashFiles('android-plugin/run-avd-ci.sh') }}
- 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
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: 16
- 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 }}
/usr/local/lib/android/sdk/emulator
/usr/local/lib/android/sdk/platform-tools
/usr/local/lib/android/sdk/platforms/android-35
/usr/local/lib/android/sdk/system-images/android-35/google_apis/x86_64
key: ${{ runner.os }}-mobilegl-avd-api35-google_apis-x86_64-pixel_6-v1-${{ hashFiles('android-plugin/run-avd-ci.sh') }}
- 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
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[@]}"
- 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
+507 -22
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,510 @@ 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}/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/MobileGL/MG_Test
run: |
if [ "${{ secrets.ACTIONS_STEP_DEBUG }}" == "true" ]; then
if [ "${{ secrets.ACTIONS_STEP_DEBUG }}" = "true" ]; then
ctest -V
else
ctest
ctest --output-on-failure
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/MobileGL/MG_Benchmark
run: ctest -V -C Release
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
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/
+12
View File
@@ -19,3 +19,15 @@
[submodule "3rdparty/VulkanMemoryAllocator"]
path = 3rdparty/VulkanMemoryAllocator
url = https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator.git
[submodule "3rdparty/Vulkan-Utility-Libraries"]
path = 3rdparty/Vulkan-Utility-Libraries
url = https://github.com/KhronosGroup/Vulkan-Utility-Libraries.git
[submodule "3rdparty/Vulkan-Headers"]
path = 3rdparty/Vulkan-Headers
url = https://github.com/KhronosGroup/Vulkan-Headers.git
[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/SPIRV-Reflect added at 10b4f09a24
Vendored Submodule
+1
Submodule 3rdparty/Vulkan-Headers added at ad9ce1235e
Vendored Submodule
+1
Submodule 3rdparty/apitrace added at 10935bb5e4
+113 -4
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)
@@ -104,10 +109,20 @@ set(SPIRV_CROSS_ENABLE_CPP OFF CACHE BOOL "Disable C++ API target" FORCE)
set(SPIRV_CROSS_CLI OFF CACHE BOOL "Disable CLI binary" FORCE)
set(SPIRV_CROSS_STATIC ON CACHE BOOL "Prefer static libs" FORCE)
set(SPIRV_REFLECT_EXECUTABLE OFF CACHE BOOL "Build spirv-reflect executable" FORCE)
set(SPIRV_REFLECT_STATIC_LIB ON CACHE BOOL "Build a SPIRV-Reflect static library" FORCE)
set(SPIRV_REFLECT_BUILD_TESTS OFF CACHE BOOL "Build the SPIRV-Reflect test suite" FORCE)
set(SPIRV_REFLECT_ENABLE_ASSERTS OFF CACHE BOOL "Enable asserts for debugging" FORCE)
set(SPIRV_REFLECT_ENABLE_ASAN OFF CACHE BOOL "Use address sanitization" FORCE)
set(SPIRV_REFLECT_INSTALL OFF CACHE BOOL "Whether to install" FORCE)
# add_subdirectory(3rdparty/DiligentCore)
add_subdirectory(3rdparty/glslang)
add_subdirectory(3rdparty/SPIRV-Cross)
add_subdirectory(3rdparty/VulkanMemoryAllocator)
add_subdirectory(3rdparty/Vulkan-Headers)
add_subdirectory(3rdparty/Vulkan-Utility-Libraries)
add_subdirectory(3rdparty/SPIRV-Reflect)
set(XXHASH_BUILD_XXHSUM OFF)
option(BUILD_SHARED_LIBS OFF)
@@ -160,6 +175,8 @@ set(SOURCE_FILES
MobileGL/MG_Util/Converters/GLToMG/RenderStateEnumConverter.cpp
MobileGL/MG_Util/Converters/GLToMG/ProgramEnumConverter.cpp
MobileGL/MG_Util/Converters/MGToMG/TextureEnumConverter.cpp
MobileGL/MG_Util/Converters/MGToVk/RenderStateEnumConverter.cpp
MobileGL/MG_Util/Converters/MGToVk/TextureEnumConverter.cpp
MobileGL/MG_Util/Classifiers/TextureEnumClassifier.cpp
@@ -167,11 +184,18 @@ 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/LowerDrawParametersPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.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
@@ -199,6 +223,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
@@ -219,13 +244,17 @@ set(SOURCE_FILES
MobileGL/MG_Backend/DirectVulkan/Renderer/FrameContext.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/UniformDescriptorBinder.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/BufferArena.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateBuilder.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferObject.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/VkFramebufferManager.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
MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureSamplerManager.cpp
MobileGL/MG_State/GLState/Core.cpp
MobileGL/MG_State/EGLState/Core.cpp
@@ -255,6 +284,21 @@ 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()
set(MOBILEGL_LINK_LIBRARIES
glslang::glslang
spirv-cross-c
@@ -262,6 +306,8 @@ set(MOBILEGL_LINK_LIBRARIES
SPIRV-Tools
xxHash::xxhash
GPUOpen::VulkanMemoryAllocator
Vulkan::UtilityHeaders
spirv-reflect-static
)
set(MOBILEGL_COMPILE_DEF
@@ -311,8 +357,22 @@ 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(NOT ANDROID)
add_library(${CMAKE_PROJECT_NAME}_s STATIC
${SOURCE_FILES}
@@ -344,6 +404,7 @@ if(NOT ANDROID)
target_compile_definitions(${CMAKE_PROJECT_NAME}_s
PUBLIC
${MOBILEGL_COMPILE_DEF}
MOBILEGL_LOG_ACTIVE_LEVEL=${MOBILEGL_LOG_ACTIVE_LEVEL}
)
endif()
@@ -362,7 +423,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)
@@ -370,7 +472,9 @@ 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)
if (MOBILEGL_BUILD_TEST)
add_subdirectory(MobileGL/MG_Test)
endif()
@@ -378,4 +482,9 @@ if (NOT ANDROID)
if (MOBILEGL_BUILD_BENCHMARK)
add_subdirectory(MobileGL/MG_Benchmark)
endif()
if (MOBILEGL_BUILD_TRACE_REPLAY)
enable_testing()
add_subdirectory(tools/trace_replay)
endif()
endif()
+45 -1
View File
@@ -14,9 +14,53 @@ 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, 2, 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;
// 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;
};
extern FeaturesTable Features;
} // namespace MobileGL::MG_Config
+69
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,61 @@ 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);
}
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");
}
inline void InitBackendType() {
String backendTypeStr;
QueryEnvVariable("MOBILEGL_BACKEND_TYPE", backendTypeStr, "DirectGLES");
@@ -77,6 +145,7 @@ namespace MobileGL::MG_ConfigLoader {
InitializeAcceptedEnvVariables();
InitBackendType();
InitFeatures();
// Destroy the map since we won't need it anymore
acceptedEnvVariablesMap.reset();
+16 -8
View File
@@ -32,9 +32,13 @@
#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
// ====================== MobileGL configurations ======================= //
#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 +67,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
+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>
+43 -10
View File
@@ -15,7 +15,39 @@
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
namespace MobileGL {
namespace {
Bool g_isInitialized = false;
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,19 +60,12 @@ 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();
delete MG_State::pGLContext;
delete MG_State::pEGLContext;
delete MG_Impl::GLImpl::TextureImpl::pProxyTextureManager;
delete MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo;
MG_Util::Debug::Close();
// TODO: add and use Destroy functions for other subsystems
DestroyImpl(true);
}
#if defined(__linux__) || defined(__APPLE__)
@@ -49,7 +74,15 @@ namespace MobileGL {
}
__attribute__((destructor)) static void AutoDestroy() {
Destroy();
if (MG_Config::Features.TraceSkipAutodestroy) {
return;
}
#if defined(__APPLE__)
// macOS injected dylibs can run destructors after logging/backend static state is already torn down.
return;
#else
DestroyImpl(false);
#endif
}
#endif
+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
+266 -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,157 @@ 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);
};
struct DynamicBackendParameters {
SizeT UniformBufferOffsetAlignment = 256;
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 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;
};
enum class WindowBackend {
Android,
// TODO: X11, Wayland, Windows, macOS, etc.
X11,
MetalLayer,
// TODO: Wayland, Windows, etc.
WindowBackendCount,
Unknown = -1
};
@@ -84,6 +299,8 @@ namespace MobileGL {
struct WindowHandle {
WindowBackend Backend = WindowBackend::Unknown;
void* Handle = nullptr;
Uint32 Width = 0;
Uint32 Height = 0;
};
class BackendObject {
@@ -95,9 +312,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 +329,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
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;
@@ -35,6 +44,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
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 +53,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 are (or are not) usable. The
// MOBILEGL_DISABLE_TIMERQUERY escape hatch is applied inside.
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported);
// 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>
@@ -25,11 +27,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 +51,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
+334 -47
View File
@@ -8,6 +8,8 @@
#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"
@@ -15,62 +17,258 @@
#include <MG_State/GLState/Core.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:
using StatePtr = SharedPtr<StateObject>;
using StateWeakPtr = std::weak_ptr<StateObject>;
using BackendPtr = SharedPtr<BackendObject>;
using BackendMap = UnorderedMap<StateObject*, BackendPtr>;
using StateRefMap = UnorderedMap<StateObject*, StateWeakPtr>;
using iterator = typename BackendMap::iterator;
using const_iterator = typename BackendMap::const_iterator;
BackendPtr& GetOrCreate(const StatePtr& stateObj) {
MOBILEGL_ASSERT(stateObj != nullptr, "State object must not be null");
auto* key = stateObj.get();
auto trackedStateIt = m_stateRefs.find(key);
if (trackedStateIt != m_stateRefs.end() && trackedStateIt->second.expired()) {
EraseByKey(key);
}
m_stateRefs[key] = stateObj;
return m_backendObjects[key];
}
iterator find(StateObject* stateObj) {
if (!IsAlive(stateObj)) {
EraseByKey(stateObj);
return m_backendObjects.end();
}
return m_backendObjects.find(stateObj);
}
const_iterator find(StateObject* stateObj) const {
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(); }
void CollectGarbageIfNeeded() {
++m_gcTick;
if (m_gcTick < kGCInterval) {
return;
}
CollectGarbage();
m_gcTick = 0;
}
void CollectGarbageNow() { CollectGarbage(); }
private:
bool IsAlive(StateObject* stateObj) const {
const auto trackedStateIt = m_stateRefs.find(stateObj);
if (trackedStateIt == m_stateRefs.end()) {
return false;
}
return !trackedStateIt->second.expired();
}
void EraseByKey(StateObject* stateObj) {
m_stateRefs.erase(stateObj);
m_backendObjects.erase(stateObj);
}
void CollectGarbage() {
if (m_isCollecting) {
return;
}
m_isCollecting = true;
Vector<StateObject*> staleKeys;
staleKeys.reserve(m_stateRefs.size());
for (const auto& [stateKey, stateWeakRef] : m_stateRefs) {
if (stateWeakRef.expired()) {
staleKeys.push_back(stateKey);
}
}
for (auto* stateKey : staleKeys) {
m_stateRefs.erase(stateKey);
m_backendObjects.erase(stateKey);
}
m_isCollecting = false;
}
private:
static constexpr Uint32 kGCInterval = 1024;
StateRefMap m_stateRefs;
BackendMap m_backendObjects;
Uint32 m_gcTick = 0;
Bool m_isCollecting = false;
};
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(SharedPtr<MG_State::GLState::BufferObject>& stateBufferObject);
Uint GetBackendBufferId() { return m_backendBufferId; }
void Bind(GLenum target = TempBufferTarget);
~GLESBufferResource() override = default;
private:
void SyncToBackend_glBufferData(SharedPtr<MG_State::GLState::BufferObject>& stateBufferObject);
void SyncToBackend_glBufferSubData(SharedPtr<MG_State::GLState::BufferObject>& stateBufferObject);
void SyncToBackend_glMapBufferRange(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 UnorderedMap<SharedPtr<MG_State::GLState::BufferObject>, SharedPtr<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 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();
void SyncToBackend(SharedPtr<MG_State::GLState::VertexArrayObject>& stateVAOObject);
Uint GetBackendVertexArrayId() { return m_backendVAOId; }
void Bind();
~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:
void BindAttributeBuffer(Uint index, const MG_State::GLState::VertexAttribute& attrib);
Uint m_backendVAOId = 0;
Array<Uint, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS> m_clientAttributeBufferIds;
Bool m_isInitialized = false;
Uint16 m_syncedIndexBufferVersion = 0;
Array<MG_State::GLState::VertexAttributeVersion, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS>
m_syncedAttributeVersions;
};
extern UnorderedMap<SharedPtr<MG_State::GLState::VertexArrayObject>, SharedPtr<BackendVertexArrayObject>>
extern StateBackendObjectRegistry<MG_State::GLState::VertexArrayObject, BackendVertexArrayObject>
g_backendVertexArrayObjects;
} // namespace VertexArrayImpl
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)
target == TextureTarget::Texture1DArray || target == TextureTarget::Texture2DArray)
return false;
return true;
}
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
TextureInternalFormat internalFormat = TextureInternalFormat::Unknown;
SizeT width = 0;
@@ -78,11 +276,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); }
@@ -92,15 +293,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
class BackendTextureObject {
public:
BackendTextureObject();
void SyncMipmapsToBackend(SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
void SyncBuiltinSamplerToBackend(SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
void SyncTextureParamsToBackend(SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
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();
Uint GetBackendTextureId() const;
private:
void RecreateBackendTexture();
Uint m_backendTextureId = 0;
Bool m_isInitialized = false;
Bool m_imageBindableStorageRequired = false;
Bool m_backendStorageImmutable = false;
StateTextureBasicInfo m_prevTextureInfo;
SamplerParameters m_cacheSamplerParameters;
UintVec2 m_cacheLodRange = {0, 1000};
@@ -113,8 +319,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
void ActivateTextureUnit(Uint unit);
void UnbindTexture(Uint unit, GLenum target);
extern UnorderedMap<SharedPtr<MG_State::GLState::ITextureObject>, SharedPtr<BackendTextureObject>>
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;
@@ -125,13 +334,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
class BackendFramebufferObject {
public:
BackendFramebufferObject();
void SyncToBackend(SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject,
void SyncToBackend(const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject,
FramebufferTarget asTarget);
Uint GetBackendFramebufferId() { return m_backendFBOId; }
void Bind(FramebufferTarget target);
bool SyncAttachmentObject(GLenum glFBOTarget,
const MG_State::GLState::FramebufferAttachmentObject& attachmentObject,
GLenum glBackendAttachment);
void InvalidateSyncedState();
Uint GetBackendFramebufferId() const { return m_backendFBOId; }
void Bind(FramebufferTarget target) const;
// FramebufferAttachmentType GetCompactedAttachmentTypeAtDrawBufferIndex(Int index);
GLenum GetBackendAttachmentType(FramebufferAttachmentType frontendAtt) const;
@@ -159,28 +366,107 @@ namespace MobileGL::MG_Backend::DirectGLES {
FramebufferObject::FramebufferAttachmentVersionArray m_syncedFrontendAttachmentVersions = {0};
};
extern UnorderedMap<SharedPtr<MG_State::GLState::FramebufferObject>, SharedPtr<BackendFramebufferObject>>
extern StateBackendObjectRegistry<MG_State::GLState::FramebufferObject, BackendFramebufferObject>
g_backendFramebufferObjects;
extern Array<Uint16, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fboBindVersions;
} // namespace FramebufferImpl
// 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(SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject);
void Use();
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 UnorderedMap<SharedPtr<MG_State::GLState::ProgramObject>, SharedPtr<BackendProgramObjectImpl>>
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
@@ -188,9 +474,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
class BackendSamplerObject {
public:
BackendSamplerObject();
void SyncToBackend(SharedPtr<MG_State::GLState::SamplerObject>& stateSamplerObject);
void SyncToBackend(const SharedPtr<MG_State::GLState::SamplerObject>& stateSamplerObject);
void Bind(Uint unit);
Uint GetBackendSamplerId();
Uint GetBackendSamplerId() const;
private:
Uint m_backendSamplerId = 0;
@@ -203,7 +489,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
extern Array<BackendSamplerObject*, MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS>
g_boundSamplersCache;
extern UnorderedMap<SharedPtr<MG_State::GLState::SamplerObject>, SharedPtr<BackendSamplerObject>>
extern StateBackendObjectRegistry<MG_State::GLState::SamplerObject, BackendSamplerObject>
g_backendSamplerObjects;
} // namespace SamplerImpl
@@ -212,8 +498,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
public:
BackendRenderbufferObject();
void SyncToBackend(const SharedPtr<MG_State::GLState::RenderbufferObject>& stateRBOObject);
Uint GetBackendRenderbufferId() { return m_backendRBOId; }
void Bind();
Uint GetBackendRenderbufferId() const { return m_backendRBOId; }
void Bind() const;
private:
Uint m_backendRBOId = 0;
@@ -221,9 +507,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
TextureInternalFormat m_cacheInternalFormat = TextureInternalFormat::Unknown;
Int m_cacheWidth = 0;
Int m_cacheHeight = 0;
Int m_cacheSamples = 0;
};
extern UnorderedMap<SharedPtr<MG_State::GLState::RenderbufferObject>, SharedPtr<BackendRenderbufferObject>>
extern StateBackendObjectRegistry<MG_State::GLState::RenderbufferObject, BackendRenderbufferObject>
g_backendRenderbufferObjects;
} // namespace RenderbufferImpl
} // namespace MobileGL::MG_Backend::DirectGLES
+234 -16
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"
@@ -19,26 +20,131 @@
#include <MG_Util/Converters/MGToGL/FramebufferEnumConverter.h>
namespace MobileGL::MG_Backend::DirectGLES {
namespace BufferImpl {} // namespace BufferImpl
namespace {
Flags<PixelFormatNormalizeOptionBit> GetForcedPixelFormatNormalizeOptions() {
Flags<PixelFormatNormalizeOptionBit> options;
if (g_GLESCapabilities.IsAngleRenderer) {
options |= PixelFormatNormalizeOptionBit::NoRgb16;
options |= PixelFormatNormalizeOptionBit::NoSnorm16;
options |= PixelFormatNormalizeOptionBit::NoSnorm8;
}
return options;
}
namespace VertexArrayImpl {} // namespace VertexArrayImpl
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 FramebufferImpl {} // namespace FramebufferImpl
namespace PrgramImpl {
String ProcessOutColorLocations(const String& glslCode) {
#ifdef TRACY_ENABLE
@@ -108,14 +214,128 @@ 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;
}
} // namespace PrgramImpl
@@ -125,7 +345,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());
}
}
@@ -166,7 +386,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
return GL_UNIFORM_BUFFER_BINDING;
case GL_FRAMEBUFFER:
return GL_FRAMEBUFFER_BINDING;
case GL_DRAW_FRAMEBUFFER:
return GL_DRAW_FRAMEBUFFER_BINDING;
case GL_READ_FRAMEBUFFER:
@@ -176,7 +395,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
return GL_RENDERBUFFER_BINDING;
case GL_VERTEX_ARRAY:
return GL_VERTEX_ARRAY_BINDING;
case GL_VERTEX_ARRAY_BINDING:
return GL_VERTEX_ARRAY_BINDING;
+12 -4
View File
@@ -14,15 +14,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
namespace DebugImpl {
class ErrorLopper {
public:
void Loop(std::function<void(GLenum)>);
void Clear();
static void Loop(const std::function<void(GLenum)>&);
static void Clear();
ErrorLopper();
~ErrorLopper();
};
class OpenGLScopeMarker {
public:
explicit OpenGLScopeMarker(String scopeName);
explicit OpenGLScopeMarker(const String& scopeName);
~OpenGLScopeMarker();
};
} // namespace DebugImpl
@@ -35,7 +35,12 @@ 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
@@ -43,6 +48,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
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);
} // namespace PrgramImpl
@@ -9,16 +9,320 @@
#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;
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,7 +331,24 @@ 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;
}
@@ -46,8 +367,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;
}
@@ -59,40 +389,48 @@ 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)) {
MGLOG_E("DirectVulkan backend only supports Android, X11, and CAMetalLayer 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);
}
@@ -105,33 +443,87 @@ 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),
.IsCompatibilityProfile = false
},
.StaticBackendCapability = {.AllowVSOnlyPrograms = false}};
return rendererInfo;
}
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported) {
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_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);
}
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 {
@@ -146,10 +538,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;
@@ -165,12 +560,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;
@@ -180,7 +614,156 @@ 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());
}
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;
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::min(m_vulkanCaps.MaxImageUnits, maxSupportedTextureUnits);
m_dynamicParameters.MaxCombinedImageUniforms = m_vulkanCaps.MaxCombinedImageUniforms;
m_dynamicParameters.MaxComputeImageUniforms = m_vulkanCaps.MaxComputeImageUniforms;
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);
}
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -12,29 +12,72 @@
#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);
// 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;
// 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);
}
@@ -0,0 +1,138 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/BufferArena.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 "BufferArena.h"
namespace MobileGL::MG_Backend::DirectVulkan {
Bool BufferArena::Initialize(const BufferArenaDesc& desc) {
Shutdown();
MOBILEGL_ASSERT(desc.allocator != nullptr, "BufferArena::Initialize requires valid allocator");
MOBILEGL_ASSERT(desc.frameCount > 0, "BufferArena::Initialize requires non-zero frame count");
MOBILEGL_ASSERT(desc.usage != 0, "BufferArena::Initialize requires non-zero buffer usage");
m_desc = desc;
m_frames.clear();
m_frames.resize(desc.frameCount);
m_deferredReleases.resize(desc.frameCount);
return true;
}
void BufferArena::Shutdown() {
for (auto& frame : m_frames) {
frame.buffer.Destroy();
frame.writeCursor = 0;
}
m_frames.clear();
m_deferredReleases.clear();
m_desc = {};
}
void BufferArena::BeginFrame(Uint32 frameIndex) {
CollectDeferredReleases(frameIndex);
ResetFrame(frameIndex);
}
void BufferArena::ResetFrame(Uint32 frameIndex) {
AssertValidFrameIndex(frameIndex);
m_frames[frameIndex].writeCursor = 0;
}
void BufferArena::CollectDeferredReleases(Uint32 frameIndex) {
AssertValidFrameIndex(frameIndex);
m_deferredReleases[frameIndex].clear();
}
Bool BufferArena::Allocate(Uint32 frameIndex, VkDeviceSize size, VkDeviceSize alignment, BufferSlice& outSlice) {
AssertValidFrameIndex(frameIndex);
MOBILEGL_ASSERT(size > 0, "BufferArena::Allocate requires non-zero size");
auto& frame = m_frames[frameIndex];
const VkDeviceSize resolvedAlignment = alignment > 0 ? alignment : 1;
const VkDeviceSize offset = (frame.writeCursor + resolvedAlignment - 1) & ~(resolvedAlignment - 1);
const VkDeviceSize endOffset = offset + size;
if (!EnsureCapacity(frameIndex, endOffset)) {
return false;
}
frame.writeCursor = endOffset;
outSlice = frame.buffer.GetSlice(offset, size);
return outSlice.IsValid();
}
Bool BufferArena::Upload(Uint32 frameIndex, const void* data, VkDeviceSize size, VkDeviceSize alignment,
BufferSlice& outSlice) {
MOBILEGL_ASSERT(data != nullptr || size == 0, "BufferArena::Upload data pointer is null");
if (!Allocate(frameIndex, size, alignment, outSlice)) {
return false;
}
if (outSlice.mapped != nullptr) {
Memcpy(outSlice.mapped, data, static_cast<SizeT>(size));
return true;
}
return m_frames[frameIndex].buffer.Upload(data, size, outSlice.offset);
}
VkDeviceSize BufferArena::GetWriteCursor(Uint32 frameIndex) const {
AssertValidFrameIndex(frameIndex);
return m_frames[frameIndex].writeCursor;
}
Uint32 BufferArena::GetFrameCount() const {
return static_cast<Uint32>(m_frames.size());
}
Bool BufferArena::EnsureCapacity(Uint32 frameIndex, VkDeviceSize requiredEndOffset) {
AssertValidFrameIndex(frameIndex);
auto& frame = m_frames[frameIndex];
auto& buffer = frame.buffer;
if (buffer.IsValid() && buffer.GetSize() >= requiredEndOffset) {
return true;
}
VkDeviceSize newCapacity = buffer.IsValid() ? buffer.GetSize() : 0;
if (newCapacity < m_desc.minBufferSize) {
newCapacity = m_desc.minBufferSize;
}
if (newCapacity == 0) {
newCapacity = requiredEndOffset;
}
while (newCapacity < requiredEndOffset) {
newCapacity *= 2;
}
if (buffer.IsValid()) {
m_deferredReleases[frameIndex].push_back(std::move(buffer));
}
VkBufferObjectDesc bufferDesc{};
bufferDesc.allocator = m_desc.allocator;
bufferDesc.size = newCapacity;
bufferDesc.usage = m_desc.usage;
bufferDesc.memoryUsage = m_desc.memoryUsage;
bufferDesc.allocationFlags = m_desc.allocationFlags;
if (!buffer.Create(bufferDesc)) {
return false;
}
if (m_desc.persistentlyMapped && buffer.Map() == nullptr) {
buffer.Destroy();
return false;
}
frame.writeCursor = 0;
return true;
}
void BufferArena::AssertValidFrameIndex(Uint32 frameIndex) const {
MOBILEGL_ASSERT(frameIndex < m_frames.size(), "BufferArena frame index out of range");
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -0,0 +1,56 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/BufferArena.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 "BufferSlice.h"
#include "VkBufferObject.h"
#include "../VkIncludes.h"
#include <Includes.h>
#include <vk_mem_alloc.h>
namespace MobileGL::MG_Backend::DirectVulkan {
struct BufferArenaDesc {
VmaAllocator allocator = nullptr;
Uint32 frameCount = 0;
VkBufferUsageFlags usage = 0;
VmaMemoryUsage memoryUsage = VMA_MEMORY_USAGE_AUTO;
VmaAllocationCreateFlags allocationFlags = 0;
VkDeviceSize minBufferSize = 0;
Bool persistentlyMapped = false;
};
class BufferArena {
public:
Bool Initialize(const BufferArenaDesc& desc);
void Shutdown();
void BeginFrame(Uint32 frameIndex);
void ResetFrame(Uint32 frameIndex);
void CollectDeferredReleases(Uint32 frameIndex);
Bool Allocate(Uint32 frameIndex, VkDeviceSize size, VkDeviceSize alignment, BufferSlice& outSlice);
Bool Upload(Uint32 frameIndex, const void* data, VkDeviceSize size, VkDeviceSize alignment, BufferSlice& outSlice);
VkDeviceSize GetWriteCursor(Uint32 frameIndex) const;
Uint32 GetFrameCount() const;
private:
struct FrameResources {
VkBufferObject buffer;
VkDeviceSize writeCursor = 0;
};
Bool EnsureCapacity(Uint32 frameIndex, VkDeviceSize requiredEndOffset);
void AssertValidFrameIndex(Uint32 frameIndex) const;
BufferArenaDesc m_desc{};
Vector<FrameResources> m_frames;
Vector<Vector<VkBufferObject>> m_deferredReleases;
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -0,0 +1,23 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/BufferSlice.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 {
struct BufferSlice {
VkBuffer buffer = VK_NULL_HANDLE;
VkDeviceSize offset = 0;
VkDeviceSize size = 0;
void* mapped = nullptr;
Bool IsValid() const { return buffer != VK_NULL_HANDLE; }
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -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
@@ -9,8 +9,111 @@
#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");
}
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,17 +122,42 @@ 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)));
if (payload.colorAttachmentCount > 0) {
XXHASH_VERIFY(XXH64_update(
m_hashState,
payload.colorBlendAttachments.data(),
sizeof(payload.colorBlendAttachments[0]) * payload.colorAttachmentCount));
}
return XXH64_digest(m_hashState);
}
@@ -59,10 +187,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{};
@@ -72,39 +214,55 @@ 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.cullMode = VK_CULL_MODE_NONE;
raster.frontFace = VK_FRONT_FACE_CLOCKWISE;
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];
}
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());
@@ -122,8 +280,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,29 +19,42 @@ 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;
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;
@@ -53,6 +67,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
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();
};
File diff suppressed because it is too large Load Diff
@@ -11,11 +11,22 @@
#include "../VkIncludes.h"
#include "MG_State/GLState/ProgramState/ProgramObject.h"
#include "MG_State/GLState/ProgramState/ShaderObject.h"
#include "MG_State/GLState/TextureState/TextureEnum.h"
#include <Includes.h>
namespace MobileGL::MG_Backend::DirectVulkan {
class ProgramFactory {
public:
enum class DescriptorBindingKind : Uint8 {
None = 0,
UniformBufferDynamic,
CombinedImageSampler,
UniformTexelBuffer,
StorageBuffer,
StorageImage
};
enum class CompileOptionBit : Uint {
None = 0,
PositionYFlip = 1 << 0,
@@ -26,69 +37,176 @@ namespace MobileGL::MG_Backend::DirectVulkan {
};
using CompileOptionFlags = Flags<CompileOptionBit>;
using HashType = Uint64;
struct BackendProgramObject {
struct VkProgramObject {
static constexpr Uint32 kMaxVertexInputLocations = 32;
HashType hash = 0;
VkDevice device = VK_NULL_HANDLE;
Vector<VkPipelineShaderStageCreateInfo> stages;
Vector<VkShaderModule> modules;
BackendProgramObject() = default;
BackendProgramObject(const BackendProgramObject&) = delete;
BackendProgramObject& operator=(const BackendProgramObject&) = delete;
BackendProgramObject(BackendProgramObject&& other) noexcept {
// Layout data (previously in separate VkProgramLayout)
VkDescriptorSetLayout descriptorSetLayout = VK_NULL_HANDLE;
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
Vector<DescriptorBindingKind> bindingKinds;
Vector<Uint32> dynamicBindings;
Vector<Int> uniformBlockIndexByBinding;
Vector<String> samplerNameByBinding;
Vector<Int> samplerUniformLocationByBinding;
Vector<TextureTarget> samplerTextureTargetByBinding;
Vector<String> storageBlockNameByBinding;
Vector<Int> storageBlockIndexByBinding;
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;
static inline VkDevice s_device = VK_NULL_HANDLE;
VkProgramObject() = default;
VkProgramObject(const VkProgramObject&) = delete;
VkProgramObject& operator=(const VkProgramObject&) = delete;
VkProgramObject(VkProgramObject&& other) noexcept {
hash = other.hash;
device = other.device;
stages = std::move(other.stages);
modules = std::move(other.modules);
descriptorSetLayout = other.descriptorSetLayout;
pipelineLayout = other.pipelineLayout;
bindingKinds = std::move(other.bindingKinds);
dynamicBindings = std::move(other.dynamicBindings);
uniformBlockIndexByBinding = std::move(other.uniformBlockIndexByBinding);
samplerNameByBinding = std::move(other.samplerNameByBinding);
samplerUniformLocationByBinding = std::move(other.samplerUniformLocationByBinding);
samplerTextureTargetByBinding = std::move(other.samplerTextureTargetByBinding);
storageBlockNameByBinding = std::move(other.storageBlockNameByBinding);
storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding);
globalUboBinding = other.globalUboBinding;
activeVertexInputLocationMask = other.activeVertexInputLocationMask;
vertexInputTypes = other.vertexInputTypes;
activeFragmentOutputLocationMask = other.activeFragmentOutputLocationMask;
fragmentOutputTypes = other.fragmentOutputTypes;
rasterizationProducerStage = other.rasterizationProducerStage;
producerOutputComponentCount = other.producerOutputComponentCount;
fragmentInputComponentCount = other.fragmentInputComponentCount;
other.hash = 0;
other.device = VK_NULL_HANDLE;
other.descriptorSetLayout = VK_NULL_HANDLE;
other.pipelineLayout = VK_NULL_HANDLE;
other.globalUboBinding = -1;
other.activeVertexInputLocationMask = 0;
other.activeFragmentOutputLocationMask = 0;
other.rasterizationProducerStage = ShaderStage::Unknown;
other.producerOutputComponentCount = 0;
other.fragmentInputComponentCount = 0;
}
BackendProgramObject& operator=(BackendProgramObject&& other) noexcept {
VkProgramObject& operator=(VkProgramObject&& other) noexcept {
if (this == &other) {
return *this;
}
DestroyModules();
stages.clear();
Destroy();
hash = other.hash;
device = other.device;
stages = std::move(other.stages);
modules = std::move(other.modules);
descriptorSetLayout = other.descriptorSetLayout;
pipelineLayout = other.pipelineLayout;
bindingKinds = std::move(other.bindingKinds);
dynamicBindings = std::move(other.dynamicBindings);
uniformBlockIndexByBinding = std::move(other.uniformBlockIndexByBinding);
samplerNameByBinding = std::move(other.samplerNameByBinding);
samplerUniformLocationByBinding = std::move(other.samplerUniformLocationByBinding);
samplerTextureTargetByBinding = std::move(other.samplerTextureTargetByBinding);
storageBlockNameByBinding = std::move(other.storageBlockNameByBinding);
storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding);
globalUboBinding = other.globalUboBinding;
activeVertexInputLocationMask = other.activeVertexInputLocationMask;
vertexInputTypes = other.vertexInputTypes;
activeFragmentOutputLocationMask = other.activeFragmentOutputLocationMask;
fragmentOutputTypes = other.fragmentOutputTypes;
rasterizationProducerStage = other.rasterizationProducerStage;
producerOutputComponentCount = other.producerOutputComponentCount;
fragmentInputComponentCount = other.fragmentInputComponentCount;
other.hash = 0;
other.device = VK_NULL_HANDLE;
other.descriptorSetLayout = VK_NULL_HANDLE;
other.pipelineLayout = VK_NULL_HANDLE;
other.globalUboBinding = -1;
other.activeVertexInputLocationMask = 0;
other.activeFragmentOutputLocationMask = 0;
other.rasterizationProducerStage = ShaderStage::Unknown;
other.producerOutputComponentCount = 0;
other.fragmentInputComponentCount = 0;
return *this;
}
~BackendProgramObject() {
DestroyModules();
stages.clear();
~VkProgramObject() {
Destroy();
}
private:
void DestroyModules() {
for (auto module : modules) {
if (module != VK_NULL_HANDLE && device != VK_NULL_HANDLE) {
vkDestroyShaderModule(device, module, nullptr);
void Destroy() {
if (s_device != VK_NULL_HANDLE) {
if (pipelineLayout != VK_NULL_HANDLE) {
vkDestroyPipelineLayout(s_device, pipelineLayout, nullptr);
pipelineLayout = VK_NULL_HANDLE;
}
if (descriptorSetLayout != VK_NULL_HANDLE) {
vkDestroyDescriptorSetLayout(s_device, descriptorSetLayout, nullptr);
descriptorSetLayout = VK_NULL_HANDLE;
}
for (auto module : modules) {
if (module != VK_NULL_HANDLE) {
vkDestroyShaderModule(s_device, module, nullptr);
}
}
}
modules.clear();
stages.clear();
}
};
explicit ProgramFactory(VkDevice device, const VulkanRendererConfig& config)
: m_device(device), m_config(config) {}
~ProgramFactory();
explicit ProgramFactory(VkDevice device, const VulkanRendererConfig& config, Uint32 maxBindings = 16,
Bool shaderDrawParametersEnabled = false)
: m_device(device), m_maxBindings(maxBindings), m_config(config),
m_shaderDrawParametersEnabled(shaderDrawParametersEnabled) {
VkProgramObject::s_device = device;
}
~ProgramFactory() = default;
ProgramFactory(const ProgramFactory&) = delete;
HashType ComputeHash(const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags) const;
Vector<VkPipelineShaderStageCreateInfo>& GetOrCreatePipelineShaderStages(
const VkProgramObject& GetOrCreateProgram(
const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags);
static VkShaderStageFlagBits ToVkStage(ShaderStage stage);
private:
struct ProgramLookupCache {
const MG_State::GLState::ProgramObject* program = nullptr;
Uint32 backendStateVersion = 0;
CompileOptionFlags flags{};
HashType hash = 0;
};
static TextureTarget UniformTypeToTextureTarget(GLenum glType);
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;
UnorderedMap<HashType, BackendProgramObject> m_cache;
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;
mutable ProgramLookupCache m_lastLookup;
static inline XXH64_state_t* m_hashState = XXH64_createState();
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -8,6 +8,9 @@
#include "SwapchainObject.h"
#include "MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h"
#include "MG_State/GLState/TextureState/TextureObject2D.h"
#if defined(__has_include)
#if __has_include(<vulkan/vk_enum_string_helper.h>)
#include <vulkan/vk_enum_string_helper.h>
@@ -28,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) {
@@ -38,6 +52,40 @@ static const char* string_VkSurfaceTransformFlagBitsKHR(VkSurfaceTransformFlagBi
#endif
namespace MobileGL::MG_Backend::DirectVulkan {
namespace {
Bool HasStencilComponent(VkFormat format) {
return format == VK_FORMAT_D24_UNORM_S8_UINT || format == VK_FORMAT_D32_SFLOAT_S8_UINT;
}
VkFormat FindSupportedDepthStencilFormat(VkPhysicalDevice physicalDevice) {
const VkFormat candidates[] = {VK_FORMAT_D24_UNORM_S8_UINT, VK_FORMAT_D32_SFLOAT_S8_UINT,
VK_FORMAT_D32_SFLOAT};
for (VkFormat format : candidates) {
VkFormatProperties props{};
vkGetPhysicalDeviceFormatProperties(physicalDevice, format, &props);
if ((props.optimalTilingFeatures & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT) != 0) {
return format;
}
}
return VK_FORMAT_UNDEFINED;
}
Uint32 FindMemoryType(VkPhysicalDevice physicalDevice, Uint32 typeFilter, VkMemoryPropertyFlags properties) {
VkPhysicalDeviceMemoryProperties memProperties{};
vkGetPhysicalDeviceMemoryProperties(physicalDevice, &memProperties);
for (Uint32 i = 0; i < memProperties.memoryTypeCount; i++) {
if ((typeFilter & (1 << i)) &&
(memProperties.memoryTypes[i].propertyFlags & properties) == properties) {
return i;
}
}
MOBILEGL_ASSERT(false, "Failed to find suitable memory type.");
return 0;
}
} // namespace
SwapchainObject::SwapchainCapabilities SwapchainObject::GetSwapchainCapabilities(VkPhysicalDevice physicalDevice,
VkSurfaceKHR surface) {
SwapchainCapabilities swapchainCapabilities{};
@@ -68,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;
}
@@ -93,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");
@@ -116,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));
@@ -127,6 +187,20 @@ 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);
}
createInfo.imageArrayLayers = 1;
const VkImageUsageFlags requiredImageUsage =
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
@@ -185,12 +259,163 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_imageLayouts.assign(imageCount, VK_IMAGE_LAYOUT_UNDEFINED);
CreateImageViews(device);
CreateDepthStencilResources(device, physicalDevice);
MGLOG_I("Swapchain created, extent = %dx%d, swapchain imageCount = %d", m_extent.width, m_extent.height,
imageCount);
// 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, {
{extentWidth, extentHeight, 1},
defaultAttachmentByteSize}); // TODO: 4 is format size
TextureInternalFormat depthFormat = TextureInternalFormat::Depth24Stencil8;
switch (m_depthStencilFormat) {
case VK_FORMAT_D24_UNORM_S8_UINT:
depthFormat = TextureInternalFormat::Depth24Stencil8;
break;
case VK_FORMAT_D32_SFLOAT_S8_UINT:
depthFormat = TextureInternalFormat::Depth32FStencil8;
break;
case VK_FORMAT_D32_SFLOAT:
depthFormat = TextureInternalFormat::DepthComponent32F;
break;
default:
depthFormat = TextureInternalFormat::Depth24Stencil8;
break;
}
auto* depthTex = static_cast<MG_State::GLState::TextureObject2D*>(defaultFBOInfo->depthAttachment.get());
depthTex->SetInternalFormat(depthFormat);
depthTex->AllocateStorage(TextureUploadTarget::Texture2D, 0, {
{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
}
void SwapchainObject::CreateDepthStencilResources(VkDevice device, VkPhysicalDevice physicalDevice) {
DestroyDepthStencilResources(device);
const auto imageCount = static_cast<Uint32>(m_images.size());
if (imageCount == 0) {
return;
}
m_depthStencilFormat = FindSupportedDepthStencilFormat(physicalDevice);
MOBILEGL_ASSERT(m_depthStencilFormat != VK_FORMAT_UNDEFINED, "No supported depth/stencil format found.");
m_depthStencilImages.assign(imageCount, VK_NULL_HANDLE);
m_depthStencilImageMemories.assign(imageCount, VK_NULL_HANDLE);
m_depthStencilImageViews.assign(imageCount, VK_NULL_HANDLE);
m_depthStencilImageLayouts.assign(imageCount, VK_IMAGE_LAYOUT_UNDEFINED);
for (Uint32 i = 0; i < imageCount; ++i) {
VkImageCreateInfo imageInfo{};
imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
imageInfo.imageType = VK_IMAGE_TYPE_2D;
imageInfo.extent.width = m_extent.width;
imageInfo.extent.height = m_extent.height;
imageInfo.extent.depth = 1;
imageInfo.mipLevels = 1;
imageInfo.arrayLayers = 1;
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 | 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)");
VkMemoryRequirements memRequirements{};
vkGetImageMemoryRequirements(device, m_depthStencilImages[i], &memRequirements);
VkMemoryAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
allocInfo.allocationSize = memRequirements.size;
allocInfo.memoryTypeIndex =
FindMemoryType(physicalDevice, memRequirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
VK_VERIFY(vkAllocateMemory(device, &allocInfo, nullptr, &m_depthStencilImageMemories[i]),
"vkAllocateMemory(depth)");
VK_VERIFY(vkBindImageMemory(device, m_depthStencilImages[i], m_depthStencilImageMemories[i], 0),
"vkBindImageMemory(depth)");
VkImageViewCreateInfo viewInfo{};
viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
viewInfo.image = m_depthStencilImages[i];
viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
viewInfo.format = m_depthStencilFormat;
viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
if (HasStencilComponent(m_depthStencilFormat)) {
viewInfo.subresourceRange.aspectMask |= VK_IMAGE_ASPECT_STENCIL_BIT;
}
viewInfo.subresourceRange.baseMipLevel = 0;
viewInfo.subresourceRange.levelCount = 1;
viewInfo.subresourceRange.baseArrayLayer = 0;
viewInfo.subresourceRange.layerCount = 1;
VK_VERIFY(vkCreateImageView(device, &viewInfo, nullptr, &m_depthStencilImageViews[i]),
"vkCreateImageView(depth)");
}
}
void SwapchainObject::DestroyDepthStencilResources(VkDevice device) {
for (auto view : m_depthStencilImageViews) {
if (view != VK_NULL_HANDLE) {
vkDestroyImageView(device, view, nullptr);
}
}
m_depthStencilImageViews.clear();
for (auto image : m_depthStencilImages) {
if (image != VK_NULL_HANDLE) {
vkDestroyImage(device, image, nullptr);
}
}
m_depthStencilImages.clear();
for (auto memory : m_depthStencilImageMemories) {
if (memory != VK_NULL_HANDLE) {
vkFreeMemory(device, memory, nullptr);
}
}
m_depthStencilImageMemories.clear();
m_depthStencilImageLayouts.clear();
m_depthStencilFormat = VK_FORMAT_UNDEFINED;
}
void SwapchainObject::Shutdown(VkDevice device) {
DestroyDepthStencilResources(device);
for (auto imageView : m_imageViews) {
vkDestroyImageView(device, imageView, nullptr);
}
@@ -221,6 +446,29 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_imageLayouts[index] = layout;
}
VkImage SwapchainObject::GetDepthStencilImage(Uint32 index) const {
MOBILEGL_ASSERT(index < m_depthStencilImages.size(), "Swapchain depth/stencil image index out of range");
return m_depthStencilImages[index];
}
VkImageView SwapchainObject::GetDepthStencilImageView(Uint32 index) const {
MOBILEGL_ASSERT(index < m_depthStencilImageViews.size(),
"Swapchain depth/stencil image view index out of range");
return m_depthStencilImageViews[index];
}
VkImageLayout SwapchainObject::GetDepthStencilImageLayout(Uint32 index) const {
MOBILEGL_ASSERT(index < m_depthStencilImageLayouts.size(),
"Swapchain depth/stencil image layout index out of range");
return m_depthStencilImageLayouts[index];
}
void SwapchainObject::SetDepthStencilImageLayout(Uint32 index, VkImageLayout layout) {
MOBILEGL_ASSERT(index < m_depthStencilImageLayouts.size(),
"Swapchain depth/stencil image layout index out of range");
m_depthStencilImageLayouts[index] = layout;
}
void SwapchainObject::CreateImageViews(VkDevice device) {
m_imageViews.resize(m_images.size(), VK_NULL_HANDLE);
for (SizeT i = 0; i < m_imageViews.size(); i++) {
@@ -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; }
@@ -38,6 +38,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkSurfaceTransformFlagBitsKHR GetPreTransform() const { return m_preTransform; }
const Vector<VkImage>& GetImages() const { return m_images; }
const Vector<VkImageView>& GetImageViews() const { return m_imageViews; }
VkFormat GetDepthStencilFormat() const { return m_depthStencilFormat; }
const Vector<VkImageView>& GetDepthStencilImageViews() const { return m_depthStencilImageViews; }
VkImage GetDepthStencilImage(Uint32 index) const;
VkImageView GetDepthStencilImageView(Uint32 index) const;
VkImageLayout GetDepthStencilImageLayout(Uint32 index) const;
void SetDepthStencilImageLayout(Uint32 index, VkImageLayout layout);
VkImage GetImage(Uint32 index) const;
VkImageLayout GetImageLayout(Uint32 index) const;
void SetImageLayout(Uint32 index, VkImageLayout layout);
@@ -45,6 +51,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
private:
void CreateImageViews(VkDevice device);
void CreateDepthStencilResources(VkDevice device, VkPhysicalDevice physicalDevice);
void DestroyDepthStencilResources(VkDevice device);
static constexpr VkPresentModeKHR s_desiredPresentModes[] {
VK_PRESENT_MODE_MAILBOX_KHR,
VK_PRESENT_MODE_IMMEDIATE_KHR,
@@ -59,5 +67,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Vector<VkImage> m_images;
Vector<VkImageView> m_imageViews;
Vector<VkImageLayout> m_imageLayouts;
VkFormat m_depthStencilFormat = VK_FORMAT_UNDEFINED;
Vector<VkImage> m_depthStencilImages;
Vector<VkDeviceMemory> m_depthStencilImageMemories;
Vector<VkImageView> m_depthStencilImageViews;
Vector<VkImageLayout> m_depthStencilImageLayouts;
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -1,875 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/UniformDescriptorBinder.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 "UniformDescriptorBinder.h"
#include "VkFramebufferManager.h"
#include "MG_State/GLState/Core.h"
#include "MG_State/GLState/ProgramState/ProgramObject.h"
#include "MG_Util/ShaderTranspiler/Types.h"
#include <limits>
namespace MobileGL::MG_Backend::DirectVulkan {
VkDeviceSize UniformDescriptorBinder::AlignUp(VkDeviceSize value, VkDeviceSize alignment) {
if (alignment == 0) {
return value;
}
return (value + alignment - 1) / alignment * alignment;
}
Uint64 UniformDescriptorBinder::ComputeProgramHash(const MG_State::GLState::ProgramObject& program) {
XXH64_state_t* state = XXH64_createState();
XXHASH_VERIFY(XXH64_reset(state, 0xC0D3A11ULL));
const auto& spirv = program.GetGeneratedSpirv();
for (const auto& module : spirv) {
XXHASH_VERIFY(XXH64_update(state, module.data(), module.size() * sizeof(Uint)));
}
const Uint32 blockCount = static_cast<Uint32>(program.GetActiveUniformBlocksCount());
XXHASH_VERIFY(XXH64_update(state, &blockCount, sizeof(blockCount)));
for (Uint32 i = 0; i < blockCount; ++i) {
const Uint32 binding = program.GetUniformBlockBinding(i);
XXHASH_VERIFY(XXH64_update(state, &binding, sizeof(binding)));
}
const Uint64 hash = XXH64_digest(state);
XXH64_freeState(state);
return hash;
}
Bool UniformDescriptorBinder::IsSamplerUniformType(GLenum glType) {
switch (glType) {
case GL_SAMPLER_1D:
case GL_SAMPLER_2D:
case GL_SAMPLER_3D:
case GL_SAMPLER_CUBE:
case GL_SAMPLER_1D_SHADOW:
case GL_SAMPLER_2D_SHADOW:
case GL_SAMPLER_1D_ARRAY:
case GL_SAMPLER_2D_ARRAY:
case GL_SAMPLER_1D_ARRAY_SHADOW:
case GL_SAMPLER_2D_ARRAY_SHADOW:
case GL_SAMPLER_2D_MULTISAMPLE:
case GL_SAMPLER_2D_MULTISAMPLE_ARRAY:
case GL_SAMPLER_CUBE_SHADOW:
case GL_SAMPLER_BUFFER:
case GL_SAMPLER_2D_RECT:
case GL_SAMPLER_2D_RECT_SHADOW:
case GL_INT_SAMPLER_1D:
case GL_INT_SAMPLER_2D:
case GL_INT_SAMPLER_3D:
case GL_INT_SAMPLER_CUBE:
case GL_INT_SAMPLER_1D_ARRAY:
case GL_INT_SAMPLER_2D_ARRAY:
case GL_INT_SAMPLER_2D_MULTISAMPLE:
case GL_INT_SAMPLER_2D_MULTISAMPLE_ARRAY:
case GL_INT_SAMPLER_BUFFER:
case GL_INT_SAMPLER_2D_RECT:
case GL_UNSIGNED_INT_SAMPLER_1D:
case GL_UNSIGNED_INT_SAMPLER_2D:
case GL_UNSIGNED_INT_SAMPLER_3D:
case GL_UNSIGNED_INT_SAMPLER_CUBE:
case GL_UNSIGNED_INT_SAMPLER_1D_ARRAY:
case GL_UNSIGNED_INT_SAMPLER_2D_ARRAY:
case GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE:
case GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE_ARRAY:
case GL_UNSIGNED_INT_SAMPLER_BUFFER:
case GL_UNSIGNED_INT_SAMPLER_2D_RECT:
return true;
default:
return false;
}
}
TextureTarget UniformDescriptorBinder::UniformTypeToTextureTarget(GLenum glType) {
switch (glType) {
case GL_SAMPLER_1D:
case GL_INT_SAMPLER_1D:
case GL_UNSIGNED_INT_SAMPLER_1D:
return TextureTarget::Texture1D;
case GL_SAMPLER_3D:
case GL_INT_SAMPLER_3D:
case GL_UNSIGNED_INT_SAMPLER_3D:
return TextureTarget::Texture3D;
case GL_SAMPLER_CUBE:
case GL_SAMPLER_CUBE_SHADOW:
case GL_INT_SAMPLER_CUBE:
case GL_UNSIGNED_INT_SAMPLER_CUBE:
return TextureTarget::TextureCubeMap;
case GL_SAMPLER_2D_MULTISAMPLE:
case GL_INT_SAMPLER_2D_MULTISAMPLE:
case GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE:
return TextureTarget::Texture2DMultisample;
case GL_SAMPLER_BUFFER:
case GL_INT_SAMPLER_BUFFER:
case GL_UNSIGNED_INT_SAMPLER_BUFFER:
return TextureTarget::TextureBuffer;
case GL_SAMPLER_1D_ARRAY:
case GL_SAMPLER_1D_ARRAY_SHADOW:
case GL_INT_SAMPLER_1D_ARRAY:
case GL_UNSIGNED_INT_SAMPLER_1D_ARRAY:
return TextureTarget::Texture1DArray;
case GL_SAMPLER_2D_ARRAY:
case GL_SAMPLER_2D_ARRAY_SHADOW:
case GL_INT_SAMPLER_2D_ARRAY:
case GL_UNSIGNED_INT_SAMPLER_2D_ARRAY:
return TextureTarget::Texture2DArray;
case GL_SAMPLER_2D_MULTISAMPLE_ARRAY:
case GL_INT_SAMPLER_2D_MULTISAMPLE_ARRAY:
case GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE_ARRAY:
return TextureTarget::Texture2DMultisampleArray;
case GL_SAMPLER_2D_RECT:
case GL_SAMPLER_2D_RECT_SHADOW:
case GL_INT_SAMPLER_2D_RECT:
case GL_UNSIGNED_INT_SAMPLER_2D_RECT:
return TextureTarget::TextureRectangle;
case GL_SAMPLER_2D:
case GL_SAMPLER_2D_SHADOW:
case GL_INT_SAMPLER_2D:
case GL_UNSIGNED_INT_SAMPLER_2D:
default:
return TextureTarget::Texture2D;
}
}
Bool UniformDescriptorBinder::Initialize(VkDevice device, VmaAllocator allocator,
VkDeviceSize minUniformBufferOffsetAlignment, Uint32 frameCount,
Uint32 maxBindings, Uint32 setsPerFrame, VkDeviceSize perFrameUploadBytes,
VkTextureSamplerManager* textureSamplerManager,
VkFramebufferManager* framebufferManager) {
Shutdown();
MOBILEGL_ASSERT(device != VK_NULL_HANDLE, "UniformDescriptorBinder::Initialize requires valid VkDevice");
MOBILEGL_ASSERT(allocator != nullptr, "UniformDescriptorBinder::Initialize requires valid VMA allocator");
MOBILEGL_ASSERT(frameCount > 0, "UniformDescriptorBinder::Initialize requires frameCount > 0");
MOBILEGL_ASSERT(maxBindings > 0, "UniformDescriptorBinder::Initialize requires maxBindings > 0");
MOBILEGL_ASSERT(setsPerFrame > 0, "UniformDescriptorBinder::Initialize requires setsPerFrame > 0");
m_device = device;
m_allocator = allocator;
m_minDynamicOffsetAlignment = std::max<VkDeviceSize>(1, minUniformBufferOffsetAlignment);
m_perFrameUploadBytes = perFrameUploadBytes;
m_frameCount = frameCount;
m_maxBindings = maxBindings;
m_setsPerFrame = setsPerFrame;
m_peakDescriptorSetsObserved = 0;
m_textureSamplerManager = textureSamplerManager;
m_framebufferManager = framebufferManager;
m_frames.resize(m_frameCount);
for (Uint32 frameIndex = 0; frameIndex < m_frameCount; ++frameIndex) {
auto& frame = m_frames[frameIndex];
frame.writeCursor = 0;
frame.activeDescriptorPoolIndex = 0;
frame.allocatedSetsThisFrame = 0;
frame.peakAllocatedSetsThisFrame = 0;
frame.descriptorPools.clear();
VkDescriptorPool initialPool = VK_NULL_HANDLE;
if (!CreateDescriptorPool(m_setsPerFrame, initialPool)) {
MGLOG_E("UniformDescriptorBinder::Initialize failed: cannot create frame descriptor pool %u",
frameIndex);
Shutdown();
return false;
}
frame.descriptorPools.push_back({initialPool, m_setsPerFrame, 0});
MGLOG_D("UniformDescriptorBinder: frame %u descriptor pool created (maxSets=%u)", frameIndex, m_setsPerFrame);
const Bool created = frame.uploadBuffer.Create(
m_allocator, m_perFrameUploadBytes, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VMA_MEMORY_USAGE_AUTO,
VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT);
if (!created) {
MGLOG_E("UniformDescriptorBinder::Initialize failed: cannot create frame upload buffer %u", frameIndex);
Shutdown();
return false;
}
}
return true;
}
void UniformDescriptorBinder::Shutdown() {
for (auto& frame : m_frames) {
frame.uploadBuffer.Destroy();
if (m_device != VK_NULL_HANDLE) {
for (auto& bucket : frame.descriptorPools) {
if (bucket.handle != VK_NULL_HANDLE) {
vkDestroyDescriptorPool(m_device, bucket.handle, nullptr);
bucket.handle = VK_NULL_HANDLE;
}
}
}
frame.descriptorPools.clear();
frame.activeDescriptorPoolIndex = 0;
frame.allocatedSetsThisFrame = 0;
frame.peakAllocatedSetsThisFrame = 0;
frame.writeCursor = 0;
}
m_frames.clear();
DestroyProgramLayouts();
m_allocator = nullptr;
m_device = VK_NULL_HANDLE;
m_minDynamicOffsetAlignment = 1;
m_perFrameUploadBytes = 0;
m_frameCount = 0;
m_maxBindings = 0;
m_setsPerFrame = 0;
m_peakDescriptorSetsObserved = 0;
m_textureSamplerManager = nullptr;
m_framebufferManager = nullptr;
}
void UniformDescriptorBinder::BeginFrame(Uint32 frameIndex) {
MOBILEGL_ASSERT(frameIndex < m_frames.size(), "UniformDescriptorBinder::BeginFrame invalid frame index");
auto& frame = m_frames[frameIndex];
if (frame.peakAllocatedSetsThisFrame > m_peakDescriptorSetsObserved) {
m_peakDescriptorSetsObserved = frame.peakAllocatedSetsThisFrame;
MGLOG_D(
"UniformDescriptorBinder: new descriptor set peak observed=%u (base setsPerFrame=%u, frame=%u, pools=%zu)",
m_peakDescriptorSetsObserved, m_setsPerFrame, frameIndex, frame.descriptorPools.size());
}
frame.writeCursor = 0;
frame.activeDescriptorPoolIndex = 0;
frame.allocatedSetsThisFrame = 0;
frame.peakAllocatedSetsThisFrame = 0;
for (auto& bucket : frame.descriptorPools) {
bucket.allocatedSets = 0;
if (bucket.handle == VK_NULL_HANDLE) {
continue;
}
VK_VERIFY(vkResetDescriptorPool(m_device, bucket.handle, 0),
"UniformDescriptorBinder::BeginFrame, vkResetDescriptorPool");
}
}
Bool UniformDescriptorBinder::ReflectBindingKinds(const MG_State::GLState::ProgramObject& program,
Vector<BindingKind>& outKinds) const {
outKinds.assign(m_maxBindings, BindingKind::None);
const auto& spirv = program.GetGeneratedSpirv();
for (const auto& module : spirv) {
if (module.empty()) {
continue;
}
spvc_context context = nullptr;
spvc_parsed_ir ir = nullptr;
spvc_compiler compiler = nullptr;
spvc_resources resources = nullptr;
if (spvc_context_create(&context) != SPVC_SUCCESS) {
return false;
}
const spvc_result parseResult = spvc_context_parse_spirv(context, module.data(), module.size(), &ir);
if (parseResult != SPVC_SUCCESS) {
spvc_context_destroy(context);
continue;
}
const spvc_result compilerResult =
spvc_context_create_compiler(context, SPVC_BACKEND_GLSL, ir, SPVC_CAPTURE_MODE_TAKE_OWNERSHIP, &compiler);
if (compilerResult != SPVC_SUCCESS) {
spvc_context_destroy(context);
continue;
}
if (spvc_compiler_create_shader_resources(compiler, &resources) != SPVC_SUCCESS) {
spvc_context_destroy(context);
continue;
}
const auto applyBindings = [&](spvc_resource_type resourceType, BindingKind kind) {
const spvc_reflected_resource* list = nullptr;
size_t count = 0;
if (spvc_resources_get_resource_list_for_type(resources, resourceType, &list, &count) != SPVC_SUCCESS) {
return;
}
for (size_t i = 0; i < count; ++i) {
const Uint32 binding =
spvc_compiler_get_decoration(compiler, list[i].id, SpvDecorationBinding);
if (binding >= m_maxBindings) {
continue;
}
if (kind == BindingKind::CombinedImageSampler) {
outKinds[binding] = BindingKind::CombinedImageSampler;
} else if (outKinds[binding] == BindingKind::None) {
outKinds[binding] = kind;
}
}
};
applyBindings(SPVC_RESOURCE_TYPE_UNIFORM_BUFFER, BindingKind::UniformBufferDynamic);
applyBindings(SPVC_RESOURCE_TYPE_SAMPLED_IMAGE, BindingKind::CombinedImageSampler);
spvc_context_destroy(context);
}
return true;
}
Bool UniformDescriptorBinder::ReflectSamplerBindings(const MG_State::GLState::ProgramObject& program,
ProgramLayout& layout) const {
layout.samplerUniformLocationByBinding.assign(m_maxBindings, -1);
layout.samplerTextureTargetByBinding.assign(m_maxBindings, TextureTarget::Texture2D);
const auto& spirv = program.GetGeneratedSpirv();
for (const auto& module : spirv) {
if (module.empty()) {
continue;
}
spvc_context context = nullptr;
spvc_parsed_ir ir = nullptr;
spvc_compiler compiler = nullptr;
spvc_resources resources = nullptr;
if (spvc_context_create(&context) != SPVC_SUCCESS) {
return false;
}
if (spvc_context_parse_spirv(context, module.data(), module.size(), &ir) != SPVC_SUCCESS) {
spvc_context_destroy(context);
continue;
}
if (spvc_context_create_compiler(context, SPVC_BACKEND_GLSL, ir, SPVC_CAPTURE_MODE_TAKE_OWNERSHIP,
&compiler) != SPVC_SUCCESS) {
spvc_context_destroy(context);
continue;
}
if (spvc_compiler_create_shader_resources(compiler, &resources) != SPVC_SUCCESS) {
spvc_context_destroy(context);
continue;
}
const spvc_reflected_resource* list = nullptr;
size_t count = 0;
if (spvc_resources_get_resource_list_for_type(resources, SPVC_RESOURCE_TYPE_SAMPLED_IMAGE, &list, &count) ==
SPVC_SUCCESS) {
for (size_t i = 0; i < count; ++i) {
const Uint32 binding =
spvc_compiler_get_decoration(compiler, list[i].id, SpvDecorationBinding);
if (binding >= m_maxBindings) {
continue;
}
String uniformName = list[i].name ? list[i].name : "";
Int location = program.GetUniformLocation(uniformName);
if (location < 0) {
const auto arraySuffix = uniformName.find("[0]");
if (arraySuffix != String::npos) {
uniformName = uniformName.substr(0, arraySuffix);
location = program.GetUniformLocation(uniformName);
}
}
if (location < 0) {
continue;
}
layout.samplerUniformLocationByBinding[binding] = location;
layout.samplerTextureTargetByBinding[binding] =
UniformTypeToTextureTarget(program.GetUniformType(static_cast<Uint>(location)));
}
}
spvc_context_destroy(context);
}
return true;
}
Bool UniformDescriptorBinder::ReflectGlobalUboBinding(const MG_State::GLState::ProgramObject& program,
ProgramLayout& layout) const {
layout.globalUboBinding = -1;
const auto& spirv = program.GetGeneratedSpirv();
for (const auto& module : spirv) {
if (module.empty()) {
continue;
}
spvc_context context = nullptr;
spvc_parsed_ir ir = nullptr;
spvc_compiler compiler = nullptr;
spvc_resources resources = nullptr;
if (spvc_context_create(&context) != SPVC_SUCCESS) {
return false;
}
if (spvc_context_parse_spirv(context, module.data(), module.size(), &ir) != SPVC_SUCCESS) {
spvc_context_destroy(context);
continue;
}
if (spvc_context_create_compiler(context, SPVC_BACKEND_GLSL, ir, SPVC_CAPTURE_MODE_TAKE_OWNERSHIP,
&compiler) != SPVC_SUCCESS) {
spvc_context_destroy(context);
continue;
}
if (spvc_compiler_create_shader_resources(compiler, &resources) != SPVC_SUCCESS) {
spvc_context_destroy(context);
continue;
}
const spvc_reflected_resource* list = nullptr;
size_t count = 0;
if (spvc_resources_get_resource_list_for_type(resources, SPVC_RESOURCE_TYPE_UNIFORM_BUFFER, &list, &count) ==
SPVC_SUCCESS) {
for (size_t i = 0; i < count; ++i) {
const char* name = list[i].name ? list[i].name : "";
if (std::strstr(name, MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME) == nullptr) {
continue;
}
const Uint32 binding =
spvc_compiler_get_decoration(compiler, list[i].id, SpvDecorationBinding);
if (binding < m_maxBindings) {
layout.globalUboBinding = static_cast<Int>(binding);
}
break;
}
}
spvc_context_destroy(context);
if (layout.globalUboBinding >= 0) {
break;
}
}
return true;
}
Bool UniformDescriptorBinder::ResolveSamplerDescriptor(VkCommandBuffer commandBuffer,
const MG_State::GLState::ProgramObject& program,
const ProgramLayout& layout, Uint32 binding,
VkDescriptorImageInfo& outImageInfo) const {
if (!m_textureSamplerManager || !MG_State::pGLContext || binding >= layout.samplerUniformLocationByBinding.size()) {
return false;
}
const Int location = layout.samplerUniformLocationByBinding[binding];
if (location < 0) {
return false;
}
const Int unit = program.GetUniformSamplerOrImageUnitIndex(static_cast<Uint>(location));
if (unit < 0) {
return false;
}
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
const auto samplerOverride = textureUnit.GetSamplerObject();
const TextureTarget preferredTarget = layout.samplerTextureTargetByBinding[binding];
auto texture = textureUnit.GetBindingSlot(preferredTarget).GetBoundObject();
if (!texture) {
auto& slots = textureUnit.GetAllBindingSlots();
for (auto& slot : slots) {
texture = slot.GetBoundObject();
if (texture) {
break;
}
}
}
if (!texture) {
return false;
}
if (m_framebufferManager &&
m_framebufferManager->TransitionOffscreenColorTextureToShaderRead(commandBuffer, texture->GetExternalIndex())) {
VkImageView offscreenView = VK_NULL_HANDLE;
if (m_framebufferManager->GetOffscreenColorViewByTexture(texture->GetExternalIndex(), offscreenView) &&
offscreenView != VK_NULL_HANDLE) {
VkDescriptorImageInfo sampledInfo{};
if (!m_textureSamplerManager->SyncTextureAndGetDescriptor(*texture, samplerOverride.get(), sampledInfo)) {
return false;
}
sampledInfo.imageView = offscreenView;
sampledInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
outImageInfo = sampledInfo;
return true;
}
}
return m_textureSamplerManager->SyncTextureAndGetDescriptor(*texture, samplerOverride.get(), outImageInfo);
}
UniformDescriptorBinder::ProgramLayout* UniformDescriptorBinder::GetOrCreateProgramLayout(
const MG_State::GLState::ProgramObject& program) {
const Uint64 hash = ComputeProgramHash(program);
auto it = m_programLayouts.find(hash);
if (it != m_programLayouts.end()) {
return &it->second;
}
ProgramLayout layout{};
layout.hash = hash;
if (!ReflectBindingKinds(program, layout.bindingKinds)) {
MGLOG_E("UniformDescriptorBinder::GetOrCreateProgramLayout failed: reflection failed");
return nullptr;
}
if (!ReflectSamplerBindings(program, layout)) {
MGLOG_E("UniformDescriptorBinder::GetOrCreateProgramLayout failed: sampler reflection failed");
return nullptr;
}
if (!ReflectGlobalUboBinding(program, layout)) {
MGLOG_E("UniformDescriptorBinder::GetOrCreateProgramLayout failed: global UBO reflection failed");
return nullptr;
}
Vector<VkDescriptorSetLayoutBinding> bindings;
bindings.reserve(m_maxBindings);
for (Uint32 binding = 0; binding < m_maxBindings; ++binding) {
const auto kind = layout.bindingKinds[binding];
if (kind == BindingKind::None) {
continue;
}
VkDescriptorSetLayoutBinding layoutBinding{};
layoutBinding.binding = binding;
layoutBinding.descriptorCount = 1;
layoutBinding.stageFlags = VK_SHADER_STAGE_ALL_GRAPHICS;
layoutBinding.pImmutableSamplers = nullptr;
if (kind == BindingKind::UniformBufferDynamic) {
layoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC;
layout.dynamicBindings.push_back(binding);
} else {
layoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
}
bindings.push_back(layoutBinding);
}
VkDescriptorSetLayoutCreateInfo setLayoutInfo{};
setLayoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
setLayoutInfo.bindingCount = static_cast<Uint32>(bindings.size());
setLayoutInfo.pBindings = bindings.data();
VK_VERIFY(vkCreateDescriptorSetLayout(m_device, &setLayoutInfo, nullptr, &layout.descriptorSetLayout),
"UniformDescriptorBinder::GetOrCreateProgramLayout, vkCreateDescriptorSetLayout");
VkPipelineLayoutCreateInfo pipelineLayoutInfo{};
pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pipelineLayoutInfo.setLayoutCount = 1;
pipelineLayoutInfo.pSetLayouts = &layout.descriptorSetLayout;
VK_VERIFY(vkCreatePipelineLayout(m_device, &pipelineLayoutInfo, nullptr, &layout.pipelineLayout),
"UniformDescriptorBinder::GetOrCreateProgramLayout, vkCreatePipelineLayout");
auto [insertIt, _] = m_programLayouts.emplace(hash, std::move(layout));
return &insertIt->second;
}
VkPipelineLayout UniformDescriptorBinder::GetOrCreatePipelineLayout(const MG_State::GLState::ProgramObject& program) {
auto* layout = GetOrCreateProgramLayout(program);
return layout ? layout->pipelineLayout : VK_NULL_HANDLE;
}
Bool UniformDescriptorBinder::AllocateUploadRegion(FrameResources& frame, VkDeviceSize size, VkDeviceSize& outOffset) {
const VkDeviceSize alignedOffset = AlignUp(frame.writeCursor, m_minDynamicOffsetAlignment);
if (alignedOffset + size > m_perFrameUploadBytes) {
return false;
}
outOffset = alignedOffset;
frame.writeCursor = alignedOffset + size;
return true;
}
Bool UniformDescriptorBinder::GatherBindingPayloads(const MG_State::GLState::ProgramObject& program,
Vector<const void*>& outData,
Vector<VkDeviceSize>& outSizes) const {
outData.assign(m_maxBindings, nullptr);
outSizes.assign(m_maxBindings, 0);
if (MG_State::pGLContext == nullptr) {
return false;
}
const Uint32 activeUniformBlockCount = static_cast<Uint32>(program.GetActiveUniformBlocksCount());
const Uint32 uniformBindingPointCount =
static_cast<Uint32>(MG_State::pGLContext->GetBufferBindingPointCount(BufferTarget::Uniform));
for (Uint32 blockIndex = 0; blockIndex < activeUniformBlockCount; ++blockIndex) {
const Uint32 binding = program.GetUniformBlockBinding(blockIndex);
if (binding >= m_maxBindings) {
continue;
}
VkDeviceSize blockSize = static_cast<VkDeviceSize>(program.GetUBOSizeAt(blockIndex));
if (blockSize == 0) {
continue;
}
if (binding >= uniformBindingPointCount) {
continue;
}
auto& bindingPoint = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::Uniform, binding);
const auto bufferObject = bindingPoint.GetBoundObject();
if (!bufferObject) {
continue;
}
const auto bufferData = bufferObject->GetDataReadOnly();
if (!bufferData || bufferData->empty()) {
continue;
}
const auto range = bindingPoint.GetRange();
const VkDeviceSize bufferSize = static_cast<VkDeviceSize>(bufferObject->GetSize());
VkDeviceSize rangeStart = static_cast<VkDeviceSize>(range.start);
VkDeviceSize rangeEnd = static_cast<VkDeviceSize>(range.end);
if (rangeStart >= bufferSize) {
continue;
}
if (rangeEnd <= rangeStart || rangeEnd > bufferSize) {
rangeEnd = bufferSize;
}
VkDeviceSize available = rangeEnd - rangeStart;
if (available == 0) {
continue;
}
outData[binding] = bufferData->data() + static_cast<SizeT>(rangeStart);
outSizes[binding] = std::min(blockSize, available);
}
return true;
}
Bool UniformDescriptorBinder::CreateDescriptorPool(Uint32 maxSets, VkDescriptorPool& outPool) const {
outPool = VK_NULL_HANDLE;
if (m_device == VK_NULL_HANDLE || maxSets == 0 || m_maxBindings == 0) {
return false;
}
const Uint64 descriptorCount64 = static_cast<Uint64>(maxSets) * static_cast<Uint64>(m_maxBindings);
if (descriptorCount64 > static_cast<Uint64>(std::numeric_limits<Uint32>::max())) {
MGLOG_E("UniformDescriptorBinder::CreateDescriptorPool failed: descriptorCount overflow");
return false;
}
const Uint32 descriptorCount = static_cast<Uint32>(descriptorCount64);
VkDescriptorPoolSize poolSizes[2]{};
poolSizes[0].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC;
poolSizes[0].descriptorCount = descriptorCount;
poolSizes[1].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
poolSizes[1].descriptorCount = descriptorCount;
VkDescriptorPoolCreateInfo poolInfo{};
poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
poolInfo.maxSets = maxSets;
poolInfo.poolSizeCount = static_cast<Uint32>(std::size(poolSizes));
poolInfo.pPoolSizes = poolSizes;
const VkResult result = vkCreateDescriptorPool(m_device, &poolInfo, nullptr, &outPool);
if (result != VK_SUCCESS) {
MGLOG_E("UniformDescriptorBinder::CreateDescriptorPool failed: vkCreateDescriptorPool returned %d", result);
return false;
}
return true;
}
Bool UniformDescriptorBinder::GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex) {
if (frame.descriptorPools.empty()) {
return false;
}
const auto& currentBucket = frame.descriptorPools[frame.activeDescriptorPoolIndex];
const Uint32 currentMaxSets = std::max<Uint32>(1, currentBucket.maxSets);
const Uint32 grownMaxSets = currentMaxSets <= (std::numeric_limits<Uint32>::max() / 2) ? (currentMaxSets * 2)
: currentMaxSets;
VkDescriptorPool grownPool = VK_NULL_HANDLE;
if (!CreateDescriptorPool(grownMaxSets, grownPool)) {
MGLOG_E("UniformDescriptorBinder::GrowFrameDescriptorPool failed: cannot create grown pool (%u -> %u sets)",
currentMaxSets, grownMaxSets);
return false;
}
frame.descriptorPools.push_back({grownPool, grownMaxSets, 0});
frame.activeDescriptorPoolIndex = static_cast<Uint32>(frame.descriptorPools.size() - 1);
MGLOG_D(
"UniformDescriptorBinder: frame %u descriptor pool exhausted, grew pool (%u -> %u sets), poolCount=%zu",
frameIndex, currentMaxSets, grownMaxSets, frame.descriptorPools.size());
return true;
}
Bool UniformDescriptorBinder::BindProgramUniformBuffers(VkCommandBuffer commandBuffer, VkPipelineLayout pipelineLayout,
const MG_State::GLState::ProgramObject& program,
Uint32 frameIndex) {
if (m_frames.empty()) {
MGLOG_E("UniformDescriptorBinder::BindProgramUniformBuffers failed: binder is not initialized");
return false;
}
if (frameIndex >= m_frames.size()) {
MGLOG_E("UniformDescriptorBinder::BindProgramUniformBuffers failed: invalid frame index %u", frameIndex);
return false;
}
ProgramLayout* layout = GetOrCreateProgramLayout(program);
if (!layout || layout->pipelineLayout == VK_NULL_HANDLE || layout->descriptorSetLayout == VK_NULL_HANDLE) {
MGLOG_E("UniformDescriptorBinder::BindProgramUniformBuffers failed: cannot get program layout");
return false;
}
if (layout->pipelineLayout != pipelineLayout) {
MGLOG_E("UniformDescriptorBinder::BindProgramUniformBuffers failed: pipelineLayout mismatch");
return false;
}
auto& frame = m_frames[frameIndex];
if (frame.descriptorPools.empty()) {
MGLOG_E("UniformDescriptorBinder::BindProgramUniformBuffers failed: frame descriptor pools are invalid");
return false;
}
if (frame.activeDescriptorPoolIndex >= frame.descriptorPools.size()) {
frame.activeDescriptorPoolIndex = 0;
}
VkDescriptorSetAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
allocInfo.descriptorSetCount = 1;
allocInfo.pSetLayouts = &layout->descriptorSetLayout;
VkDescriptorSet descriptorSet = VK_NULL_HANDLE;
auto allocateFromActivePool = [&](VkResult& outResult) {
auto& bucket = frame.descriptorPools[frame.activeDescriptorPoolIndex];
allocInfo.descriptorPool = bucket.handle;
outResult = vkAllocateDescriptorSets(m_device, &allocInfo, &descriptorSet);
if (outResult == VK_SUCCESS) {
++bucket.allocatedSets;
++frame.allocatedSetsThisFrame;
frame.peakAllocatedSetsThisFrame = std::max(frame.peakAllocatedSetsThisFrame, frame.allocatedSetsThisFrame);
}
};
VkResult allocResult = VK_SUCCESS;
allocateFromActivePool(allocResult);
if (allocResult == VK_ERROR_OUT_OF_POOL_MEMORY || allocResult == VK_ERROR_FRAGMENTED_POOL) {
if (!GrowFrameDescriptorPool(frame, frameIndex)) {
MGLOG_E("UniformDescriptorBinder::BindProgramUniformBuffers failed: descriptor pool growth failed");
return false;
}
allocateFromActivePool(allocResult);
}
if (allocResult != VK_SUCCESS || descriptorSet == VK_NULL_HANDLE) {
MGLOG_E("UniformDescriptorBinder::BindProgramUniformBuffers failed: vkAllocateDescriptorSets returned %d",
allocResult);
return false;
}
Vector<const void*> bindingData;
Vector<VkDeviceSize> bindingSizes;
if (!GatherBindingPayloads(program, bindingData, bindingSizes)) {
MGLOG_E("UniformDescriptorBinder::BindProgramUniformBuffers failed: cannot gather UBO payloads");
return false;
}
static const Uint8 kFallbackData[16] = {};
VkDescriptorImageInfo fallbackImageInfo{};
const Bool hasFallbackImage = m_textureSamplerManager && m_textureSamplerManager->GetFallbackDescriptor(fallbackImageInfo);
Vector<VkWriteDescriptorSet> writes;
writes.reserve(m_maxBindings);
Vector<VkDescriptorBufferInfo> bufferInfos;
Vector<VkDescriptorImageInfo> imageInfos;
Vector<Uint32> dynamicOffsets;
bufferInfos.reserve(m_maxBindings);
imageInfos.reserve(m_maxBindings);
dynamicOffsets.reserve(layout->dynamicBindings.size());
for (Uint32 binding = 0; binding < m_maxBindings; ++binding) {
const auto kind = layout->bindingKinds[binding];
if (kind == BindingKind::None) {
continue;
}
VkWriteDescriptorSet write{};
write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
write.dstSet = descriptorSet;
write.dstBinding = binding;
write.dstArrayElement = 0;
write.descriptorCount = 1;
if (kind == BindingKind::UniformBufferDynamic) {
const void* payload = bindingData[binding];
VkDeviceSize payloadSize = bindingSizes[binding];
if (payload == nullptr || payloadSize == 0) {
if (layout->globalUboBinding == static_cast<Int>(binding)) {
const void* globalUboData = program.GetUBOData();
const VkDeviceSize globalUboSize = static_cast<VkDeviceSize>(program.GetUBOSize());
if (globalUboData != nullptr && globalUboSize > 0) {
payload = globalUboData;
payloadSize = globalUboSize;
}
}
if (payload == nullptr || payloadSize == 0) {
payload = kFallbackData;
payloadSize = sizeof(kFallbackData);
}
}
VkDeviceSize payloadOffset = 0;
if (!AllocateUploadRegion(frame, payloadSize, payloadOffset)) {
MGLOG_E("UniformDescriptorBinder::BindProgramUniformBuffers failed: frame upload buffer exhausted");
return false;
}
if (!frame.uploadBuffer.Upload(payload, payloadSize, payloadOffset)) {
MGLOG_E("UniformDescriptorBinder::BindProgramUniformBuffers failed: UBO upload failed on binding %u",
binding);
return false;
}
VkDescriptorBufferInfo bufferInfo{};
bufferInfo.buffer = frame.uploadBuffer.GetHandle();
bufferInfo.offset = 0;
bufferInfo.range = payloadSize;
bufferInfos.push_back(bufferInfo);
write.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC;
write.pBufferInfo = &bufferInfos.back();
writes.push_back(write);
dynamicOffsets.push_back(static_cast<Uint32>(payloadOffset));
} else {
VkDescriptorImageInfo imageInfo{};
Bool hasImage = ResolveSamplerDescriptor(commandBuffer, program, *layout, binding, imageInfo);
if (!hasImage) {
if (!hasFallbackImage) {
MGLOG_E("UniformDescriptorBinder::BindProgramUniformBuffers failed: fallback sampler/texture is unavailable");
return false;
}
imageInfo = fallbackImageInfo;
}
if (imageInfo.sampler == VK_NULL_HANDLE || imageInfo.imageView == VK_NULL_HANDLE) {
MGLOG_E("UniformDescriptorBinder::BindProgramUniformBuffers failed: fallback sampler/texture is unavailable");
return false;
}
imageInfos.push_back(imageInfo);
write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
write.pImageInfo = &imageInfos.back();
writes.push_back(write);
}
}
if (!writes.empty()) {
vkUpdateDescriptorSets(m_device, static_cast<Uint32>(writes.size()), writes.data(), 0, nullptr);
}
vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &descriptorSet,
static_cast<Uint32>(dynamicOffsets.size()), dynamicOffsets.data());
return true;
}
void UniformDescriptorBinder::DestroyProgramLayouts() {
for (auto& [_, layout] : m_programLayouts) {
if (layout.pipelineLayout != VK_NULL_HANDLE) {
vkDestroyPipelineLayout(m_device, layout.pipelineLayout, nullptr);
layout.pipelineLayout = VK_NULL_HANDLE;
}
if (layout.descriptorSetLayout != VK_NULL_HANDLE) {
vkDestroyDescriptorSetLayout(m_device, layout.descriptorSetLayout, nullptr);
layout.descriptorSetLayout = VK_NULL_HANDLE;
}
}
m_programLayouts.clear();
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -1,104 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/UniformDescriptorBinder.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 "VkBufferObject.h"
#include "VkTextureSamplerManager.h"
#include "../VkIncludes.h"
#include <Includes.h>
#include <vk_mem_alloc.h>
namespace MobileGL::MG_State::GLState {
class ProgramObject;
}
namespace MobileGL::MG_Backend::DirectVulkan {
class VkFramebufferManager;
class UniformDescriptorBinder {
public:
enum class BindingKind : Uint8 {
None = 0,
UniformBufferDynamic,
CombinedImageSampler
};
Bool Initialize(VkDevice device, VmaAllocator allocator, VkDeviceSize minUniformBufferOffsetAlignment,
Uint32 frameCount, Uint32 maxBindings = 16, Uint32 setsPerFrame = 64,
VkDeviceSize perFrameUploadBytes = 4 * 1024 * 1024,
VkTextureSamplerManager* textureSamplerManager = nullptr,
VkFramebufferManager* framebufferManager = nullptr);
void Shutdown();
void BeginFrame(Uint32 frameIndex);
VkPipelineLayout GetOrCreatePipelineLayout(const MG_State::GLState::ProgramObject& program);
Bool BindProgramUniformBuffers(VkCommandBuffer commandBuffer, VkPipelineLayout pipelineLayout,
const MG_State::GLState::ProgramObject& program, Uint32 frameIndex);
private:
struct DescriptorPoolBucket {
VkDescriptorPool handle = VK_NULL_HANDLE;
Uint32 maxSets = 0;
Uint32 allocatedSets = 0;
};
struct FrameResources {
VkBufferObject uploadBuffer;
Vector<DescriptorPoolBucket> descriptorPools;
Uint32 activeDescriptorPoolIndex = 0;
Uint32 allocatedSetsThisFrame = 0;
Uint32 peakAllocatedSetsThisFrame = 0;
VkDeviceSize writeCursor = 0;
};
struct ProgramLayout {
Uint64 hash = 0;
VkDescriptorSetLayout descriptorSetLayout = VK_NULL_HANDLE;
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
Vector<BindingKind> bindingKinds;
Vector<Uint32> dynamicBindings;
Vector<Int> samplerUniformLocationByBinding;
Vector<TextureTarget> samplerTextureTargetByBinding;
Int globalUboBinding = -1;
};
static VkDeviceSize AlignUp(VkDeviceSize value, VkDeviceSize alignment);
static Uint64 ComputeProgramHash(const MG_State::GLState::ProgramObject& program);
static Bool IsSamplerUniformType(GLenum glType);
static TextureTarget UniformTypeToTextureTarget(GLenum glType);
Bool ReflectSamplerBindings(const MG_State::GLState::ProgramObject& program, ProgramLayout& layout) const;
Bool ReflectGlobalUboBinding(const MG_State::GLState::ProgramObject& program, ProgramLayout& layout) const;
Bool ResolveSamplerDescriptor(VkCommandBuffer commandBuffer, const MG_State::GLState::ProgramObject& program,
const ProgramLayout& layout, Uint32 binding,
VkDescriptorImageInfo& outImageInfo) const;
Bool ReflectBindingKinds(const MG_State::GLState::ProgramObject& program, Vector<BindingKind>& outKinds) const;
ProgramLayout* GetOrCreateProgramLayout(const MG_State::GLState::ProgramObject& program);
Bool AllocateUploadRegion(FrameResources& frame, VkDeviceSize size, VkDeviceSize& outOffset);
Bool GatherBindingPayloads(const MG_State::GLState::ProgramObject& program, Vector<const void*>& outData,
Vector<VkDeviceSize>& outSizes) const;
Bool CreateDescriptorPool(Uint32 maxSets, VkDescriptorPool& outPool) const;
Bool GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex);
void DestroyProgramLayouts();
VkDevice m_device = VK_NULL_HANDLE;
VmaAllocator m_allocator = nullptr;
Vector<FrameResources> m_frames;
UnorderedMap<Uint64, ProgramLayout> m_programLayouts;
VkDeviceSize m_minDynamicOffsetAlignment = 1;
VkDeviceSize m_perFrameUploadBytes = 0;
Uint32 m_frameCount = 0;
Uint32 m_maxBindings = 0;
Uint32 m_setsPerFrame = 0;
Uint32 m_peakDescriptorSetsObserved = 0;
VkTextureSamplerManager* m_textureSamplerManager = nullptr;
VkFramebufferManager* m_framebufferManager = nullptr;
};
} // namespace MobileGL::MG_Backend::DirectVulkan
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,173 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/UniformDescriptorBinder.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 "ProgramFactory.h"
#include "VkBufferManager.h"
#include "VkSamplerManager.h"
#include "VkTextureManager.h"
#include "../VkIncludes.h"
#include <Includes.h>
namespace MobileGL::MG_State::GLState {
class ITextureObject;
class ProgramObject;
class SamplerObject;
}
namespace MobileGL::MG_Backend::DirectVulkan {
class UniformManager {
public:
struct SamplerBindingOverride {
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,
ProgramFactory* programFactory,
VkDeviceSize minUniformBufferOffsetAlignment, Uint32 frameCount,
Uint32 maxBindings = 16, Uint32 setsPerFrame = 64,
VkTextureManager* textureManager = nullptr, VkSamplerManager* samplerManager = nullptr);
void Shutdown();
void BeginFrame(Uint32 frameIndex);
Bool CollectSampledTextures(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj,
Vector<MG_State::GLState::ITextureObject*>& outTextures);
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);
private:
struct DescriptorPoolBucket {
VkDescriptorPool handle = VK_NULL_HANDLE;
Uint32 maxSets = 0;
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;
};
static Bool ResolveSamplerTexture(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
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 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,
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;
ProgramFactory* m_programFactory = nullptr;
Vector<FrameResources> m_frames;
VkDeviceSize m_minDynamicOffsetAlignment = 1;
Uint32 m_frameCount = 0;
Uint32 m_maxBindings = 0;
Uint32 m_setsPerFrame = 0;
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 valid = 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,72 @@ 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;
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);
const auto vkFormat = ToVkVertexFormat(attr.Type, attr.Size, attr.Normalized, attr.IsInteger, attr.IsBgra);
if (vkFormat == VK_FORMAT_UNDEFINED) {
MGLOG_D("Skipping unsupported vertex attribute layout (location=%u, type=%s, size=%d)",
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)",
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 stride =
attr.Stride > 0 ? static_cast<Uint32>(attr.Stride) : static_cast<Uint32>(attribByteSize);
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);
builder.AddBinding(binding, stride, inputRate);
builder.AddAttribute(location, binding, vkFormat, 0);
}
const auto& state = builder.Build();
@@ -111,14 +114,42 @@ 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.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 +158,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 +190,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 +205,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 +220,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 +235,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 +272,14 @@ 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);
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -24,6 +24,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Vector<VkVertexInputBindingDescription> bindings;
Vector<VkVertexInputAttributeDescription> attributes;
Vector<SizeT> bindingBufferKeys;
Vector<SizeT> bindingBaseOffsets;
Vector<Uint32> bindingAttributeLocations;
Vector<Bool> bindingUsesClientMemory;
// 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
};
@@ -35,11 +42,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
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);
const VulkanRendererConfig& m_config;
UnorderedMap<HashType, BackendVertexInputState> m_cache;
@@ -0,0 +1,620 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.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 "VkBufferManager.h"
namespace MobileGL::MG_Backend::DirectVulkan {
namespace {
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) {
Shutdown();
MOBILEGL_ASSERT(initInfo.allocator != nullptr, "VkBufferManager::Initialize requires valid allocator");
MOBILEGL_ASSERT(initInfo.frameCount > 0, "VkBufferManager::Initialize requires non-zero frame count");
m_initInfo = initInfo;
m_deferredBufferReleases.resize(initInfo.frameCount);
m_deferredResourceReleases.resize(initInfo.frameCount);
m_currentFrameIndex = 0;
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();
DestroyAllDeferredReleases();
ReleaseAllLiveResources();
m_copyProvider = nullptr;
m_initInfo = {};
m_currentFrameIndex = 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(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;
DestroyAllDeferredReleases();
m_deferredBufferReleases.resize(frameCount);
m_deferredResourceReleases.resize(frameCount);
m_currentFrameIndex = 0;
return InitializeTransientArenas();
}
void VkBufferManager::BeginFrame(Uint32 frameIndex) {
MOBILEGL_ASSERT(frameIndex < m_deferredBufferReleases.size(),
"VkBufferManager::BeginFrame frame index out of range");
m_currentFrameIndex = 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;
return m_transientUploadArena.Upload(frameIndex, data, size, alignment, outSlice);
}
Bool VkBufferManager::InitializeTransientArenas() {
return m_transientUploadArena.Initialize({
.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_INDIRECT_BUFFER_BIT |
VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
.memoryUsage = m_initInfo.transientMemoryUsage,
.allocationFlags = m_initInfo.transientAllocationFlags,
.minBufferSize = m_initInfo.minUploadBytes,
.persistentlyMapped = m_initInfo.transientPersistentMapping,
});
}
VkBufferResource* VkBufferManager::ResourceOf(MG_State::GLState::BufferObject& bufferObject) {
return static_cast<VkBufferResource*>(bufferObject.GetBackendResource().get());
}
SharedPtr<VkBufferResource> VkBufferManager::GetOrCreateResource(
const SharedPtr<MG_State::GLState::BufferObject>& bufferObject) {
auto existing = std::static_pointer_cast<VkBufferResource>(bufferObject->GetBackendResource());
if (existing) {
return existing;
}
auto resource = MakeShared<VkBufferResource>();
bufferObject->SetBackendResource(resource);
TrackLiveResource(resource);
return resource;
}
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;
}
resource.storageSize = size;
resource.usageFlags = usage;
return true;
}
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;
}
if (!resource.buffer.Upload(bufferObject.MappedData(), size, 0)) {
MGLOG_E("VkBufferManager::SwapStorageAndUploadAll: upload failed");
resource.pendingFullUpload = true;
return false;
}
resource.pendingFullUpload = false;
return true;
}
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_deferredBufferReleases.empty()) {
buffer.Destroy();
return;
}
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::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) {
switch (kind) {
case BufferKind::Vertex:
case BufferKind::Index:
// A GL buffer can be rebound between ARRAY_BUFFER and ELEMENT_ARRAY_BUFFER,
// and may even be used as both within the same draw setup. Keep resident
// vertex/index buffers compatible with both roles from the start so we
// 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::DestroyAllDeferredReleases() {
for (auto& releases : m_deferredBufferReleases) {
for (auto& buffer : releases) {
buffer.Destroy();
}
releases.clear();
}
m_deferredBufferReleases.clear();
for (auto& releases : m_deferredResourceReleases) {
for (auto& resource : releases) {
resource->buffer.Destroy();
}
releases.clear();
}
m_deferredResourceReleases.clear();
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -0,0 +1,153 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.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 "BufferArena.h"
#include "MG_State/GLState/BufferState/BufferObject.h"
#include "../VkIncludes.h"
#include <Includes.h>
#include <vk_mem_alloc.h>
namespace MobileGL::MG_Backend::DirectVulkan {
enum class BufferKind : Uint8 {
Vertex,
Index,
Uniform,
TextureBuffer,
ShaderStorage,
Indirect,
};
struct VkBufferManagerInitInfo {
VmaAllocator allocator = nullptr;
Uint32 frameCount = 0;
VkDeviceSize minUploadBytes = 4 * 1024 * 1024;
VmaMemoryUsage transientMemoryUsage = VMA_MEMORY_USAGE_AUTO;
VmaAllocationCreateFlags transientAllocationFlags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT;
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);
void Shutdown();
// 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);
// 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:
Bool InitializeTransientArenas();
static VkBufferUsageFlags GetVkBufferUsage(BufferKind kind);
SharedPtr<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;
IBufferCopyCommandProvider* m_copyProvider = nullptr;
Vector<Vector<VkBufferObject>> m_deferredBufferReleases;
Vector<Vector<SharedPtr<VkBufferResource>>> m_deferredResourceReleases;
Vector<WeakPtr<VkBufferResource>> m_liveResources;
Uint32 m_currentFrameIndex = 0;
Uint64 m_frameSerial = 1;
Uint64 m_completedSerialFloor = 0;
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -13,11 +13,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_allocator = other.m_allocator;
m_buffer = other.m_buffer;
m_allocation = other.m_allocation;
m_mappedData = other.m_mappedData;
m_size = other.m_size;
other.m_allocator = nullptr;
other.m_buffer = VK_NULL_HANDLE;
other.m_allocation = nullptr;
other.m_mappedData = nullptr;
other.m_size = 0;
}
@@ -31,11 +33,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_allocator = other.m_allocator;
m_buffer = other.m_buffer;
m_allocation = other.m_allocation;
m_mappedData = other.m_mappedData;
m_size = other.m_size;
other.m_allocator = nullptr;
other.m_buffer = VK_NULL_HANDLE;
other.m_allocation = nullptr;
other.m_mappedData = nullptr;
other.m_size = 0;
return *this;
}
@@ -44,8 +48,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Destroy();
}
Bool VkBufferObject::Create(const VkBufferObjectDesc& desc) {
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");
@@ -61,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);
@@ -78,6 +89,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
void VkBufferObject::Destroy() {
Unmap();
if (m_allocator != nullptr && m_buffer != VK_NULL_HANDLE && m_allocation != nullptr) {
vmaDestroyBuffer(m_allocator, m_buffer, m_allocation);
}
@@ -87,6 +99,33 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_size = 0;
}
void* VkBufferObject::Map() {
MOBILEGL_ASSERT(IsValid(), "VkBufferObject::Map called on invalid buffer");
if (m_mappedData != nullptr) {
return m_mappedData;
}
const VkResult mapResult = vmaMapMemory(m_allocator, m_allocation, &m_mappedData);
if (mapResult != VK_SUCCESS || m_mappedData == nullptr) {
MGLOG_E("VkBufferObject::Map failed: vmaMapMemory returned %d", mapResult);
m_mappedData = nullptr;
return nullptr;
}
return m_mappedData;
}
void VkBufferObject::Unmap() {
if (!IsValid() || m_mappedData == nullptr) {
m_mappedData = nullptr;
return;
}
vmaUnmapMemory(m_allocator, m_allocation);
m_mappedData = nullptr;
}
Bool VkBufferObject::Upload(const void* data, VkDeviceSize size, VkDeviceSize offset) {
MOBILEGL_ASSERT(IsValid(), "VkBufferObject::Upload called on invalid buffer");
MOBILEGL_ASSERT(data != nullptr || size == 0, "VkBufferObject::Upload data pointer is null");
@@ -96,15 +135,38 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return true;
}
void* mapped = nullptr;
const VkResult mapResult = vmaMapMemory(m_allocator, m_allocation, &mapped);
if (mapResult != VK_SUCCESS || mapped == nullptr) {
MGLOG_E("VkBufferObject::Upload failed: vmaMapMemory returned %d", mapResult);
const Bool wasMapped = IsMapped();
void* mapped = wasMapped ? m_mappedData : Map();
if (mapped == nullptr) {
MGLOG_E("VkBufferObject::Upload failed: unable to map buffer");
return false;
}
Memcpy(static_cast<Uint8*>(mapped) + offset, data, static_cast<SizeT>(size));
vmaUnmapMemory(m_allocator, m_allocation);
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;
}
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;
MOBILEGL_ASSERT(offset + resolvedSize <= m_size, "VkBufferObject::GetSlice range out of bounds");
BufferSlice slice{};
slice.buffer = m_buffer;
slice.offset = offset;
slice.size = resolvedSize;
slice.mapped = (m_mappedData != nullptr) ? static_cast<Uint8*>(m_mappedData) + offset : nullptr;
return slice;
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -8,11 +8,23 @@
#pragma once
#include "BufferSlice.h"
#include "../VkIncludes.h"
#include <Includes.h>
#include <vk_mem_alloc.h>
namespace MobileGL::MG_Backend::DirectVulkan {
struct VkBufferObjectDesc {
VmaAllocator allocator = nullptr;
VkDeviceSize size = 0;
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 {
public:
VkBufferObject() = default;
@@ -23,20 +35,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkBufferObject(VkBufferObject&& other) noexcept;
VkBufferObject& operator=(VkBufferObject&& other) noexcept;
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);
VkBuffer GetHandle() const { return m_buffer; }
VkDeviceSize GetSize() const { return m_size; }
BufferSlice GetSlice(VkDeviceSize offset = 0, VkDeviceSize size = VK_WHOLE_SIZE) const;
void* GetMappedData() const { return m_mappedData; }
Bool IsMapped() const { return m_mappedData != nullptr; }
Bool IsValid() const { return m_allocator != nullptr && m_buffer != VK_NULL_HANDLE && m_allocation != nullptr; }
private:
VmaAllocator m_allocator = nullptr;
VkBuffer m_buffer = VK_NULL_HANDLE;
VmaAllocation m_allocation = nullptr;
void* m_mappedData = nullptr;
VkDeviceSize m_size = 0;
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -0,0 +1,402 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/VkClearManager.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 "VkClearManager.h"
#include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h"
#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,
const MG_State::GLState::FramebufferObject& drawFbo) {
if (mask & GL_COLOR_BUFFER_BIT) {
auto& drawbufs = drawFbo.GetDrawBuffers();
// This should automatically work on default & offscreen FBO
for (auto drawbuf: drawbufs) {
const auto* attachment = GetClearableAttachment(drawFbo, drawbuf);
if (!attachment) {
continue;
}
QueueClear({
.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(),
attachment->GetTexture()->GetExternalIndex(),
clearPayload.color[0], clearPayload.color[1], clearPayload.color[2], clearPayload.color[3]);
}
}
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) {
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) {
if (clearPayload.mask == 0 || !texture) {
return;
}
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) {
if (texture == nullptr) {
return false;
}
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) {
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;
}
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);
}
}
if (expiredTextures.empty()) {
return 0;
}
for (const auto& identity : expiredTextures) {
ErasePendingClearsForTextureLocked(identity);
}
return expiredTextures.size();
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -0,0 +1,127 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/VkClearManager.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 "../VulkanRendererConfig.h"
#include "MG_State/GLState/FramebufferState/FramebufferObject.h"
#include "MG_Util/Math/VectorTypes.h"
#include <Includes.h>
#include <unordered_map>
namespace MobileGL::MG_Backend::DirectVulkan {
struct ClearFramebufferPayload {
FloatVec4 color;
Float depth{};
Uint32 stencil{};
};
struct ClearAttachmentPayload {
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();
void QueueClear(GLbitfield mask, const ClearFramebufferPayload& clearPayload, const MG_State::GLState::FramebufferObject& drawFbo);
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 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;
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
@@ -1,547 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/VkFramebufferManager.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 "VkFramebufferManager.h"
#include <MG_State/GLState/RenderbufferState/RenderbufferObject.h>
#include <MG_State/GLState/TextureState/TextureEnum.h>
namespace MobileGL::MG_Backend::DirectVulkan {
Bool VkFramebufferManager::Initialize(const InitInfo& initInfo) {
m_device = initInfo.device;
m_physicalDevice = initInfo.physicalDevice;
return m_device != VK_NULL_HANDLE && m_physicalDevice != VK_NULL_HANDLE;
}
void VkFramebufferManager::Shutdown() {
for (auto& [_, target] : m_offscreenColorTargets) {
DestroyOffscreenColorTarget(target);
}
m_offscreenColorTargets.clear();
m_device = VK_NULL_HANDLE;
m_physicalDevice = VK_NULL_HANDLE;
}
Bool VkFramebufferManager::EnsureOffscreenColorTarget(Uint glFboExternalIndex,
const MG_State::GLState::FramebufferObject& glFbo) {
const auto& colorAttachment = glFbo.GetAttachment(FramebufferAttachmentType::Color0);
if (!colorAttachment.IsValid() || colorAttachment.IsEmpty()) {
MGLOG_W("VkFramebufferManager: FBO %u has no valid COLOR0 attachment", glFboExternalIndex);
return false;
}
const auto objectVersion = glFbo.GetObjectVersion();
auto& target = m_offscreenColorTargets[glFboExternalIndex];
if (target.image != VK_NULL_HANDLE && target.glObjectVersion == objectVersion) {
return true;
}
return RecreateOffscreenColorTarget(target, glFbo, colorAttachment, objectVersion);
}
Bool VkFramebufferManager::TransitionOffscreenColorToAttachment(VkCommandBuffer commandBuffer,
Uint glFboExternalIndex) {
auto it = m_offscreenColorTargets.find(glFboExternalIndex);
if (it == m_offscreenColorTargets.end()) {
MGLOG_W("VkFramebufferManager::TransitionOffscreenColorToAttachment skipped: FBO %u not found",
glFboExternalIndex);
return false;
}
auto& target = it->second;
if (!TransitionImageLayout(commandBuffer, target.image, target.layout, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, 0,
VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
VK_IMAGE_ASPECT_COLOR_BIT)) {
return false;
}
if (target.depthStencilImage == VK_NULL_HANDLE) {
return true;
}
VkImageAspectFlags aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
if (target.depthStencilFormat == VK_FORMAT_D24_UNORM_S8_UINT ||
target.depthStencilFormat == VK_FORMAT_D32_SFLOAT_S8_UINT) {
aspectMask |= VK_IMAGE_ASPECT_STENCIL_BIT;
}
return TransitionImageLayout(
commandBuffer, target.depthStencilImage, target.depthStencilLayout,
VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT, 0,
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT, aspectMask);
}
Bool VkFramebufferManager::TransitionOffscreenColorToTransferSrc(VkCommandBuffer commandBuffer,
Uint glFboExternalIndex) {
auto it = m_offscreenColorTargets.find(glFboExternalIndex);
if (it == m_offscreenColorTargets.end()) {
MGLOG_W("VkFramebufferManager::TransitionOffscreenColorToTransferSrc skipped: FBO %u not found",
glFboExternalIndex);
return false;
}
auto& target = it->second;
return TransitionImageLayout(commandBuffer, target.image, target.layout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0,
VK_ACCESS_TRANSFER_READ_BIT, VK_IMAGE_ASPECT_COLOR_BIT);
}
Bool VkFramebufferManager::TransitionOffscreenColorToTransferDst(VkCommandBuffer commandBuffer,
Uint glFboExternalIndex) {
auto it = m_offscreenColorTargets.find(glFboExternalIndex);
if (it == m_offscreenColorTargets.end()) {
MGLOG_W("VkFramebufferManager::TransitionOffscreenColorToTransferDst skipped: FBO %u not found",
glFboExternalIndex);
return false;
}
auto& target = it->second;
return TransitionImageLayout(commandBuffer, target.image, target.layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0,
VK_ACCESS_TRANSFER_WRITE_BIT, VK_IMAGE_ASPECT_COLOR_BIT);
}
Bool VkFramebufferManager::TransitionOffscreenColorToGeneral(VkCommandBuffer commandBuffer,
Uint glFboExternalIndex) {
auto it = m_offscreenColorTargets.find(glFboExternalIndex);
if (it == m_offscreenColorTargets.end()) {
MGLOG_W("VkFramebufferManager::TransitionOffscreenColorToGeneral skipped: FBO %u not found",
glFboExternalIndex);
return false;
}
auto& target = it->second;
return TransitionImageLayout(commandBuffer, target.image, target.layout, VK_IMAGE_LAYOUT_GENERAL,
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0,
VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT,
VK_IMAGE_ASPECT_COLOR_BIT);
}
Bool VkFramebufferManager::TransitionOffscreenDepthStencilToTransferSrc(VkCommandBuffer commandBuffer,
Uint glFboExternalIndex) {
auto it = m_offscreenColorTargets.find(glFboExternalIndex);
if (it == m_offscreenColorTargets.end()) {
MGLOG_W("VkFramebufferManager::TransitionOffscreenDepthStencilToTransferSrc skipped: FBO %u not found",
glFboExternalIndex);
return false;
}
auto& target = it->second;
if (target.depthStencilImage == VK_NULL_HANDLE) {
return false;
}
VkImageAspectFlags aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
if (target.depthStencilFormat == VK_FORMAT_D24_UNORM_S8_UINT ||
target.depthStencilFormat == VK_FORMAT_D32_SFLOAT_S8_UINT) {
aspectMask |= VK_IMAGE_ASPECT_STENCIL_BIT;
}
return TransitionImageLayout(commandBuffer, target.depthStencilImage, target.depthStencilLayout,
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT, 0, VK_ACCESS_TRANSFER_READ_BIT, aspectMask);
}
Bool VkFramebufferManager::TransitionOffscreenDepthStencilToTransferDst(VkCommandBuffer commandBuffer,
Uint glFboExternalIndex) {
auto it = m_offscreenColorTargets.find(glFboExternalIndex);
if (it == m_offscreenColorTargets.end()) {
MGLOG_W("VkFramebufferManager::TransitionOffscreenDepthStencilToTransferDst skipped: FBO %u not found",
glFboExternalIndex);
return false;
}
auto& target = it->second;
if (target.depthStencilImage == VK_NULL_HANDLE) {
return false;
}
VkImageAspectFlags aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
if (target.depthStencilFormat == VK_FORMAT_D24_UNORM_S8_UINT ||
target.depthStencilFormat == VK_FORMAT_D32_SFLOAT_S8_UINT) {
aspectMask |= VK_IMAGE_ASPECT_STENCIL_BIT;
}
return TransitionImageLayout(commandBuffer, target.depthStencilImage, target.depthStencilLayout,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT, 0, VK_ACCESS_TRANSFER_WRITE_BIT, aspectMask);
}
Bool VkFramebufferManager::TransitionOffscreenDepthStencilToGeneral(VkCommandBuffer commandBuffer,
Uint glFboExternalIndex) {
auto it = m_offscreenColorTargets.find(glFboExternalIndex);
if (it == m_offscreenColorTargets.end()) {
MGLOG_W("VkFramebufferManager::TransitionOffscreenDepthStencilToGeneral skipped: FBO %u not found",
glFboExternalIndex);
return false;
}
auto& target = it->second;
if (target.depthStencilImage == VK_NULL_HANDLE) {
return false;
}
VkImageAspectFlags aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
if (target.depthStencilFormat == VK_FORMAT_D24_UNORM_S8_UINT ||
target.depthStencilFormat == VK_FORMAT_D32_SFLOAT_S8_UINT) {
aspectMask |= VK_IMAGE_ASPECT_STENCIL_BIT;
}
return TransitionImageLayout(commandBuffer, target.depthStencilImage, target.depthStencilLayout,
VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT, 0,
VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, aspectMask);
}
Bool VkFramebufferManager::TransitionOffscreenColorTextureToShaderRead(VkCommandBuffer commandBuffer,
Uint textureExternalIndex) {
for (auto& [_, target] : m_offscreenColorTargets) {
if (target.colorTextureExternalIndex != textureExternalIndex || target.image == VK_NULL_HANDLE) {
continue;
}
const Bool fromUndefined = (target.layout == VK_IMAGE_LAYOUT_UNDEFINED);
return TransitionImageLayout(
commandBuffer, target.image, target.layout, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
fromUndefined ? VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT
: (VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT),
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
fromUndefined ? 0 : (VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT),
VK_ACCESS_SHADER_READ_BIT, VK_IMAGE_ASPECT_COLOR_BIT);
}
return false;
}
Bool VkFramebufferManager::GetOffscreenColorImage(Uint glFboExternalIndex, VkImage& outImage,
VkExtent2D& outExtent) const {
auto it = m_offscreenColorTargets.find(glFboExternalIndex);
if (it == m_offscreenColorTargets.end() || it->second.image == VK_NULL_HANDLE) {
return false;
}
outImage = it->second.image;
outExtent = it->second.extent;
return true;
}
Bool VkFramebufferManager::GetOffscreenDepthStencilImage(Uint glFboExternalIndex, VkImage& outImage,
VkExtent2D& outExtent, VkFormat& outFormat) const {
auto it = m_offscreenColorTargets.find(glFboExternalIndex);
if (it == m_offscreenColorTargets.end() || it->second.depthStencilImage == VK_NULL_HANDLE) {
return false;
}
outImage = it->second.depthStencilImage;
outExtent = it->second.extent;
outFormat = it->second.depthStencilFormat;
return true;
}
Bool VkFramebufferManager::GetOffscreenColorViewByTexture(Uint textureExternalIndex,
VkImageView& outImageView) const {
for (const auto& [_, target] : m_offscreenColorTargets) {
if (target.colorTextureExternalIndex != textureExternalIndex || target.imageView == VK_NULL_HANDLE) {
continue;
}
outImageView = target.imageView;
return true;
}
return false;
}
Bool VkFramebufferManager::GetOffscreenRenderSurface(Uint glFboExternalIndex, VkImageView& outColorView,
VkFormat& outColorFormat, VkImageView& outDepthStencilView,
VkFormat& outDepthStencilFormat, VkExtent2D& outExtent) const {
auto it = m_offscreenColorTargets.find(glFboExternalIndex);
if (it == m_offscreenColorTargets.end() || it->second.imageView == VK_NULL_HANDLE) {
return false;
}
outColorView = it->second.imageView;
outColorFormat = it->second.format;
outDepthStencilView = it->second.depthStencilImageView;
outExtent = it->second.extent;
outDepthStencilFormat = it->second.depthStencilFormat;
return true;
}
Bool VkFramebufferManager::RecreateOffscreenColorTarget(
OffscreenColorTarget& target, const MG_State::GLState::FramebufferObject& glFbo,
const MG_State::GLState::FramebufferAttachmentObject& colorAttachment, Uint16 glObjectVersion) {
DestroyOffscreenColorTarget(target);
const auto size = colorAttachment.GetSize();
if (size.x() <= 0 || size.y() <= 0) {
MGLOG_W("VkFramebufferManager: COLOR0 attachment size is invalid (%d, %d)", size.x(), size.y());
return false;
}
const VkFormat format = ResolveColorFormat(colorAttachment);
if (format == VK_FORMAT_UNDEFINED) {
MGLOG_W("VkFramebufferManager: COLOR0 attachment format is unsupported for Vulkan clear");
return false;
}
VkImageCreateInfo imageInfo{};
imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
imageInfo.imageType = VK_IMAGE_TYPE_2D;
imageInfo.extent.width = static_cast<Uint32>(size.x());
imageInfo.extent.height = static_cast<Uint32>(size.y());
imageInfo.extent.depth = 1;
imageInfo.mipLevels = 1;
imageInfo.arrayLayers = 1;
imageInfo.format = format;
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
imageInfo.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT |
VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
VK_VERIFY(vkCreateImage(m_device, &imageInfo, nullptr, &target.image), "vkCreateImage(offscreen color)");
VkMemoryRequirements memoryRequirements{};
vkGetImageMemoryRequirements(m_device, target.image, &memoryRequirements);
VkMemoryAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
allocInfo.allocationSize = memoryRequirements.size;
allocInfo.memoryTypeIndex =
FindMemoryType(memoryRequirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
VK_VERIFY(vkAllocateMemory(m_device, &allocInfo, nullptr, &target.memory), "vkAllocateMemory(offscreen color)");
VK_VERIFY(vkBindImageMemory(m_device, target.image, target.memory, 0), "vkBindImageMemory(offscreen color)");
VkImageViewCreateInfo viewInfo{};
viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
viewInfo.image = target.image;
viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
viewInfo.format = format;
viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
viewInfo.subresourceRange.baseMipLevel = 0;
viewInfo.subresourceRange.levelCount = 1;
viewInfo.subresourceRange.baseArrayLayer = 0;
viewInfo.subresourceRange.layerCount = 1;
VK_VERIFY(vkCreateImageView(m_device, &viewInfo, nullptr, &target.imageView),
"vkCreateImageView(offscreen color)");
const auto& depthAttachment = glFbo.GetAttachment(FramebufferAttachmentType::Depth);
const auto& stencilAttachment = glFbo.GetAttachment(FramebufferAttachmentType::Stencil);
const Bool requestedDepthStencil = (depthAttachment.IsValid() && !depthAttachment.IsEmpty()) ||
(stencilAttachment.IsValid() && !stencilAttachment.IsEmpty());
VkFormat depthStencilFormat = ResolveDepthStencilFormat(depthAttachment, stencilAttachment);
if (depthStencilFormat == VK_FORMAT_D24_UNORM_S8_UINT) {
depthStencilFormat =
FindSupportedDepthStencilFormat({VK_FORMAT_D24_UNORM_S8_UINT, VK_FORMAT_D32_SFLOAT_S8_UINT});
} else if (depthStencilFormat == VK_FORMAT_D32_SFLOAT) {
depthStencilFormat = FindSupportedDepthStencilFormat({VK_FORMAT_D32_SFLOAT, VK_FORMAT_D16_UNORM});
}
const Bool hasDepthStencil = (depthStencilFormat != VK_FORMAT_UNDEFINED);
if (requestedDepthStencil && !hasDepthStencil) {
MGLOG_W("VkFramebufferManager: FBO %u depth/stencil attachment exists but format is unsupported",
glFbo.GetExternalIndex());
}
if (hasDepthStencil) {
VkImageCreateInfo depthImageInfo{};
depthImageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
depthImageInfo.imageType = VK_IMAGE_TYPE_2D;
depthImageInfo.extent.width = static_cast<Uint32>(size.x());
depthImageInfo.extent.height = static_cast<Uint32>(size.y());
depthImageInfo.extent.depth = 1;
depthImageInfo.mipLevels = 1;
depthImageInfo.arrayLayers = 1;
depthImageInfo.format = depthStencilFormat;
depthImageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
depthImageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
depthImageInfo.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;
depthImageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
depthImageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
VK_VERIFY(vkCreateImage(m_device, &depthImageInfo, nullptr, &target.depthStencilImage),
"vkCreateImage(offscreen depth/stencil)");
VkMemoryRequirements depthMemoryRequirements{};
vkGetImageMemoryRequirements(m_device, target.depthStencilImage, &depthMemoryRequirements);
VkMemoryAllocateInfo depthAllocInfo{};
depthAllocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
depthAllocInfo.allocationSize = depthMemoryRequirements.size;
depthAllocInfo.memoryTypeIndex =
FindMemoryType(depthMemoryRequirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
VK_VERIFY(vkAllocateMemory(m_device, &depthAllocInfo, nullptr, &target.depthStencilMemory),
"vkAllocateMemory(offscreen depth/stencil)");
VK_VERIFY(vkBindImageMemory(m_device, target.depthStencilImage, target.depthStencilMemory, 0),
"vkBindImageMemory(offscreen depth/stencil)");
VkImageAspectFlags depthAspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
if (depthStencilFormat == VK_FORMAT_D24_UNORM_S8_UINT ||
depthStencilFormat == VK_FORMAT_D32_SFLOAT_S8_UINT) {
depthAspectMask |= VK_IMAGE_ASPECT_STENCIL_BIT;
}
VkImageViewCreateInfo depthViewInfo{};
depthViewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
depthViewInfo.image = target.depthStencilImage;
depthViewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
depthViewInfo.format = depthStencilFormat;
depthViewInfo.subresourceRange.aspectMask = depthAspectMask;
depthViewInfo.subresourceRange.baseMipLevel = 0;
depthViewInfo.subresourceRange.levelCount = 1;
depthViewInfo.subresourceRange.baseArrayLayer = 0;
depthViewInfo.subresourceRange.layerCount = 1;
VK_VERIFY(vkCreateImageView(m_device, &depthViewInfo, nullptr, &target.depthStencilImageView),
"vkCreateImageView(offscreen depth/stencil)");
}
target.layout = VK_IMAGE_LAYOUT_UNDEFINED;
target.extent = {static_cast<Uint32>(size.x()), static_cast<Uint32>(size.y())};
target.format = format;
target.depthStencilLayout = VK_IMAGE_LAYOUT_UNDEFINED;
target.depthStencilFormat = depthStencilFormat;
target.glObjectVersion = glObjectVersion;
target.colorTextureExternalIndex = (colorAttachment.IsTexture() && colorAttachment.GetTexture())
? colorAttachment.GetTexture()->GetExternalIndex()
: 0;
return true;
}
void VkFramebufferManager::DestroyOffscreenColorTarget(OffscreenColorTarget& target) {
if (target.imageView != VK_NULL_HANDLE) {
vkDestroyImageView(m_device, target.imageView, nullptr);
target.imageView = VK_NULL_HANDLE;
}
if (target.depthStencilImageView != VK_NULL_HANDLE) {
vkDestroyImageView(m_device, target.depthStencilImageView, nullptr);
target.depthStencilImageView = VK_NULL_HANDLE;
}
if (target.image != VK_NULL_HANDLE) {
vkDestroyImage(m_device, target.image, nullptr);
target.image = VK_NULL_HANDLE;
}
if (target.depthStencilImage != VK_NULL_HANDLE) {
vkDestroyImage(m_device, target.depthStencilImage, nullptr);
target.depthStencilImage = VK_NULL_HANDLE;
}
if (target.memory != VK_NULL_HANDLE) {
vkFreeMemory(m_device, target.memory, nullptr);
target.memory = VK_NULL_HANDLE;
}
if (target.depthStencilMemory != VK_NULL_HANDLE) {
vkFreeMemory(m_device, target.depthStencilMemory, nullptr);
target.depthStencilMemory = VK_NULL_HANDLE;
}
target.layout = VK_IMAGE_LAYOUT_UNDEFINED;
target.depthStencilLayout = VK_IMAGE_LAYOUT_UNDEFINED;
target.extent = {0, 0};
target.format = VK_FORMAT_UNDEFINED;
target.depthStencilFormat = VK_FORMAT_UNDEFINED;
target.glObjectVersion = 0;
target.colorTextureExternalIndex = 0;
}
Bool VkFramebufferManager::TransitionImageLayout(VkCommandBuffer commandBuffer, VkImage image,
VkImageLayout& trackedLayout, VkImageLayout newLayout,
VkPipelineStageFlags srcStageMask,
VkPipelineStageFlags dstStageMask, VkAccessFlags srcAccessMask,
VkAccessFlags dstAccessMask, VkImageAspectFlags aspectMask) {
if (image == VK_NULL_HANDLE) {
return false;
}
if (trackedLayout == newLayout) {
return true;
}
VkImageMemoryBarrier barrier{};
barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
barrier.srcAccessMask = srcAccessMask;
barrier.dstAccessMask = dstAccessMask;
barrier.oldLayout = trackedLayout;
barrier.newLayout = newLayout;
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.image = image;
barrier.subresourceRange.aspectMask = aspectMask;
barrier.subresourceRange.baseMipLevel = 0;
barrier.subresourceRange.levelCount = 1;
barrier.subresourceRange.baseArrayLayer = 0;
barrier.subresourceRange.layerCount = 1;
vkCmdPipelineBarrier(commandBuffer, srcStageMask, dstStageMask, 0, 0, nullptr, 0, nullptr, 1, &barrier);
trackedLayout = newLayout;
return true;
}
Uint32 VkFramebufferManager::FindMemoryType(Uint32 typeFilter, VkMemoryPropertyFlags properties) const {
VkPhysicalDeviceMemoryProperties memoryProperties{};
vkGetPhysicalDeviceMemoryProperties(m_physicalDevice, &memoryProperties);
for (Uint32 i = 0; i < memoryProperties.memoryTypeCount; ++i) {
if ((typeFilter & (1U << i)) &&
(memoryProperties.memoryTypes[i].propertyFlags & properties) == properties) {
return i;
}
}
MOBILEGL_ASSERT(false, "VkFramebufferManager::FindMemoryType failed");
return 0;
}
VkFormat VkFramebufferManager::ResolveColorFormat(
const MG_State::GLState::FramebufferAttachmentObject& colorAttachment) {
TextureInternalFormat internalFormat = TextureInternalFormat::Unknown;
if (colorAttachment.IsTexture()) {
const auto texture = colorAttachment.GetTexture();
internalFormat = texture ? texture->GetFormat() : TextureInternalFormat::Unknown;
} else if (colorAttachment.IsRenderbuffer()) {
const auto renderbuffer = colorAttachment.GetRenderbuffer();
internalFormat = renderbuffer ? renderbuffer->GetInternalFormat() : TextureInternalFormat::Unknown;
}
switch (internalFormat) {
case TextureInternalFormat::RGBA:
case TextureInternalFormat::RGBA8:
case TextureInternalFormat::SRGB8Alpha8:
return VK_FORMAT_R8G8B8A8_UNORM;
default:
return VK_FORMAT_UNDEFINED;
}
}
VkFormat VkFramebufferManager::ResolveDepthStencilFormat(
const MG_State::GLState::FramebufferAttachmentObject& depthAttachment,
const MG_State::GLState::FramebufferAttachmentObject& stencilAttachment) {
const auto resolveAttachmentFormat = [](const MG_State::GLState::FramebufferAttachmentObject& attachment) {
TextureInternalFormat internalFormat = TextureInternalFormat::Unknown;
if (attachment.IsTexture()) {
const auto texture = attachment.GetTexture();
internalFormat = texture ? texture->GetFormat() : TextureInternalFormat::Unknown;
} else if (attachment.IsRenderbuffer()) {
const auto renderbuffer = attachment.GetRenderbuffer();
internalFormat = renderbuffer ? renderbuffer->GetInternalFormat() : TextureInternalFormat::Unknown;
}
return internalFormat;
};
const auto depthFormat = resolveAttachmentFormat(depthAttachment);
const auto stencilFormat = resolveAttachmentFormat(stencilAttachment);
switch (depthFormat) {
case TextureInternalFormat::Depth24Stencil8:
case TextureInternalFormat::Depth32FStencil8:
case TextureInternalFormat::DepthStencil:
return VK_FORMAT_D24_UNORM_S8_UINT;
case TextureInternalFormat::DepthComponent16:
return VK_FORMAT_D16_UNORM;
case TextureInternalFormat::DepthComponent24:
case TextureInternalFormat::DepthComponent32:
case TextureInternalFormat::DepthComponent32F:
case TextureInternalFormat::DepthComponent:
return VK_FORMAT_D32_SFLOAT;
default:
break;
}
switch (stencilFormat) {
case TextureInternalFormat::Depth24Stencil8:
case TextureInternalFormat::Depth32FStencil8:
case TextureInternalFormat::DepthStencil:
return VK_FORMAT_D24_UNORM_S8_UINT;
default:
return VK_FORMAT_UNDEFINED;
}
}
VkFormat VkFramebufferManager::FindSupportedDepthStencilFormat(const Vector<VkFormat>& candidates) const {
for (auto format : candidates) {
VkFormatProperties properties{};
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &properties);
if ((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT) != 0) {
return format;
}
}
return VK_FORMAT_UNDEFINED;
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -1,83 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/VkFramebufferManager.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>
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
namespace MobileGL::MG_Backend::DirectVulkan {
class VkFramebufferManager {
public:
struct InitInfo {
VkDevice device = VK_NULL_HANDLE;
VkPhysicalDevice physicalDevice = VK_NULL_HANDLE;
};
VkFramebufferManager() = default;
~VkFramebufferManager() = default;
Bool Initialize(const InitInfo& initInfo);
void Shutdown();
Bool EnsureOffscreenColorTarget(Uint glFboExternalIndex, const MG_State::GLState::FramebufferObject& glFbo);
Bool TransitionOffscreenColorToAttachment(VkCommandBuffer commandBuffer, Uint glFboExternalIndex);
Bool TransitionOffscreenColorToTransferSrc(VkCommandBuffer commandBuffer, Uint glFboExternalIndex);
Bool TransitionOffscreenColorToTransferDst(VkCommandBuffer commandBuffer, Uint glFboExternalIndex);
Bool TransitionOffscreenColorToGeneral(VkCommandBuffer commandBuffer, Uint glFboExternalIndex);
Bool TransitionOffscreenDepthStencilToTransferSrc(VkCommandBuffer commandBuffer, Uint glFboExternalIndex);
Bool TransitionOffscreenDepthStencilToTransferDst(VkCommandBuffer commandBuffer, Uint glFboExternalIndex);
Bool TransitionOffscreenDepthStencilToGeneral(VkCommandBuffer commandBuffer, Uint glFboExternalIndex);
Bool TransitionOffscreenColorTextureToShaderRead(VkCommandBuffer commandBuffer, Uint textureExternalIndex);
Bool GetOffscreenColorImage(Uint glFboExternalIndex, VkImage& outImage, VkExtent2D& outExtent) const;
Bool GetOffscreenDepthStencilImage(Uint glFboExternalIndex, VkImage& outImage, VkExtent2D& outExtent,
VkFormat& outFormat) const;
Bool GetOffscreenColorViewByTexture(Uint textureExternalIndex, VkImageView& outImageView) const;
Bool GetOffscreenRenderSurface(Uint glFboExternalIndex, VkImageView& outColorView, VkFormat& outColorFormat,
VkImageView& outDepthStencilView, VkFormat& outDepthStencilFormat,
VkExtent2D& outExtent) const;
private:
struct OffscreenColorTarget {
VkImage image = VK_NULL_HANDLE;
VkDeviceMemory memory = VK_NULL_HANDLE;
VkImageView imageView = VK_NULL_HANDLE;
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
VkExtent2D extent = {0, 0};
VkFormat format = VK_FORMAT_UNDEFINED;
VkImage depthStencilImage = VK_NULL_HANDLE;
VkDeviceMemory depthStencilMemory = VK_NULL_HANDLE;
VkImageView depthStencilImageView = VK_NULL_HANDLE;
VkImageLayout depthStencilLayout = VK_IMAGE_LAYOUT_UNDEFINED;
VkFormat depthStencilFormat = VK_FORMAT_UNDEFINED;
Uint16 glObjectVersion = 0;
Uint colorTextureExternalIndex = 0;
};
Bool RecreateOffscreenColorTarget(OffscreenColorTarget& target,
const MG_State::GLState::FramebufferObject& glFbo,
const MG_State::GLState::FramebufferAttachmentObject& colorAttachment,
Uint16 glObjectVersion);
void DestroyOffscreenColorTarget(OffscreenColorTarget& target);
Bool TransitionImageLayout(VkCommandBuffer commandBuffer, VkImage image, VkImageLayout& trackedLayout,
VkImageLayout newLayout, VkPipelineStageFlags srcStageMask,
VkPipelineStageFlags dstStageMask, VkAccessFlags srcAccessMask,
VkAccessFlags dstAccessMask, VkImageAspectFlags aspectMask);
Uint32 FindMemoryType(Uint32 typeFilter, VkMemoryPropertyFlags properties) const;
static VkFormat ResolveColorFormat(const MG_State::GLState::FramebufferAttachmentObject& colorAttachment);
static VkFormat ResolveDepthStencilFormat(
const MG_State::GLState::FramebufferAttachmentObject& depthAttachment,
const MG_State::GLState::FramebufferAttachmentObject& stencilAttachment);
VkFormat FindSupportedDepthStencilFormat(const Vector<VkFormat>& candidates) const;
VkDevice m_device = VK_NULL_HANDLE;
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
UnorderedMap<Uint, OffscreenColorTarget> m_offscreenColorTargets;
};
} // namespace MobileGL::MG_Backend::DirectVulkan
File diff suppressed because it is too large Load Diff
@@ -8,79 +8,233 @@
#pragma once
#include "SwapchainObject.h"
#include "VkClearManager.h"
#include "VkTextureManager.h"
#include "../VkIncludes.h"
#include "../VulkanRendererConfig.h"
#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 {
Uint32 attachmentIndex = 0;
PendingClearKey key{};
MG_State::GLState::RenderbufferObject* renderbuffer = nullptr;
Bool hasInlinePayload = false;
ClearAttachmentPayload inlinePayload{};
};
struct TrackedAttachmentLayoutInfo {
TrackedAttachmentTarget target = TrackedAttachmentTarget::Texture;
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;
Uint64 hash = 0;
VkRenderPass renderPass = VK_NULL_HANDLE;
VkFramebuffer framebuffer = VK_NULL_HANDLE;
Uint64 compatibilityHash = 0;
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;
RenderPassEntry() = default;
RenderPassEntry(const RenderPassEntry&) = delete;
RenderPassEntry(RenderPassEntry&& that) noexcept {
std::swap(hash, that.hash);
std::swap(renderPass, that.renderPass);
std::swap(framebuffer, that.framebuffer);
std::swap(compatibilityHash, that.compatibilityHash);
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);
}
RenderPassEntry(
Uint64 hash,
VkRenderPass renderpass,
VkFramebuffer framebuffer,
Uint64 compatibilityHash,
const Vector<PendingClearAttachmentInfo>& pendingClearAttachments,
const Vector<TrackedAttachmentLayoutInfo>& trackedAttachmentLayouts,
Uint32 attachmentCount,
Uint32 colorAttachmentCount,
Bool hasDepthStencilAttachment,
VkSampleCountFlagBits sampleCount,
IntVec2 extent, int subpass):
hash(hash),
renderPass(renderpass),
framebuffer(framebuffer),
compatibilityHash(compatibilityHash),
pendingClearAttachments(Move(pendingClearAttachments)),
trackedAttachmentLayouts(Move(trackedAttachmentLayouts)),
attachmentCount(attachmentCount),
colorAttachmentCount(colorAttachmentCount),
hasDepthStencilAttachment(hasDepthStencilAttachment),
sampleCount(sampleCount),
extent(extent),
subpass(subpass)
{}
~RenderPassEntry() {
if (renderPass != VK_NULL_HANDLE) {
vkDestroyRenderPass(s_device, renderPass, nullptr);
}
if (framebuffer != VK_NULL_HANDLE) {
vkDestroyFramebuffer(s_device, framebuffer, nullptr);
}
}
Bool CompatibleWith(const RenderPassEntry& that) const {
return this->compatibilityHash == that.compatibilityHash;
}
Bool CompatibleWith(Uint64 compatibilityHash) const {
return this->compatibilityHash == compatibilityHash;
}
};
struct ActiveRenderPassInfo {
Uint64 hash = 0;
Uint64 compatibilityHash = 0;
Vector<TrackedAttachmentLayoutInfo> trackedAttachmentLayouts;
IntVec2 extent = {0, 0};
Bool CompatibleWith(const RenderPassEntry& that) const {
return compatibilityHash == that.compatibilityHash;
}
Bool CompatibleWith(Uint64 thatCompatibilityHash) const {
return compatibilityHash == thatCompatibilityHash;
}
};
class VkRenderPassManager {
public:
struct InitInfo {
VkDevice device = VK_NULL_HANDLE;
VkFormat colorFormat = VK_FORMAT_UNDEFINED;
VkFormat depthStencilFormat = VK_FORMAT_UNDEFINED;
};
using HashType = Uint64;
VkRenderPassManager(VkDevice device,
VkPhysicalDevice physicalDevice, VmaAllocator allocator, const VulkanRendererConfig& config,
VkClearManager& clearManager, VkTextureManager& textureManager, SwapchainObject& swapchainObject);
~VkRenderPassManager();
struct OffscreenRenderTargetInfo {
Uint targetExternalIndex = 0;
Uint16 targetVersion = 0;
VkImageView colorView = VK_NULL_HANDLE;
VkFormat colorFormat = VK_FORMAT_UNDEFINED;
VkImageView depthStencilView = VK_NULL_HANDLE;
VkFormat depthStencilFormat = VK_FORMAT_UNDEFINED;
VkExtent2D extent = {0, 0};
};
Bool Initialize(const InitInfo& initInfo);
Bool Initialize();
void Shutdown();
Bool RecreateDefaultFramebuffers(const Vector<VkImageView>& colorViews,
const Vector<VkImageView>& depthStencilViews, VkExtent2D extent);
Bool GetDefaultRenderTarget(Uint32 imageIndex, VkRenderPass& outRenderPass, VkFramebuffer& outFramebuffer,
VkExtent2D& outExtent, VkFormat& outDepthStencilFormat) const;
Bool EnsureOffscreenRenderTarget(const OffscreenRenderTargetInfo& targetInfo);
Bool GetOffscreenRenderTarget(Uint targetExternalIndex, VkRenderPass& outRenderPass,
VkFramebuffer& outFramebuffer, VkExtent2D& outExtent,
VkFormat& outDepthStencilFormat) const;
void RemoveOffscreenRenderTarget(Uint targetExternalIndex);
void BeginRenderPass(VkCommandBuffer commandBuffer, VkRenderPass renderPass, VkFramebuffer framebuffer,
VkExtent2D extent) const;
void EndRenderPass(VkCommandBuffer commandBuffer) const;
void RecordColorClear(VkCommandBuffer commandBuffer, VkExtent2D extent,
const VkClearColorValue& clearColor) const;
void RecordDepthStencilClear(VkCommandBuffer commandBuffer, VkExtent2D extent, GLbitfield mask, Float depth,
Uint32 stencil, VkFormat depthStencilFormat) const;
VkRenderPass GetLoadRenderPass() const;
VkRenderPass GetClearRenderPass() const;
HashType ComputeHash(
const MG_State::GLState::FramebufferObject& fbo,
Uint32 swapchainImageIndex,
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);
static Bool BeginRenderPass(VkCommandBuffer commandBuffer, RenderPassEntry& renderPassEntry);
static Bool EndRenderPass(VkCommandBuffer commandBuffer);
static ActiveRenderPassInfo* GetActiveRenderPass();
private:
struct OffscreenRenderTarget {
Uint16 targetVersion = 0;
VkImageView colorView = VK_NULL_HANDLE;
VkFormat colorFormat = VK_FORMAT_UNDEFINED;
VkImageView depthStencilView = VK_NULL_HANDLE;
VkFormat depthStencilFormat = VK_FORMAT_UNDEFINED;
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;
// 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;
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};
VkRenderPass renderPassLoad = VK_NULL_HANDLE;
VkFramebuffer framebuffer = VK_NULL_HANDLE;
VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
TextureInternalFormat internalFormat = TextureInternalFormat::Unknown;
Int samples = 0;
void Destroy(VkDevice device, VmaAllocator allocator);
};
VkRenderPass CreateDefaultRenderPass(VkAttachmentLoadOp colorLoadOp) const;
VkRenderPass CreateRenderPass(VkFormat colorFormat, VkFormat depthStencilFormat, VkAttachmentLoadOp colorLoadOp,
VkImageLayout colorFinalLayout) const;
void DestroyDefaultFramebuffers();
void DestroyOffscreenRenderTarget(OffscreenRenderTarget& target);
static Bool HasStencilComponent(VkFormat format);
struct PendingRenderbufferClear {
WeakPtr<MG_State::GLState::RenderbufferObject> renderbuffer;
ClearAttachmentPayload payload{};
};
VkDevice m_device = VK_NULL_HANDLE;
VkFormat m_colorFormat = VK_FORMAT_UNDEFINED;
VkFormat m_depthStencilFormat = VK_FORMAT_UNDEFINED;
VkRenderPass m_renderPassLoad = VK_NULL_HANDLE;
VkRenderPass m_renderPassClear = VK_NULL_HANDLE;
Vector<VkFramebuffer> m_defaultFramebuffers;
VkExtent2D m_defaultExtent = {0, 0};
UnorderedMap<Uint, OffscreenRenderTarget> m_offscreenRenderTargets;
UnorderedMap<MG_State::GLState::RenderbufferObject*, RenderbufferResource> m_renderbufferResources;
UnorderedMap<MG_State::GLState::RenderbufferObject*, PendingRenderbufferClear> m_pendingRenderbufferClears;
RenderbufferResource* GetOrCreateRenderbufferResource(
const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer);
Bool GetPendingRenderbufferClear(MG_State::GLState::RenderbufferObject* renderbuffer,
ClearAttachmentPayload& outPayload) const;
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
@@ -0,0 +1,252 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.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 "VkSamplerManager.h"
#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;
MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE && m_config != nullptr,
"VkSamplerManager::Initialize failed: invalid initialization info");
return true;
}
void VkSamplerManager::Shutdown() {
for (auto& [_, sampler] : m_samplers) {
if (m_device != VK_NULL_HANDLE && sampler.handle != VK_NULL_HANDLE) {
vkDestroySampler(m_device, sampler.handle, nullptr);
}
sampler.handle = VK_NULL_HANDLE;
}
m_samplers.clear();
m_device = VK_NULL_HANDLE;
m_config = nullptr;
}
Uint64 VkSamplerManager::BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture) const {
MOBILEGL_ASSERT(m_config != nullptr, "VkSamplerManager::BuildSamplerKey: m_config is null");
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config->CacheVersion));
const auto minFilter = sampler.GetMinFilter();
XXHASH_VERIFY(XXH64_update(m_hashState, &minFilter, sizeof(minFilter)));
const auto magFilter = sampler.GetMagFilter();
XXHASH_VERIFY(XXH64_update(m_hashState, &magFilter, sizeof(magFilter)));
const auto mipmapMode = sampler.GetMipmapMode();
XXHASH_VERIFY(XXH64_update(m_hashState, &mipmapMode, sizeof(mipmapMode)));
const auto wrapS = sampler.GetWrapS();
XXHASH_VERIFY(XXH64_update(m_hashState, &wrapS, sizeof(wrapS)));
const auto wrapT = sampler.GetWrapT();
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 maxLod = ResolveEffectiveMaxLod(sampler);
const auto minLod = ResolveEffectiveMinLod(sampler, maxLod);
XXHASH_VERIFY(XXH64_update(m_hashState, &minLod, sizeof(minLod)));
XXHASH_VERIFY(XXH64_update(m_hashState, &maxLod, sizeof(maxLod)));
const auto lodBias = sampler.GetLodBias();
XXHASH_VERIFY(XXH64_update(m_hashState, &lodBias, sizeof(lodBias)));
// Anisotropy is currently an accepted frontend-only state on DirectVulkan.
// Keep it out of the key so changing this no-op does not manufacture duplicate
// VkSamplers while sampler versioning still exposes the new frontend value.
const auto compareMode = sampler.GetCompareMode();
XXHASH_VERIFY(XXH64_update(m_hashState, &compareMode, sizeof(compareMode)));
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 MG_State::GLState::ITextureObject& texture) {
const Uint64 key = BuildSamplerKey(sampler, texture);
auto it = m_samplers.find(key);
if (it != m_samplers.end()) {
return it->second.handle;
}
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.addressModeU = ToVkAddressMode(sampler.GetWrapS());
samplerInfo.addressModeV = ToVkAddressMode(sampler.GetWrapT());
samplerInfo.addressModeW = ToVkAddressMode(sampler.GetWrapR());
samplerInfo.mipLodBias = sampler.GetLodBias();
// DirectVulkan does not yet plumb samplerAnisotropy feature/limit discovery;
// preserve the accepted frontend state without requesting an unsupported feature.
samplerInfo.anisotropyEnable = VK_FALSE;
samplerInfo.maxAnisotropy = 1.0f;
samplerInfo.compareEnable = sampler.GetCompareMode() == SamplerCompareMode::CompareToTexture ? VK_TRUE : VK_FALSE;
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;
VK_VERIFY(vkCreateSampler(m_device, &samplerInfo, nullptr, &vkSampler), "vkCreateSampler(texture)");
SamplerCacheEntry entry{};
entry.handle = vkSampler;
entry.externalIndex = sampler.GetExternalIndex();
entry.version = sampler.GetVersion();
m_samplers[key] = entry;
return vkSampler;
}
VkFilter VkSamplerManager::ToVkFilter(SamplerFilterMode mode) {
return mode == SamplerFilterMode::Nearest ? VK_FILTER_NEAREST : VK_FILTER_LINEAR;
}
VkSamplerMipmapMode VkSamplerManager::ToVkMipmapMode(SamplerMipmapMode mode) {
switch (mode) {
case SamplerMipmapMode::Nearest:
return VK_SAMPLER_MIPMAP_MODE_NEAREST;
case SamplerMipmapMode::Linear:
return VK_SAMPLER_MIPMAP_MODE_LINEAR;
case SamplerMipmapMode::None:
default:
return VK_SAMPLER_MIPMAP_MODE_NEAREST;
}
}
VkSamplerAddressMode VkSamplerManager::ToVkAddressMode(SamplerWrapMode mode) {
switch (mode) {
case SamplerWrapMode::ClampToEdge:
return VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
case SamplerWrapMode::MirroredRepeat:
return VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT;
case SamplerWrapMode::Repeat:
return VK_SAMPLER_ADDRESS_MODE_REPEAT;
case SamplerWrapMode::ClampToBorder:
return VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER;
case SamplerWrapMode::MirrorClampToEdge:
return VK_SAMPLER_ADDRESS_MODE_MIRROR_CLAMP_TO_EDGE;
default:
return VK_SAMPLER_ADDRESS_MODE_REPEAT;
}
}
VkCompareOp VkSamplerManager::ToVkCompareOp(SamplerCompareFunc func) {
switch (func) {
case SamplerCompareFunc::Never:
return VK_COMPARE_OP_NEVER;
case SamplerCompareFunc::Less:
return VK_COMPARE_OP_LESS;
case SamplerCompareFunc::Equal:
return VK_COMPARE_OP_EQUAL;
case SamplerCompareFunc::LessEqual:
return VK_COMPARE_OP_LESS_OR_EQUAL;
case SamplerCompareFunc::Greater:
return VK_COMPARE_OP_GREATER;
case SamplerCompareFunc::NotEqual:
return VK_COMPARE_OP_NOT_EQUAL;
case SamplerCompareFunc::GreaterEqual:
return VK_COMPARE_OP_GREATER_OR_EQUAL;
case SamplerCompareFunc::Always:
default:
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
@@ -0,0 +1,58 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.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 "../VulkanRendererConfig.h"
#include <Includes.h>
#include <MG_State/GLState/SamplerState/SamplerObject.h>
namespace MobileGL::MG_State::GLState {
class SamplerObject;
class ITextureObject;
}
namespace MobileGL::MG_Backend::DirectVulkan {
class VkSamplerManager {
public:
struct InitInfo {
VkDevice device = VK_NULL_HANDLE;
const VulkanRendererConfig* config = nullptr;
};
Bool Initialize(const InitInfo& initInfo);
void Shutdown();
VkSampler GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture);
private:
struct SamplerCacheEntry {
VkSampler handle = VK_NULL_HANDLE;
Uint externalIndex = 0;
Uint16 version = 0;
};
Uint64 BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture) 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);
VkDevice m_device = VK_NULL_HANDLE;
const VulkanRendererConfig* m_config = nullptr;
UnorderedMap<Uint64, SamplerCacheEntry> m_samplers;
static inline XXH64_state_t* m_hashState = XXH64_createState();
};
} // namespace MobileGL::MG_Backend::DirectVulkan
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,304 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.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>
#include <MG_State/GLState/TextureState/TextureObject.h>
#include <vk_mem_alloc.h>
#include <unordered_map>
namespace MobileGL::MG_State::GLState {
class ITextureObject;
}
namespace MobileGL::MG_Backend::DirectVulkan {
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;
}
};
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;
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;
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;
TextureResource(TextureResource&& that) noexcept {
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->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->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);
}
}
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();
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;
syncedTextureParamsVersion = 0;
syncedContentVersion = 0;
syncedMipLevelCount = 0;
}
~TextureResource() {
Reset();
}
static inline VkDevice s_device = VK_NULL_HANDLE;
static inline VmaAllocator s_allocator = VK_NULL_HANDLE;
};
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);
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);
static VkImageAspectFlags ResolveSampledImageViewAspectMask(VkImageAspectFlags imageAspect);
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 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);
Bool SyncTextureResource(const MG_State::GLState::ITextureObject &texture,
TextureUploadTarget uploadTarget,
const IntVec3 &texelSize, SizeT byteSize, Uint32 mipLevels,
TextureResource &resource);
Bool SyncTextureViews(const MG_State::GLState::ITextureObject& texture, TextureResource& resource);
VkImageView CreateImageView(VkImage image, VkFormat format, VkImageAspectFlags aspect,
VkImageViewType viewType, Uint32 baseMipLevel, Uint32 levelCount,
Uint32 baseArrayLayer,
Uint32 layerCount,
const VkComponentMapping* components = nullptr) const;
Bool UploadDirtyMipLevels(MG_State::GLState::TextureObjectMipmap &mipmapTexture,
TextureUploadTarget uploadTarget,
TextureResource &outResource);
static Bool CheckMipmapCompleteness(const MG_State::GLState::ITextureObject& texture,
TextureUploadTarget& outTarget,
IntVec3& outTexelSize,
SizeT& outByteSize,
Uint32& outMipLevelCount);
static Uint32 GetUploadMipLevelCount(const MG_State::GLState::TextureObjectMipmap& texture, TextureUploadTarget target);
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;
// 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;
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
@@ -1,644 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureSamplerManager.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 "VkTextureSamplerManager.h"
#include "MG_State/GLState/Core.h"
namespace MobileGL::MG_Backend::DirectVulkan {
namespace {
constexpr Uint64 BuildSamplerKey(Uint externalIndex, Uint16 version) {
return (static_cast<Uint64>(externalIndex) << 16) | static_cast<Uint64>(version);
}
} // namespace
Bool VkTextureSamplerManager::Initialize(const InitInfo& initInfo) {
Shutdown();
m_device = initInfo.device;
m_physicalDevice = initInfo.physicalDevice;
m_commandPool = initInfo.commandPool;
m_graphicsQueue = initInfo.graphicsQueue;
if (m_device == VK_NULL_HANDLE || m_physicalDevice == VK_NULL_HANDLE || m_commandPool == VK_NULL_HANDLE ||
m_graphicsQueue == VK_NULL_HANDLE) {
MGLOG_E("VkTextureSamplerManager::Initialize failed: invalid Vulkan handles");
Shutdown();
return false;
}
if (!UploadFallbackTexture()) {
MGLOG_E("VkTextureSamplerManager::Initialize failed: fallback texture creation failed");
Shutdown();
return false;
}
return true;
}
void VkTextureSamplerManager::Shutdown() {
for (auto& [_, resource] : m_textureResources) {
DestroyTextureResource(resource);
}
m_textureResources.clear();
for (auto& [_, sampler] : m_samplers) {
if (m_device != VK_NULL_HANDLE && sampler.handle != VK_NULL_HANDLE) {
vkDestroySampler(m_device, sampler.handle, nullptr);
}
sampler.handle = VK_NULL_HANDLE;
}
m_samplers.clear();
if (m_device != VK_NULL_HANDLE && m_fallbackSampler != VK_NULL_HANDLE) {
vkDestroySampler(m_device, m_fallbackSampler, nullptr);
}
if (m_device != VK_NULL_HANDLE && m_fallbackImageView != VK_NULL_HANDLE) {
vkDestroyImageView(m_device, m_fallbackImageView, nullptr);
}
if (m_device != VK_NULL_HANDLE && m_fallbackImage != VK_NULL_HANDLE) {
vkDestroyImage(m_device, m_fallbackImage, nullptr);
}
if (m_device != VK_NULL_HANDLE && m_fallbackImageMemory != VK_NULL_HANDLE) {
vkFreeMemory(m_device, m_fallbackImageMemory, nullptr);
}
m_fallbackSampler = VK_NULL_HANDLE;
m_fallbackImageView = VK_NULL_HANDLE;
m_fallbackImage = VK_NULL_HANDLE;
m_fallbackImageMemory = VK_NULL_HANDLE;
m_device = VK_NULL_HANDLE;
m_physicalDevice = VK_NULL_HANDLE;
m_commandPool = VK_NULL_HANDLE;
m_graphicsQueue = VK_NULL_HANDLE;
}
Bool VkTextureSamplerManager::GetFallbackDescriptor(VkDescriptorImageInfo& outImageInfo) const {
if (m_fallbackSampler == VK_NULL_HANDLE || m_fallbackImageView == VK_NULL_HANDLE) {
return false;
}
outImageInfo.sampler = m_fallbackSampler;
outImageInfo.imageView = m_fallbackImageView;
outImageInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
return true;
}
Bool VkTextureSamplerManager::SyncTextureAndGetDescriptor(const MG_State::GLState::ITextureObject& texture,
const MG_State::GLState::SamplerObject* samplerOverride,
VkDescriptorImageInfo& outImageInfo) {
if (m_device == VK_NULL_HANDLE) {
return GetFallbackDescriptor(outImageInfo);
}
auto it = m_textureResources.find(texture.GetExternalIndex());
if (it == m_textureResources.end()) {
TextureResource initial{};
initial.textureExternalIndex = texture.GetExternalIndex();
auto [insertIt, _] = m_textureResources.emplace(texture.GetExternalIndex(), initial);
it = insertIt;
}
if (!EnsureTextureSynced(it->second, texture)) {
return GetFallbackDescriptor(outImageInfo);
}
const MG_State::GLState::SamplerObject* samplerToUse = samplerOverride;
if (!samplerToUse) {
auto textureSampler = texture.GetSamplerObject();
if (textureSampler) {
samplerToUse = textureSampler.get();
}
}
VkSampler sampler = m_fallbackSampler;
if (samplerToUse) {
sampler = GetOrCreateSampler(*samplerToUse);
}
if (sampler == VK_NULL_HANDLE) {
sampler = m_fallbackSampler;
}
if (it->second.view == VK_NULL_HANDLE || sampler == VK_NULL_HANDLE) {
return GetFallbackDescriptor(outImageInfo);
}
outImageInfo.sampler = sampler;
outImageInfo.imageView = it->second.view;
outImageInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
return true;
}
Bool VkTextureSamplerManager::EnsureTextureSynced(TextureResource& resource,
const MG_State::GLState::ITextureObject& texture) {
TextureUploadTarget level0Target = TextureUploadTarget::Unknown;
IntVec3 texelSize{0, 0, 0};
SizeT byteSize = 0;
if (!ResolveLevel0(texture, level0Target, texelSize, byteSize)) {
return false;
}
if (!EnsureTextureResource(resource, texture, level0Target, texelSize, byteSize)) {
return false;
}
const auto* mipTexture = dynamic_cast<const MG_State::GLState::TextureObjectMipmap*>(&texture);
if (!mipTexture) {
return false;
}
if (!mipTexture->IsStorageDirty(level0Target, 0)) {
return true;
}
if (!UploadLevel0(resource, *mipTexture, level0Target, byteSize)) {
return false;
}
auto& mutableTexture = const_cast<MG_State::GLState::TextureObjectMipmap&>(*mipTexture);
mutableTexture.MarkStorageDirty(level0Target, 0, false);
return true;
}
Bool VkTextureSamplerManager::EnsureTextureResource(TextureResource& resource,
const MG_State::GLState::ITextureObject& texture,
TextureUploadTarget level0Target, const IntVec3& texelSize,
SizeT byteSize) {
const VkFormat format = ResolveTextureFormat(texture.GetFormat());
if (format == VK_FORMAT_UNDEFINED) {
return false;
}
if (texelSize.x() <= 0 || texelSize.y() <= 0 || byteSize == 0) {
return false;
}
if (level0Target != TextureUploadTarget::Texture2D) {
return false;
}
const Bool compatible = resource.image != VK_NULL_HANDLE && resource.format == format &&
resource.extent.width == static_cast<Uint32>(texelSize.x()) &&
resource.extent.height == static_cast<Uint32>(texelSize.y());
if (compatible) {
return true;
}
DestroyTextureResource(resource);
VkImageCreateInfo imageInfo{};
imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
imageInfo.imageType = VK_IMAGE_TYPE_2D;
imageInfo.extent.width = static_cast<Uint32>(texelSize.x());
imageInfo.extent.height = static_cast<Uint32>(texelSize.y());
imageInfo.extent.depth = 1;
imageInfo.mipLevels = 1;
imageInfo.arrayLayers = 1;
imageInfo.format = format;
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
VK_VERIFY(vkCreateImage(m_device, &imageInfo, nullptr, &resource.image), "vkCreateImage(texture)");
VkMemoryRequirements requirements{};
vkGetImageMemoryRequirements(m_device, resource.image, &requirements);
VkMemoryAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
allocInfo.allocationSize = requirements.size;
allocInfo.memoryTypeIndex = FindMemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
VK_VERIFY(vkAllocateMemory(m_device, &allocInfo, nullptr, &resource.memory), "vkAllocateMemory(texture)");
VK_VERIFY(vkBindImageMemory(m_device, resource.image, resource.memory, 0), "vkBindImageMemory(texture)");
VkImageViewCreateInfo viewInfo{};
viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
viewInfo.image = resource.image;
viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
viewInfo.format = format;
viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
viewInfo.subresourceRange.baseMipLevel = 0;
viewInfo.subresourceRange.levelCount = 1;
viewInfo.subresourceRange.baseArrayLayer = 0;
viewInfo.subresourceRange.layerCount = 1;
VK_VERIFY(vkCreateImageView(m_device, &viewInfo, nullptr, &resource.view), "vkCreateImageView(texture)");
resource.layout = VK_IMAGE_LAYOUT_UNDEFINED;
resource.extent = {static_cast<Uint32>(texelSize.x()), static_cast<Uint32>(texelSize.y())};
resource.format = format;
resource.textureExternalIndex = texture.GetExternalIndex();
return true;
}
Bool VkTextureSamplerManager::UploadLevel0(TextureResource& resource,
const MG_State::GLState::TextureObjectMipmap& mipmapTexture,
TextureUploadTarget level0Target, SizeT byteSize) {
auto& mutableTexture = const_cast<MG_State::GLState::TextureObjectMipmap&>(mipmapTexture);
const void* source = mutableTexture.MapMipmapData(level0Target, 0);
if (source == nullptr || byteSize == 0) {
return false;
}
VkBuffer stagingBuffer = VK_NULL_HANDLE;
VkDeviceMemory stagingMemory = VK_NULL_HANDLE;
VkBufferCreateInfo bufferInfo{};
bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
bufferInfo.size = byteSize;
bufferInfo.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
VK_VERIFY(vkCreateBuffer(m_device, &bufferInfo, nullptr, &stagingBuffer), "vkCreateBuffer(staging texture)");
VkMemoryRequirements requirements{};
vkGetBufferMemoryRequirements(m_device, stagingBuffer, &requirements);
VkMemoryAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
allocInfo.allocationSize = requirements.size;
allocInfo.memoryTypeIndex =
FindMemoryType(requirements.memoryTypeBits,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
VK_VERIFY(vkAllocateMemory(m_device, &allocInfo, nullptr, &stagingMemory), "vkAllocateMemory(staging texture)");
VK_VERIFY(vkBindBufferMemory(m_device, stagingBuffer, stagingMemory, 0), "vkBindBufferMemory(staging texture)");
void* mapped = nullptr;
VK_VERIFY(vkMapMemory(m_device, stagingMemory, 0, byteSize, 0, &mapped), "vkMapMemory(staging texture)");
std::memcpy(mapped, source, byteSize);
vkUnmapMemory(m_device, stagingMemory);
const Bool ok = ExecuteImmediate([&](VkCommandBuffer commandBuffer) {
VkImageMemoryBarrier toTransferDst{};
toTransferDst.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
toTransferDst.srcAccessMask = 0;
toTransferDst.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
toTransferDst.oldLayout = resource.layout;
toTransferDst.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
toTransferDst.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
toTransferDst.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
toTransferDst.image = resource.image;
toTransferDst.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
toTransferDst.subresourceRange.baseMipLevel = 0;
toTransferDst.subresourceRange.levelCount = 1;
toTransferDst.subresourceRange.baseArrayLayer = 0;
toTransferDst.subresourceRange.layerCount = 1;
vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0,
nullptr, 0, nullptr, 1, &toTransferDst);
VkBufferImageCopy copy{};
copy.bufferOffset = 0;
copy.bufferRowLength = 0;
copy.bufferImageHeight = 0;
copy.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
copy.imageSubresource.mipLevel = 0;
copy.imageSubresource.baseArrayLayer = 0;
copy.imageSubresource.layerCount = 1;
copy.imageOffset = {0, 0, 0};
copy.imageExtent = {resource.extent.width, resource.extent.height, 1};
vkCmdCopyBufferToImage(commandBuffer, stagingBuffer, resource.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1,
&copy);
VkImageMemoryBarrier toSampled{};
toSampled.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
toSampled.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
toSampled.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
toSampled.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
toSampled.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
toSampled.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
toSampled.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
toSampled.image = resource.image;
toSampled.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
toSampled.subresourceRange.baseMipLevel = 0;
toSampled.subresourceRange.levelCount = 1;
toSampled.subresourceRange.baseArrayLayer = 0;
toSampled.subresourceRange.layerCount = 1;
vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0,
0, nullptr, 0, nullptr, 1, &toSampled);
});
vkDestroyBuffer(m_device, stagingBuffer, nullptr);
vkFreeMemory(m_device, stagingMemory, nullptr);
if (!ok) {
return false;
}
resource.layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
return true;
}
Bool VkTextureSamplerManager::ExecuteImmediate(const std::function<void(VkCommandBuffer)>& recorder) const {
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 commandBuffer = VK_NULL_HANDLE;
VK_VERIFY(vkAllocateCommandBuffers(m_device, &allocInfo, &commandBuffer), "vkAllocateCommandBuffers(texture)");
VkCommandBufferBeginInfo beginInfo{};
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
VK_VERIFY(vkBeginCommandBuffer(commandBuffer, &beginInfo), "vkBeginCommandBuffer(texture)");
recorder(commandBuffer);
VK_VERIFY(vkEndCommandBuffer(commandBuffer), "vkEndCommandBuffer(texture)");
VkSubmitInfo submitInfo{};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &commandBuffer;
VK_VERIFY(vkQueueSubmit(m_graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE), "vkQueueSubmit(texture)");
VK_VERIFY(vkQueueWaitIdle(m_graphicsQueue), "vkQueueWaitIdle(texture)");
vkFreeCommandBuffers(m_device, m_commandPool, 1, &commandBuffer);
return true;
}
void VkTextureSamplerManager::DestroyTextureResource(TextureResource& resource) const {
if (m_device != VK_NULL_HANDLE && resource.view != VK_NULL_HANDLE) {
vkDestroyImageView(m_device, resource.view, nullptr);
}
if (m_device != VK_NULL_HANDLE && resource.image != VK_NULL_HANDLE) {
vkDestroyImage(m_device, resource.image, nullptr);
}
if (m_device != VK_NULL_HANDLE && resource.memory != VK_NULL_HANDLE) {
vkFreeMemory(m_device, resource.memory, nullptr);
}
resource.view = VK_NULL_HANDLE;
resource.image = VK_NULL_HANDLE;
resource.memory = VK_NULL_HANDLE;
resource.layout = VK_IMAGE_LAYOUT_UNDEFINED;
resource.extent = {0, 0};
resource.format = VK_FORMAT_UNDEFINED;
}
Bool VkTextureSamplerManager::ResolveLevel0(const MG_State::GLState::ITextureObject& texture,
TextureUploadTarget& outTarget, IntVec3& outTexelSize,
SizeT& outByteSize) {
const auto* mipTexture = dynamic_cast<const MG_State::GLState::TextureObjectMipmap*>(&texture);
if (!mipTexture) {
return false;
}
const auto& targets = texture.GetUploadTargets();
if (targets.empty()) {
return false;
}
outTarget = targets.front();
outTexelSize = mipTexture->GetMipmapTexelSize(outTarget, 0);
outByteSize = mipTexture->GetMipmapByteSize(outTarget, 0);
return outTexelSize.x() > 0 && outTexelSize.y() > 0 && outByteSize > 0;
}
VkFormat VkTextureSamplerManager::ResolveTextureFormat(TextureInternalFormat format) {
switch (format) {
case TextureInternalFormat::RGBA:
case TextureInternalFormat::RGBA8:
return VK_FORMAT_R8G8B8A8_UNORM;
case TextureInternalFormat::SRGB8Alpha8:
return VK_FORMAT_R8G8B8A8_SRGB;
default:
return VK_FORMAT_UNDEFINED;
}
}
Uint32 VkTextureSamplerManager::FindMemoryType(Uint32 typeFilter, VkMemoryPropertyFlags properties) const {
VkPhysicalDeviceMemoryProperties memProperties{};
vkGetPhysicalDeviceMemoryProperties(m_physicalDevice, &memProperties);
for (Uint32 i = 0; i < memProperties.memoryTypeCount; ++i) {
if ((typeFilter & (1u << i)) != 0 &&
(memProperties.memoryTypes[i].propertyFlags & properties) == properties) {
return i;
}
}
MOBILEGL_ASSERT(false, "VkTextureSamplerManager::FindMemoryType failed");
return 0;
}
VkSampler VkTextureSamplerManager::GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler) {
const Uint64 key = BuildSamplerKey(sampler.GetExternalIndex(), sampler.GetVersion());
auto it = m_samplers.find(key);
if (it != m_samplers.end()) {
return it->second.handle;
}
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.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;
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.unnormalizedCoordinates = VK_FALSE;
VkSampler vkSampler = VK_NULL_HANDLE;
VK_VERIFY(vkCreateSampler(m_device, &samplerInfo, nullptr, &vkSampler), "vkCreateSampler(texture)");
SamplerCacheEntry entry{};
entry.handle = vkSampler;
entry.externalIndex = sampler.GetExternalIndex();
entry.version = sampler.GetVersion();
m_samplers[key] = entry;
return vkSampler;
}
VkFilter VkTextureSamplerManager::ToVkFilter(SamplerFilterMode mode) {
return mode == SamplerFilterMode::Nearest ? VK_FILTER_NEAREST : VK_FILTER_LINEAR;
}
VkSamplerMipmapMode VkTextureSamplerManager::ToVkMipmapMode(SamplerMipmapMode mode) {
switch (mode) {
case SamplerMipmapMode::Nearest:
return VK_SAMPLER_MIPMAP_MODE_NEAREST;
case SamplerMipmapMode::Linear:
return VK_SAMPLER_MIPMAP_MODE_LINEAR;
case SamplerMipmapMode::None:
default:
return VK_SAMPLER_MIPMAP_MODE_NEAREST;
}
}
VkSamplerAddressMode VkTextureSamplerManager::ToVkAddressMode(SamplerWrapMode mode) {
switch (mode) {
case SamplerWrapMode::ClampToEdge:
return VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
case SamplerWrapMode::MirroredRepeat:
return VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT;
case SamplerWrapMode::Repeat:
return VK_SAMPLER_ADDRESS_MODE_REPEAT;
case SamplerWrapMode::ClampToBorder:
return VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER;
case SamplerWrapMode::MirrorClampToEdge:
return VK_SAMPLER_ADDRESS_MODE_MIRROR_CLAMP_TO_EDGE;
default:
return VK_SAMPLER_ADDRESS_MODE_REPEAT;
}
}
VkCompareOp VkTextureSamplerManager::ToVkCompareOp(SamplerCompareFunc func) {
switch (func) {
case SamplerCompareFunc::Never:
return VK_COMPARE_OP_NEVER;
case SamplerCompareFunc::Less:
return VK_COMPARE_OP_LESS;
case SamplerCompareFunc::Equal:
return VK_COMPARE_OP_EQUAL;
case SamplerCompareFunc::LessEqual:
return VK_COMPARE_OP_LESS_OR_EQUAL;
case SamplerCompareFunc::Greater:
return VK_COMPARE_OP_GREATER;
case SamplerCompareFunc::NotEqual:
return VK_COMPARE_OP_NOT_EQUAL;
case SamplerCompareFunc::GreaterEqual:
return VK_COMPARE_OP_GREATER_OR_EQUAL;
case SamplerCompareFunc::Always:
default:
return VK_COMPARE_OP_ALWAYS;
}
}
Bool VkTextureSamplerManager::UploadFallbackTexture() {
const Uint32 rgba = 0xFFFFFFFFu;
VkImageCreateInfo imageInfo{};
imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
imageInfo.imageType = VK_IMAGE_TYPE_2D;
imageInfo.extent = {1, 1, 1};
imageInfo.mipLevels = 1;
imageInfo.arrayLayers = 1;
imageInfo.format = VK_FORMAT_R8G8B8A8_UNORM;
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
VK_VERIFY(vkCreateImage(m_device, &imageInfo, nullptr, &m_fallbackImage), "vkCreateImage(fallback)");
VkMemoryRequirements imageMemReq{};
vkGetImageMemoryRequirements(m_device, m_fallbackImage, &imageMemReq);
VkMemoryAllocateInfo imageAllocInfo{};
imageAllocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
imageAllocInfo.allocationSize = imageMemReq.size;
imageAllocInfo.memoryTypeIndex = FindMemoryType(imageMemReq.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
VK_VERIFY(vkAllocateMemory(m_device, &imageAllocInfo, nullptr, &m_fallbackImageMemory),
"vkAllocateMemory(fallback)");
VK_VERIFY(vkBindImageMemory(m_device, m_fallbackImage, m_fallbackImageMemory, 0), "vkBindImageMemory(fallback)");
VkBuffer stagingBuffer = VK_NULL_HANDLE;
VkDeviceMemory stagingMemory = VK_NULL_HANDLE;
VkBufferCreateInfo bufferInfo{};
bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
bufferInfo.size = sizeof(rgba);
bufferInfo.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
VK_VERIFY(vkCreateBuffer(m_device, &bufferInfo, nullptr, &stagingBuffer), "vkCreateBuffer(fallback)");
VkMemoryRequirements stagingMemReq{};
vkGetBufferMemoryRequirements(m_device, stagingBuffer, &stagingMemReq);
VkMemoryAllocateInfo stagingAllocInfo{};
stagingAllocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
stagingAllocInfo.allocationSize = stagingMemReq.size;
stagingAllocInfo.memoryTypeIndex =
FindMemoryType(stagingMemReq.memoryTypeBits,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
VK_VERIFY(vkAllocateMemory(m_device, &stagingAllocInfo, nullptr, &stagingMemory), "vkAllocateMemory(fallback)");
VK_VERIFY(vkBindBufferMemory(m_device, stagingBuffer, stagingMemory, 0), "vkBindBufferMemory(fallback)");
void* mapped = nullptr;
VK_VERIFY(vkMapMemory(m_device, stagingMemory, 0, sizeof(rgba), 0, &mapped), "vkMapMemory(fallback)");
std::memcpy(mapped, &rgba, sizeof(rgba));
vkUnmapMemory(m_device, stagingMemory);
const Bool uploadOk = ExecuteImmediate([&](VkCommandBuffer commandBuffer) {
VkImageMemoryBarrier toTransferDst{};
toTransferDst.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
toTransferDst.srcAccessMask = 0;
toTransferDst.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
toTransferDst.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
toTransferDst.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
toTransferDst.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
toTransferDst.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
toTransferDst.image = m_fallbackImage;
toTransferDst.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
toTransferDst.subresourceRange.baseMipLevel = 0;
toTransferDst.subresourceRange.levelCount = 1;
toTransferDst.subresourceRange.baseArrayLayer = 0;
toTransferDst.subresourceRange.layerCount = 1;
vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0,
nullptr, 0, nullptr, 1, &toTransferDst);
VkBufferImageCopy copy{};
copy.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
copy.imageSubresource.mipLevel = 0;
copy.imageSubresource.baseArrayLayer = 0;
copy.imageSubresource.layerCount = 1;
copy.imageExtent = {1, 1, 1};
vkCmdCopyBufferToImage(commandBuffer, stagingBuffer, m_fallbackImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1,
&copy);
VkImageMemoryBarrier toSampled{};
toSampled.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
toSampled.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
toSampled.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
toSampled.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
toSampled.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
toSampled.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
toSampled.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
toSampled.image = m_fallbackImage;
toSampled.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
toSampled.subresourceRange.baseMipLevel = 0;
toSampled.subresourceRange.levelCount = 1;
toSampled.subresourceRange.baseArrayLayer = 0;
toSampled.subresourceRange.layerCount = 1;
vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0,
0, nullptr, 0, nullptr, 1, &toSampled);
});
vkDestroyBuffer(m_device, stagingBuffer, nullptr);
vkFreeMemory(m_device, stagingMemory, nullptr);
if (!uploadOk) {
return false;
}
VkImageViewCreateInfo viewInfo{};
viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
viewInfo.image = m_fallbackImage;
viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
viewInfo.format = VK_FORMAT_R8G8B8A8_UNORM;
viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
viewInfo.subresourceRange.baseMipLevel = 0;
viewInfo.subresourceRange.levelCount = 1;
viewInfo.subresourceRange.baseArrayLayer = 0;
viewInfo.subresourceRange.layerCount = 1;
VK_VERIFY(vkCreateImageView(m_device, &viewInfo, nullptr, &m_fallbackImageView), "vkCreateImageView(fallback)");
VkSamplerCreateInfo samplerInfo{};
samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
samplerInfo.magFilter = VK_FILTER_NEAREST;
samplerInfo.minFilter = VK_FILTER_NEAREST;
samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
samplerInfo.compareEnable = VK_FALSE;
samplerInfo.minLod = 0.0f;
samplerInfo.maxLod = 0.0f;
samplerInfo.maxAnisotropy = 1.0f;
VK_VERIFY(vkCreateSampler(m_device, &samplerInfo, nullptr, &m_fallbackSampler), "vkCreateSampler(fallback)");
return true;
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -1,88 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureSamplerManager.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>
#include <MG_State/GLState/SamplerState/SamplerObject.h>
#include <MG_State/GLState/TextureState/TextureObject.h>
namespace MobileGL::MG_State::GLState {
class ITextureObject;
class SamplerObject;
}
namespace MobileGL::MG_Backend::DirectVulkan {
class VkTextureSamplerManager {
public:
struct InitInfo {
VkDevice device = VK_NULL_HANDLE;
VkPhysicalDevice physicalDevice = VK_NULL_HANDLE;
VkCommandPool commandPool = VK_NULL_HANDLE;
VkQueue graphicsQueue = VK_NULL_HANDLE;
};
Bool Initialize(const InitInfo& initInfo);
void Shutdown();
Bool GetFallbackDescriptor(VkDescriptorImageInfo& outImageInfo) const;
Bool SyncTextureAndGetDescriptor(const MG_State::GLState::ITextureObject& texture,
const MG_State::GLState::SamplerObject* samplerOverride,
VkDescriptorImageInfo& outImageInfo);
private:
struct TextureResource {
VkImage image = VK_NULL_HANDLE;
VkDeviceMemory memory = VK_NULL_HANDLE;
VkImageView view = VK_NULL_HANDLE;
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
VkExtent2D extent = {0, 0};
VkFormat format = VK_FORMAT_UNDEFINED;
Uint textureExternalIndex = 0;
};
struct SamplerCacheEntry {
VkSampler handle = VK_NULL_HANDLE;
Uint externalIndex = 0;
Uint16 version = 0;
};
Bool EnsureTextureSynced(TextureResource& resource, const MG_State::GLState::ITextureObject& texture);
Bool EnsureTextureResource(TextureResource& resource, const MG_State::GLState::ITextureObject& texture,
TextureUploadTarget level0Target, const IntVec3& texelSize, SizeT byteSize);
Bool UploadLevel0(TextureResource& resource, const MG_State::GLState::TextureObjectMipmap& mipmapTexture,
TextureUploadTarget level0Target, SizeT byteSize);
Bool ExecuteImmediate(const std::function<void(VkCommandBuffer)>& recorder) const;
void DestroyTextureResource(TextureResource& resource) const;
static Bool ResolveLevel0(const MG_State::GLState::ITextureObject& texture, TextureUploadTarget& outTarget,
IntVec3& outTexelSize, SizeT& outByteSize);
static VkFormat ResolveTextureFormat(TextureInternalFormat format);
Uint32 FindMemoryType(Uint32 typeFilter, VkMemoryPropertyFlags properties) const;
VkSampler GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler);
static VkFilter ToVkFilter(SamplerFilterMode mode);
static VkSamplerMipmapMode ToVkMipmapMode(SamplerMipmapMode mode);
static VkSamplerAddressMode ToVkAddressMode(SamplerWrapMode mode);
static VkCompareOp ToVkCompareOp(SamplerCompareFunc func);
Bool UploadFallbackTexture();
VkDevice m_device = VK_NULL_HANDLE;
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
VkCommandPool m_commandPool = VK_NULL_HANDLE;
VkQueue m_graphicsQueue = VK_NULL_HANDLE;
UnorderedMap<Uint, TextureResource> m_textureResources;
UnorderedMap<Uint64, SamplerCacheEntry> m_samplers;
VkImage m_fallbackImage = VK_NULL_HANDLE;
VkDeviceMemory m_fallbackImageMemory = VK_NULL_HANDLE;
VkImageView m_fallbackImageView = VK_NULL_HANDLE;
VkSampler m_fallbackSampler = VK_NULL_HANDLE;
};
} // 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
@@ -12,12 +12,15 @@
#include "PipelineFactory.h"
#include "ProgramFactory.h"
#include "SwapchainObject.h"
#include "UniformDescriptorBinder.h"
#include "UniformManager.h"
#include "VertexInputStateFactory.h"
#include "VkBufferObject.h"
#include "VkFramebufferManager.h"
#include "VkBufferManager.h"
#include "VkClearManager.h"
#include "VkRenderPassManager.h"
#include "VkTextureSamplerManager.h"
#include "VkSamplerManager.h"
#include "VkTextureManager.h"
#include "VkTimerQueryManager.h"
#include "MG_Util/Math/VectorTypes.h"
#include <Includes.h>
#include <vk_mem_alloc.h>
@@ -27,23 +30,67 @@
namespace MobileGL::MG_State::GLState {
class FramebufferObject;
class ProgramObject;
class SamplerObject;
class VertexArrayObject;
} // namespace MobileGL::MG_State::GLState
namespace MobileGL::MG_Backend::DirectVulkan {
struct DrawArrayPayload {
GLenum mode = GL_TRIANGLES;
GLint first = 0;
GLsizei count = 0;
const MG_State::GLState::ProgramObject* program = nullptr;
const MG_State::GLState::VertexArrayObject* vertexArray = nullptr;
enum class DrawSetupAspect: Uint8 {
FramebufferObject = 1 << 0,
VertexArrayObject = 1 << 1,
UniformBuffer = 1 << 2,
VertexBuffer = 1 << 3,
IndexBuffer = 1 << 4,
IndirectDrawBuffer = 1 << 5,
Viewport = 1 << 6,
Scissor = 1 << 7,
};
struct DrawElementPayload {
DrawArrayPayload drawArray;
struct DrawCmdParam {
Uint32 vertexCount = 0;
Uint32 instanceCount = 1;
Uint32 firstVertex = 0;
Uint32 firstInstance = 0;
};
struct DrawIndexedCmdParam {
Uint32 indexCount = 0;
Uint32 instanceCount = 1;
Uint32 firstIndex = 0;
Int32 vertexOffset = 0;
Int32 firstInstance = 0;
};
struct DrawCmd {
GLenum mode = GL_TRIANGLES;
DrawCmdParam params;
};
struct IndexBufferView {
GLenum indexType = GL_UNSIGNED_SHORT;
SizeT indexByteOffset = 0;
GLint baseVertex = 0;
SizeT indexByteSize = 0;
};
struct DrawIndexedCmd {
GLenum mode = GL_TRIANGLES;
IndexBufferView indexBufferView;
DrawIndexedCmdParam params;
};
struct MultiDrawIndexedCmd {
GLenum mode = GL_TRIANGLES;
IndexBufferView indexBufferView;
Uint32 drawCount = 0;
DrawIndexedCmdParam* pParams = nullptr;
};
struct MultiDrawCmd {
GLenum mode = GL_TRIANGLES;
Uint32 drawCount = 0;
DrawCmdParam* pParams = nullptr;
};
struct QueueFamilyIndices {
@@ -61,7 +108,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
};
class VulkanRenderer {
class VulkanRenderer : public IBufferCopyCommandProvider, public FrameContext::IRecordingObserver {
public:
VulkanRenderer(NativeWindowType window, const VulkanRendererConfig& cfg = {});
~VulkanRenderer();
@@ -69,36 +116,219 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void Initialize();
void Shutdown();
void RequestClear(GLbitfield mask, const FloatVec4& color, Float depth, Uint32 stencil,
Uint drawFboExternalIndex, Bool isDefaultFramebufferTarget);
Bool ConsumePendingColorClear(VkClearColorValue& outClearColor);
void EnsureFrameRecordingStarted();
void DrawArrays(const DrawArrayPayload& payload);
void DrawElements(const DrawElementPayload& payload);
void MultiDrawElements(const Vector<DrawElementPayload>& payloads);
Bool BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1,
GLint dstY1, GLbitfield mask, GLenum filter, Uint readFboExternalIndex,
Uint drawFboExternalIndex, Bool readIsDefaultFramebuffer, Bool drawIsDefaultFramebuffer);
void Render();
// 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);
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);
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;
// 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;
// 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);
void RecreateSwapchain();
private:
struct PendingClearState {
GLbitfield mask = 0;
VkClearColorValue color = {{0.0f, 0.0f, 0.0f, 1.0f}};
Float depth = 1.0f;
Uint32 stencil = 0;
Uint drawFboExternalIndex = 0;
Bool targetsDefaultFramebuffer = true;
struct BlitUniformData {
float srcRect[4] = {0.f, 0.f, 1.f, 1.f};
float dstRect[4] = {0.f, 0.f, 1.f, 1.f};
Int surfaceTransform = 0;
Int padding[3] = {0, 0, 0};
};
struct BlitResources {
SharedPtr<MG_State::GLState::ProgramObject> program;
SharedPtr<MG_State::GLState::SamplerObject> nearestSampler;
SharedPtr<MG_State::GLState::SamplerObject> linearSampler;
Int srcRectLocation = -1;
Int dstRectLocation = -1;
Int surfaceTransformLocation = -1;
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);
// ---- 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;
// Vulkan objects
Bool m_validationLayersEnabled = false;
@@ -106,56 +336,94 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkInstance m_instance = VK_NULL_HANDLE;
VkDebugUtilsMessengerEXT m_debugMessenger = VK_NULL_HANDLE;
PhysicalDevice m_physicalDevice;
// VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
VkDevice m_device = VK_NULL_HANDLE;
VmaAllocator m_allocator = nullptr;
VkSurfaceKHR m_surface = VK_NULL_HANDLE;
SwapchainObject m_swapchainObject;
// Vector<VkQueueFamilyProperties> m_queueFamilies;
// QueueFamilyIndices m_queueFamilyIndices;
VkQueue m_graphicsQueue = VK_NULL_HANDLE;
VkQueue m_presentQueue = VK_NULL_HANDLE;
Bool m_drawIndirectCountExtensionEnabled = false;
Bool m_indexTypeUint8ExtensionEnabled = false;
Bool m_logicOpFeatureEnabled = false;
Bool m_multiDrawIndirectFeatureEnabled = false;
Bool m_shaderDrawParametersExtensionEnabled = false;
Bool m_shaderDrawParametersFeatureEnabled = 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,
Uint32 stride);
PFNDrawIndexedIndirectCountFunc m_cmdDrawIndexedIndirectCount = nullptr;
static inline PFNDrawIndexedIndirectCountFunc s_vkCmdDrawIndexedIndirectCount = nullptr;
VkCommandPool m_commandPool = VK_NULL_HANDLE;
VkFormat m_depthStencilFormat = VK_FORMAT_UNDEFINED;
Vector<VkImage> m_depthStencilImages;
Vector<VkDeviceMemory> m_depthStencilImageMemories;
Vector<VkImageView> m_depthStencilImageViews;
Vector<VkImageLayout> m_depthStencilImageLayouts;
VkPipelineLayout m_pipelineLayout = VK_NULL_HANDLE;
Vector<VkBufferObject> m_frameVertexUploadBuffers;
Vector<VkDeviceSize> m_frameVertexUploadHeads;
Vector<VkBufferObject> m_frameIndexUploadBuffers;
Vector<VkDeviceSize> m_frameIndexUploadHeads;
Vector<Vector<VkBufferObject>> m_deferredBufferReleases;
VkBufferManager m_bufferManager;
Uint m_imageIndexAcquired = 0;
FrameContext m_frameContext;
UnorderedMap<Uint64, PendingClearState> m_pendingClears;
Bool m_isMainRenderPassActive = false;
VkRenderPass m_activeRenderPass = VK_NULL_HANDLE;
VkExtent2D m_activeRenderExtent = {0, 0};
VkFormat m_activeDepthStencilFormat = VK_FORMAT_UNDEFINED;
Bool m_activeRenderTargetIsDefault = true;
Uint m_activeDrawFboExternalIndex = 0;
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<UniformDescriptorBinder> m_uniformDescriptorBinder;
UniquePtr<UniformManager> m_uniformManager;
UniquePtr<VertexInputStateFactory> m_vertexInputStateFactory;
UniquePtr<VkFramebufferManager> m_framebufferManager;
UniquePtr<VkClearManager> m_clearManager;
UniquePtr<VkRenderPassManager> m_renderPassManager;
UniquePtr<VkTextureSamplerManager> m_textureSamplerManager;
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<VkBuffer> m_vertexBuffersScratch;
Vector<VkDeviceSize> m_vertexOffsetsScratch;
Vector<VkVertexInputAttributeDescription> m_patchedAttributesScratch;
void CreateInstance();
VkResult SetupDebugMessenger();
@@ -168,31 +436,51 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void DestroyAllocator();
void CreateSwapchain();
void CreateCommandPool();
void CreateFrameContexts();
void CreateDepthStencilResources();
void DestroyDepthStencilResources();
VkRenderPass GetDefaultLoadRenderPass() const;
Bool GetDefaultRenderTargetForCurrentImage(VkRenderPass& outRenderPass, VkFramebuffer& outFramebuffer,
VkExtent2D& outExtent, VkFormat& outDepthStencilFormat) const;
Bool EnsureOffscreenRenderTarget(Uint glFboExternalIndex, const MG_State::GLState::FramebufferObject& glFbo,
VkRenderPass& outRenderPass, VkFramebuffer& outFramebuffer,
VkExtent2D& outExtent, VkFormat& outDepthStencilFormat);
void PrepareDemoPipeline();
VkPipeline GetOrCreatePipeline(const MG_State::GLState::ProgramObject& program, VkPipelineLayout pipelineLayout,
Uint64 vertexInputHash,
const VkPipelineVertexInputStateCreateInfo& vertexInputState);
void TransitionSwapchainImageToColorAttachment(VkCommandBuffer commandBuffer, Uint32 imageIndex);
void TransitionDepthStencilImageToAttachment(VkCommandBuffer commandBuffer, Uint32 imageIndex);
void EndFrameRecordingIfNeeded();
void DeferDestroyBuffer(VkBufferObject& buffer);
void CollectDeferredBufferReleases(Uint32 frameIndex);
Bool EnsureFrameUploadBufferCapacity(Uint32 frameIndex, Bool isIndexBuffer, VkDeviceSize requiredEndOffset,
VkDeviceSize minCapacity, VkBufferUsageFlags usage);
Bool UploadAndBindVertexStreams(const VertexInputStateFactory::BackendVertexInputState& vertexInputState,
const DrawArrayPayload& payload, VkCommandBuffer commandBuffer);
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 UploadAndBindVertexBuffers(VkCommandBuffer commandBuffer, const MG_State::GLState::VertexArrayObject& vao,
const ProgramFactory::VkProgramObject& programObj,
const DrawCmdParam& drawParams);
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,
GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
GLenum filter);
Bool MaterializePendingClearForTexture(VkCommandBuffer commandBuffer,
MG_State::GLState::ITextureObject& texture);
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();
// Static functions
static Int GetPresentQueueFamilyIndex(const PhysicalDevice& physicalDevice, VkSurfaceKHR surface,
const Vector<VkQueueFamilyProperties>& queueFamilies,
Int preferredFamilyIndex = -1);
@@ -212,11 +500,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
static Bool GetMoreCapablePhysicalDevice(VkPhysicalDevice newVkDevice, VkSurfaceKHR surface,
const PhysicalDevice& compareWithDevice,
PhysicalDevice& outBetterDevice);
Uint32 FindMemoryType(Uint32 typeFilter, VkMemoryPropertyFlags properties) const;
static Uint64 BuildPendingClearKey(Uint drawFboExternalIndex, Bool targetsDefaultFramebuffer);
static Bool HasStencilComponent(VkFormat format);
static VkFormat FindSupportedDepthStencilFormat(VkPhysicalDevice physicalDevice);
static constexpr VkDynamicState s_dynamicStates[] = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR};
static constexpr const char* s_validationLayerNames[] = {"VK_LAYER_KHRONOS_validation"};
static constexpr const char* s_deviceExtensionNames[] = {VK_KHR_SWAPCHAIN_EXTENSION_NAME};
static Bool CheckValidationLayerSupport();
+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";
Version Version = MG_Config::CoreVersion;
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
} // namespace MobileGL::MG_Backend::DirectVulkan
+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
+175 -24
View File
@@ -10,6 +10,8 @@
#include "../GetProcAddress.h"
#include <MG_Backend/BackendObjects.h>
#include <MG_State/EGLState/Core.h>
#include <mutex>
#include <sstream>
#include <type_traits>
namespace MobileGL::MG_Impl::EGLImpl {
@@ -20,7 +22,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
if (!MG_State::pEGLContext) {
MGLOG_E("pEGLContext is null. MG_State may not be initialized.");
}
return MG_State::pEGLContext;
return MG_State::pEGLContext.get();
}
MG_Backend::BackendObject* GetBackendObject(EGLStateContext* state) {
@@ -31,14 +33,53 @@ 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(__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 +118,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 +162,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;
}
@@ -172,6 +224,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 +234,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 +268,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 +288,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 +307,13 @@ 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();
}
return EGL_TRUE;
}
EGLBoolean ReleaseThread() {
@@ -238,6 +321,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;
}
@@ -310,7 +396,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 +415,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 +433,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) {
@@ -547,22 +667,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,
+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
+41 -20
View File
@@ -9,24 +9,45 @@
#pragma once
#include <Includes.h>
namespace MobileGL {
namespace MG_Impl::GLImpl {
/* @INSERTION_POINT:FUNCTION_DECLARATION@ */
void GetBufferParameteriv(GLenum target, GLenum pname, GLint* 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 CopyBufferSubData(GLenum readTarget, GLenum writeTarget, GLintptr readOffset, GLintptr writeOffset,
GLsizeiptr size);
void BufferSubData(GLenum target, GLintptr offset, GLsizeiptr size, const 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);
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 MG_Impl::GLImpl
} // namespace MobileGL
} // namespace MobileGL::MG_Impl::GLImpl
+108 -92
View File
@@ -7,111 +7,127 @@
// 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>
#include <MG_Util/Converters/MGToGL/BufferEnumConverter.h>
#include <MG_Util/Converters/MGToStr/BufferEnumConverter.h>
namespace MobileGL::MG_Impl::GLImpl {
namespace BufferImpl {
Bool ValidateBufferTarget(BufferTarget target) {
if (target == BufferTarget::Unknown) {
using namespace MG_Util;
String bufferTargetStr = ConvertBufferTargetToString(target);
String glTargetStr = ConvertGLEnumToString(ConvertBufferTargetToGLEnum(target));
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeShared<GenericErrorInfo>(
"MG_Impl/GLImpl/BufferImpl", "ValidateBufferTarget",
std::format("Target {} ({}) is not valid.", bufferTargetStr, glTargetStr)));
return false;
}
if (target == BufferTarget::Index && MG_State::pGLContext->GetBoundVertexArray() == nullptr) {
MG_State::pGLContext->RecordError(ErrorCode::InvalidOperation,
MakeShared<GenericErrorInfo>("MG_Impl/GLImpl/BufferImpl",
"ValidateBufferTarget",
"No vertex array object is bound."));
return false;
}
return true;
}
Bool ValidateBufferBindingPointTarget(BufferTarget target) {
if (target != BufferTarget::Uniform && target != BufferTarget::AtomicCounter &&
target != BufferTarget::TransformFeedback && target != BufferTarget::ShaderStorage) {
using namespace MG_Util;
String bufferTargetStr = ConvertBufferTargetToString(target);
String glTargetStr = ConvertGLEnumToString(ConvertBufferTargetToGLEnum(target));
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeShared<GenericErrorInfo>(
"MG_Impl/GLImpl/BufferImpl", "ValidateBufferTarget",
std::format("Target {} ({}) is not valid.", bufferTargetStr, glTargetStr)));
return false;
}
return true;
}
Bool ValidateBufferName(Uint index, Bool allowZero) {
if (index == 0) {
if (allowZero) return true;
MG_State::pGLContext->RecordError(ErrorCode::InvalidValue,
MakeShared<GenericErrorInfo>("MG_Impl/GLImpl/BufferImpl",
"ValidateBufferName",
"Buffer name 0 is not valid."));
return false;
}
Bool isValid = MG_State::pGLContext->ValidateBufferName(index);
if (isValid) return true;
namespace MobileGL::MG_Impl::GLImpl::BufferImpl {
Bool ValidateBufferTarget(BufferTarget target) {
if (target == BufferTarget::Unknown) {
using namespace MG_Util;
String bufferTargetStr = ConvertBufferTargetToString(target);
String glTargetStr = ConvertGLEnumToString(ConvertBufferTargetToGLEnum(target));
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeShared<GenericErrorInfo>("MG_Impl/GLImpl/BufferImpl", "ValidateBufferName",
std::format("Buffer name {} is not valid.", index)));
ErrorCode::InvalidEnum, MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl/BufferImpl", "ValidateBufferTarget",
std::format("Target {} ({}) is not valid.", bufferTargetStr, glTargetStr)));
return false;
}
Bool ValidateBufferUsage(BufferUsage usage) {
if (usage != BufferUsage::Unknown) {
return true;
}
if (target == BufferTarget::Index && MG_State::pGLContext->GetBoundVertexArray() == nullptr) {
MG_State::pGLContext->RecordError(ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl/BufferImpl",
"ValidateBufferTarget",
"No vertex array object is bound."));
return false;
}
return true;
}
Bool ValidateBufferBindingPointTarget(BufferTarget target) {
if (target != BufferTarget::Uniform && target != BufferTarget::AtomicCounter &&
target != BufferTarget::TransformFeedback && target != BufferTarget::ShaderStorage) {
using namespace MG_Util;
String bufferUsageStr = ConvertBufferUsageToString(usage);
String glUsageStr = ConvertGLEnumToString(ConvertBufferUsageToGLEnum(usage));
String bufferTargetStr = ConvertBufferTargetToString(target);
String glTargetStr = ConvertGLEnumToString(ConvertBufferTargetToGLEnum(target));
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum, MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl/BufferImpl", "ValidateBufferTarget",
std::format("Target {} ({}) is not valid.", bufferTargetStr, glTargetStr)));
return false;
}
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;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl/BufferImpl", "ValidateBufferName",
"Buffer name 0 is not valid."));
return false;
}
Bool isValid = MG_State::pGLContext->ValidateBufferName(index);
if (isValid) return true;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl/BufferImpl", "ValidateBufferName",
std::format("Buffer name {} is not valid.", index)));
return false;
}
Bool ValidateBufferUsage(BufferUsage usage) {
if (usage != BufferUsage::Unknown) {
return true;
}
using namespace MG_Util;
String bufferUsageStr = ConvertBufferUsageToString(usage);
String glUsageStr = ConvertGLEnumToString(ConvertBufferUsageToGLEnum(usage));
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl/BufferImpl", "ValidateBufferUsage",
std::format("Usage {} ({}) is not one of the allowable values.", bufferUsageStr, glUsageStr)));
return false;
}
Bool ValidateBufferMappingAccess(Flags<BufferMappingAccessBit> accessBits) {
if (accessBits == BufferMappingAccessBit::Null) {
MG_State::pGLContext->RecordError(ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl/BufferImpl",
"ValidateBufferMappingAccess",
"Access bits cannot be null."));
return false;
}
const auto validBits = BufferMappingAccessBit::Read | BufferMappingAccessBit::Write |
BufferMappingAccessBit::InvalidateRange | BufferMappingAccessBit::InvalidateBuffer |
BufferMappingAccessBit::FlushExplicit | BufferMappingAccessBit::Unsynchronized |
BufferMappingAccessBit::Persistent | BufferMappingAccessBit::Coherent;
if ((accessBits & validBits) != accessBits) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeShared<GenericErrorInfo>(
"MG_Impl/GLImpl/BufferImpl", "ValidateBufferUsage",
std::format("Usage {} ({}) is not one of the allowable values.", bufferUsageStr, glUsageStr)));
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl/BufferImpl", "ValidateBufferMappingAccess",
"Access bits cannot contain invalid flags."));
return false;
}
Bool ValidateBufferMappingAccess(Flags<BufferMappingAccessBit> accessBits) {
if (accessBits == BufferMappingAccessBit::Null) {
MG_State::pGLContext->RecordError(ErrorCode::InvalidEnum,
MakeShared<GenericErrorInfo>("MG_Impl/GLImpl/BufferImpl",
"ValidateBufferMappingAccess",
"Access bits cannot be null."));
return false;
}
const auto validBits = BufferMappingAccessBit::Read | BufferMappingAccessBit::Write |
BufferMappingAccessBit::InvalidateRange | BufferMappingAccessBit::InvalidateBuffer |
BufferMappingAccessBit::FlushExplicit | BufferMappingAccessBit::Unsynchronized |
BufferMappingAccessBit::Persistent | BufferMappingAccessBit::Coherent;
if ((accessBits & validBits) != accessBits) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeShared<GenericErrorInfo>("MG_Impl/GLImpl/BufferImpl", "ValidateBufferMappingAccess",
"Access bits cannot contain invalid flags."));
return false;
}
return true;
}
} // namespace BufferImpl
} // namespace MobileGL::MG_Impl::GLImpl
return true;
}
} // namespace MobileGL::MG_Impl::GLImpl::BufferImpl
+8 -9
View File
@@ -10,12 +10,11 @@
#include <Includes.h>
#include <MG_State/GLState/BufferState/BufferObject.h>
namespace MobileGL::MG_Impl::GLImpl {
namespace BufferImpl {
Bool ValidateBufferTarget(BufferTarget target);
Bool ValidateBufferName(Uint index, Bool allowZero = false);
Bool ValidateBufferUsage(BufferUsage usage);
Bool ValidateBufferMappingAccess(Flags<BufferMappingAccessBit> accessBits);
Bool ValidateBufferBindingPointTarget(BufferTarget target);
} // namespace BufferImpl
} // namespace MobileGL::MG_Impl::GLImpl
namespace MobileGL::MG_Impl::GLImpl::BufferImpl {
Bool ValidateBufferTarget(BufferTarget target);
Bool ValidateBufferName(Uint index, Bool allowZero = false);
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 (MG_State::pEGLContext->IsCurrentContextOpenGLCoreProfile() && vao && vao->GetExternalIndex() == 0) {
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);
}
+37 -31
View File
@@ -9,34 +9,40 @@
#pragma once
#include <Includes.h>
namespace MobileGL {
namespace MG_Impl::GLImpl {
/* @INSERTION_POINT:FUNCTION_DECLARATION@ */
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 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);
void DrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
GLsizei instancecount, GLint basevertex, GLuint baseinstance);
void DrawElementsInstancedBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices,
GLsizei instancecount, GLint basevertex);
void DrawElementsInstancedBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
GLsizei instancecount, GLuint baseinstance);
void DrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount);
void DrawElementsIndirect(GLenum mode, GLenum type, const void* indirect);
void DrawArraysInstancedBaseInstance(GLenum mode, GLint first, GLsizei count, GLsizei instancecount,
GLuint baseinstance);
void DrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei instancecount);
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 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,
GLsizei drawcount, const GLint* basevertex);
void Clear(GLbitfield mask);
void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices);
} // namespace MG_Impl::GLImpl
} // namespace MobileGL
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);
void DrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
GLsizei instancecount, GLint basevertex, GLuint baseinstance);
void DrawElementsInstancedBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices,
GLsizei instancecount, GLint basevertex);
void DrawElementsInstancedBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
GLsizei instancecount, GLuint baseinstance);
void DrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount);
void DrawElementsIndirect(GLenum mode, GLenum type, const void* indirect);
void DrawArraysInstancedBaseInstance(GLenum mode, GLint first, GLsizei count, GLsizei instancecount,
GLuint baseinstance);
void DrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei instancecount);
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,
GLsizei drawcount, const GLint* basevertex);
void Clear(GLbitfield mask);
void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices);
} // namespace MobileGL::MG_Impl::GLImpl
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -11,51 +11,73 @@
#include <Includes.h>
#include <MG_State/GLState/Core.h>
namespace MobileGL {
namespace MG_Impl::GLImpl {
/* @INSERTION_POINT:FUNCTION_DECLARATION@ */
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels);
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 RenderbufferStorageMultisample(GLenum target, GLsizei samples, GLenum internalformat, GLsizei width,
GLsizei height);
void RenderbufferStorage(GLenum target, GLenum internalformat, GLsizei width, GLsizei height);
GLboolean IsRenderbuffer(GLuint renderbuffer);
void GetRenderbufferParameteriv(GLenum target, GLenum pname, GLint* params);
void GenRenderbuffers(GLsizei n, GLuint* renderbuffers);
void FramebufferRenderbuffer(GLenum target, 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 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 DrawBuffer(GLenum buf);
void DrawBuffers(GLsizei n, const GLenum* bufs);
void ReadBuffer(GLenum src);
void DeleteFramebuffers(GLsizei n, const GLuint* framebuffers);
GLenum CheckFramebufferStatus(GLenum target);
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1,
GLint dstY1, GLbitfield mask, GLenum filter);
void BindFramebuffer(GLenum target, GLuint framebuffer);
namespace MobileGL::MG_Impl::GLImpl {
/* @INSERTION_POINT:FUNCTION_DECLARATION@ */
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels);
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 RenderbufferStorageMultisample(GLenum target, GLsizei samples, GLenum internalformat, GLsizei width,
GLsizei height);
void RenderbufferStorage(GLenum target, GLenum internalformat, GLsizei width, GLsizei height);
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);
void DeleteFramebuffers(GLsizei n, const GLuint* framebuffers);
GLenum CheckFramebufferStatus(GLenum target);
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1,
GLint dstY1, GLbitfield mask, GLenum filter);
void BindFramebuffer(GLenum target, GLuint framebuffer);
namespace FramebufferImpl {
struct DefaultFramebufferInfo {
SharedPtr<MG_State::GLState::FramebufferObject> defaultFBO;
SharedPtr<MG_State::GLState::ITextureObject> colorAttachment;
SharedPtr<MG_State::GLState::ITextureObject> depthAttachment;
SharedPtr<MG_State::GLState::ITextureObject> stencilAttachment;
};
namespace FramebufferImpl {
struct DefaultFramebufferInfo {
SharedPtr<MG_State::GLState::FramebufferObject> defaultFBO;
SharedPtr<MG_State::GLState::ITextureObject> colorAttachment;
SharedPtr<MG_State::GLState::ITextureObject> depthAttachment;
SharedPtr<MG_State::GLState::ITextureObject> stencilAttachment;
};
extern DefaultFramebufferInfo* pDefaultFramebufferInfo;
} // namespace FramebufferImpl
} // namespace MG_Impl::GLImpl
} // namespace MobileGL
extern UniquePtr<DefaultFramebufferInfo> pDefaultFramebufferInfo;
} // namespace FramebufferImpl
} // namespace MobileGL::MG_Impl::GLImpl
@@ -13,85 +13,82 @@
#include <MG_Util/Converters/MGToGL/FramebufferEnumConverter.h>
#include <MG_Util/Converters/MGToStr/FramebufferEnumConverter.h>
namespace MobileGL::MG_Impl::GLImpl {
namespace FramebufferImpl {
Bool ValidateFramebufferTarget(FramebufferTarget target) {
if (target == FramebufferTarget::Unknown) {
using namespace MG_Util;
String bufferTargetStr = ConvertFramebufferTargetToString(target);
String glTargetStr = ConvertGLEnumToString(ConvertFramebufferTargetToGLEnum(target));
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeShared<GenericErrorInfo>(
"MG_Impl/GLImpl/FramebufferImpl", "ValidateFramebufferTarget",
std::format("Target {} ({}) is not valid.", bufferTargetStr, glTargetStr)));
return false;
}
return true;
}
Bool ValidateFramebufferName(Uint index, Bool allowZero) {
if (index == 0 && !allowZero) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeShared<GenericErrorInfo>("MG_Impl/GLImpl/FramebufferImpl", "ValidateFramebufferName",
"Framebuffer name 0 is not valid in this situation."));
return false;
}
Bool isValid = MG_State::pGLContext->ValidateFramebufferName(index);
if (isValid) return true;
namespace MobileGL::MG_Impl::GLImpl::FramebufferImpl {
Bool ValidateFramebufferTarget(FramebufferTarget target) {
if (target == FramebufferTarget::Unknown) {
using namespace MG_Util;
String bufferTargetStr = ConvertFramebufferTargetToString(target);
String glTargetStr = ConvertGLEnumToString(ConvertFramebufferTargetToGLEnum(target));
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeShared<GenericErrorInfo>("MG_Impl/GLImpl/FramebufferImpl", "ValidateFramebufferName",
std::format("Framebuffer name {} is not valid.", index)));
ErrorCode::InvalidEnum, MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl/FramebufferImpl", "ValidateFramebufferTarget",
std::format("Target {} ({}) is not valid.", bufferTargetStr, glTargetStr)));
return false;
}
return true;
}
Bool ValidateFramebufferAttachmentType(FramebufferAttachmentType attachment) {
if (attachment == FramebufferAttachmentType::Unknown) {
using namespace MG_Util;
String attachmentStr = ConvertFramebufferAttachmentTypeToString(attachment);
String glAttachmentStr = ConvertGLEnumToString(ConvertFramebufferAttachmentTypeToGLEnum(attachment));
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeShared<GenericErrorInfo>(
"MG_Impl/GLImpl/FramebufferImpl", "ValidateFramebufferAttachmentType",
std::format("Attachment type {} ({}) is not valid.", attachmentStr, glAttachmentStr)));
return false;
}
return true;
}
Bool ValidateRenderbufferTarget(RenderbufferTarget target) {
if (target == RenderbufferTarget::Unknown) {
using namespace MG_Util;
String renderbufferTargetStr = ConvertRenderbufferTargetToString(target);
String glTargetStr = ConvertGLEnumToString(ConvertRenderbufferTargetToGLEnum(target));
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeShared<GenericErrorInfo>(
"MG_Impl/GLImpl/FramebufferImpl", "ValidateRenderbufferTarget",
std::format("Target {} ({}) is not valid.", renderbufferTargetStr, glTargetStr)));
return false;
}
return true;
}
Bool ValidateRenderbufferName(Uint index, Bool allowZero) {
if (index == 0 && !allowZero) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeShared<GenericErrorInfo>("MG_Impl/GLImpl/FramebufferImpl", "ValidateRenderbufferName",
"Renderbuffer name 0 is not valid in this situation."));
return false;
}
Bool isValid = MG_State::pGLContext->ValidateRenderbufferName(index);
if (isValid) return true;
Bool ValidateFramebufferName(Uint index, Bool allowZero) {
if (index == 0 && !allowZero) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeShared<GenericErrorInfo>("MG_Impl/GLImpl/FramebufferImpl", "ValidateRenderbufferName",
std::format("Renderbuffer name {} is not valid.", index)));
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl/FramebufferImpl", "ValidateFramebufferName",
"Framebuffer name 0 is not valid in this situation."));
return false;
}
} // namespace FramebufferImpl
} // namespace MobileGL::MG_Impl::GLImpl
Bool isValid = MG_State::pGLContext->ValidateFramebufferName(index);
if (isValid) return true;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl/FramebufferImpl", "ValidateFramebufferName",
std::format("Framebuffer name {} is not valid.", index)));
return false;
}
Bool ValidateFramebufferAttachmentType(FramebufferAttachmentType attachment) {
if (attachment == FramebufferAttachmentType::Unknown) {
using namespace MG_Util;
String attachmentStr = ConvertFramebufferAttachmentTypeToString(attachment);
String glAttachmentStr = ConvertGLEnumToString(ConvertFramebufferAttachmentTypeToGLEnum(attachment));
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl/FramebufferImpl", "ValidateFramebufferAttachmentType",
std::format("Attachment type {} ({}) is not valid.", attachmentStr, glAttachmentStr)));
return false;
}
return true;
}
Bool ValidateRenderbufferTarget(RenderbufferTarget target) {
if (target == RenderbufferTarget::Unknown) {
using namespace MG_Util;
String renderbufferTargetStr = ConvertRenderbufferTargetToString(target);
String glTargetStr = ConvertGLEnumToString(ConvertRenderbufferTargetToGLEnum(target));
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl/FramebufferImpl", "ValidateRenderbufferTarget",
std::format("Target {} ({}) is not valid.", renderbufferTargetStr, glTargetStr)));
return false;
}
return true;
}
Bool ValidateRenderbufferName(Uint index, Bool allowZero) {
if (index == 0 && !allowZero) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl/FramebufferImpl", "ValidateRenderbufferName",
"Renderbuffer name 0 is not valid in this situation."));
return false;
}
Bool isValid = MG_State::pGLContext->ValidateRenderbufferName(index);
if (isValid) return true;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl/FramebufferImpl", "ValidateRenderbufferName",
std::format("Renderbuffer name {} is not valid.", index)));
return false;
}
} // namespace MobileGL::MG_Impl::GLImpl::FramebufferImpl
@@ -10,12 +10,10 @@
#include <Includes.h>
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
namespace MobileGL::MG_Impl::GLImpl {
namespace FramebufferImpl {
Bool ValidateFramebufferTarget(FramebufferTarget target);
Bool ValidateFramebufferName(Uint index, Bool allowZero = true);
Bool ValidateFramebufferAttachmentType(FramebufferAttachmentType attachment);
Bool ValidateRenderbufferTarget(RenderbufferTarget target);
Bool ValidateRenderbufferName(Uint index, Bool allowZero = true);
} // namespace FramebufferImpl
} // namespace MobileGL::MG_Impl::GLImpl
namespace MobileGL::MG_Impl::GLImpl::FramebufferImpl {
Bool ValidateFramebufferTarget(FramebufferTarget target);
Bool ValidateFramebufferName(Uint index, Bool allowZero = true);
Bool ValidateFramebufferAttachmentType(FramebufferAttachmentType attachment);
Bool ValidateRenderbufferTarget(RenderbufferTarget target);
Bool ValidateRenderbufferName(Uint index, Bool allowZero = true);
} // namespace MobileGL::MG_Impl::GLImpl::FramebufferImpl
File diff suppressed because it is too large Load Diff
@@ -13,6 +13,12 @@ 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();
} // namespace MobileGL::MG_Impl::GLImpl
File diff suppressed because it is too large Load Diff
+130 -58
View File
@@ -9,61 +9,133 @@
#pragma once
#include <Includes.h>
namespace MobileGL {
namespace MG_Impl::GLImpl {
void AttachShader(GLuint program, GLuint shader);
void BindAttribLocation(GLuint program, GLuint index, const GLchar* name);
void CompileShader(GLuint shader);
GLuint CreateProgram(void);
GLuint CreateShader(GLenum type);
void DeleteProgram(GLuint program);
void DeleteShader(GLuint shader);
void DetachShader(GLuint program, GLuint shader);
void GetActiveAttrib(GLuint program, GLuint index, GLsizei bufSize, GLsizei* length, GLint* size, GLenum* type,
GLchar* name);
void GetActiveUniform(GLuint program, GLuint index, GLsizei bufSize, GLsizei* length, GLint* size, GLenum* type,
GLchar* name);
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);
void GetProgramInfoLog(GLuint program, GLsizei bufSize, GLsizei* length, GLchar* infoLog);
void GetShaderiv(GLuint shader, GLenum pname, GLint* params);
void GetShaderInfoLog(GLuint shader, GLsizei bufSize, GLsizei* length, GLchar* infoLog);
void GetShaderSource(GLuint shader, GLsizei bufSize, GLsizei* length, GLchar* source);
GLint GetUniformLocation(GLuint program, const GLchar* name);
void GetUniformfv(GLuint program, GLint location, GLfloat* params);
void GetUniformiv(GLuint program, GLint location, GLint* params);
GLboolean IsProgram(GLuint program);
GLboolean IsShader(GLuint shader);
void LinkProgram(GLuint program);
void ShaderSource(GLuint shader, GLsizei count, const GLchar* const* string, const GLint* length);
void UseProgram(GLuint program);
void Uniform1f(GLint location, GLfloat v0);
void Uniform2f(GLint location, GLfloat v0, GLfloat v1);
void Uniform3f(GLint location, GLfloat v0, GLfloat v1, GLfloat v2);
void Uniform4f(GLint location, GLfloat v0, GLfloat v1, GLfloat v2, GLfloat v3);
void Uniform1i(GLint location, GLint v0);
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 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);
void Uniform4fv(GLint location, GLsizei count, const GLfloat* value);
void Uniform1iv(GLint location, GLsizei count, const GLint* value);
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 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);
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);
GLint GetFragDataLocation(GLuint program, const char* name);
void ValidateProgram(GLuint program);
} // namespace MG_Impl::GLImpl
} // namespace MobileGL
namespace MobileGL::MG_Impl::GLImpl {
void AttachShader(GLuint program, GLuint shader);
void BindAttribLocation(GLuint program, GLuint index, const GLchar* name);
void CompileShader(GLuint shader);
GLuint CreateProgram(void);
GLuint CreateShader(GLenum type);
void DeleteProgram(GLuint program);
void DeleteShader(GLuint shader);
void DetachShader(GLuint program, GLuint shader);
void GetActiveAttrib(GLuint program, GLuint index, GLsizei bufSize, GLsizei* length, GLint* size, GLenum* type,
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);
void GetProgramInfoLog(GLuint program, GLsizei bufSize, GLsizei* length, GLchar* infoLog);
void GetShaderiv(GLuint shader, GLenum pname, GLint* params);
void GetShaderInfoLog(GLuint shader, GLsizei bufSize, GLsizei* length, GLchar* infoLog);
void GetShaderSource(GLuint shader, GLsizei bufSize, GLsizei* length, GLchar* source);
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);
void ShaderSource(GLuint shader, GLsizei count, const GLchar* const* string, const GLint* length);
void UseProgram(GLuint program);
void Uniform1f(GLint location, GLfloat v0);
void Uniform2f(GLint location, GLfloat v0, GLfloat v1);
void Uniform3f(GLint location, GLfloat v0, GLfloat v1, GLfloat v2);
void Uniform4f(GLint location, GLfloat v0, GLfloat v1, GLfloat v2, GLfloat v3);
void Uniform1i(GLint location, GLint v0);
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);
void Uniform4fv(GLint location, GLsizei count, const GLfloat* value);
void Uniform1iv(GLint location, GLsizei count, const GLint* value);
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
File diff suppressed because it is too large Load Diff
@@ -9,47 +9,56 @@
#pragma once
#include <Includes.h>
namespace MobileGL {
namespace MG_Impl::GLImpl {
/* @INSERTION_POINT:FUNCTION_DECLARATION@ */
void BlendFuncSeparatei(GLuint buf, GLenum srcRGB, GLenum dstRGB, GLenum srcAlpha, GLenum dstAlpha);
void Disablei(GLenum target, GLuint index);
void Enablei(GLenum target, GLuint index);
void BlendFunc(GLenum sfactor, GLenum dfactor);
void Viewport(GLint x, GLint y, GLsizei width, GLsizei height);
void StencilOpSeparate(GLenum face, GLenum sfail, GLenum dpfail, GLenum dppass);
void StencilOp(GLenum fail, GLenum zfail, GLenum zpass);
void StencilMaskSeparate(GLenum face, GLuint mask);
void StencilMask(GLuint mask);
void StencilFuncSeparate(GLenum face, GLenum func, GLint ref, GLuint mask);
void StencilFunc(GLenum func, GLint ref, GLuint mask);
void Scissor(GLint x, GLint y, GLsizei width, GLsizei height);
void SampleCoverage(GLfloat value, GLboolean invert);
void PolygonOffset(GLfloat factor, GLfloat units);
void PolygonMode(GLenum face, GLenum mode);
void PointSize(GLfloat size);
void PointParameterf(GLenum pname, GLfloat param);
void PointParameteri(GLenum pname, GLint param);
void PixelStorei(GLenum pname, GLint 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 Enable(GLenum cap);
void Disable(GLenum cap);
void DepthRange(GLclampd near_val, GLclampd far_val);
void DepthMask(GLboolean flag);
void DepthFunc(GLenum func);
void CullFace(GLenum mode);
void ColorMask(GLboolean red, GLboolean green, GLboolean blue, GLboolean alpha);
void ClampColor(GLenum target, GLenum clamp);
void BlendFuncSeparate(GLenum sfactorRGB, GLenum dfactorRGB, GLenum sfactorAlpha, GLenum dfactorAlpha);
void BlendEquation(GLenum mode);
void BlendColor(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha);
void ClearStencil(GLint s);
void ClearDepth(GLclampd depth);
void ClearColor(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha);
} // namespace MG_Impl::GLImpl
} // namespace MobileGL
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);
void Viewport(GLint x, GLint y, GLsizei width, GLsizei height);
void StencilOpSeparate(GLenum face, GLenum sfail, GLenum dpfail, GLenum dppass);
void StencilOp(GLenum fail, GLenum zfail, GLenum zpass);
void StencilMaskSeparate(GLenum face, GLuint mask);
void StencilMask(GLuint mask);
void StencilFuncSeparate(GLenum face, GLenum func, GLint ref, GLuint mask);
void StencilFunc(GLenum func, GLint ref, GLuint mask);
void Scissor(GLint x, GLint y, GLsizei width, GLsizei height);
void SampleCoverage(GLfloat value, GLboolean invert);
void PolygonOffset(GLfloat factor, GLfloat units);
void PolygonMode(GLenum face, GLenum mode);
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);
void DepthMask(GLboolean flag);
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);
void ClearColor(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha);
} // namespace MobileGL::MG_Impl::GLImpl
+267 -208
View File
@@ -12,254 +12,313 @@
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
namespace MobileGL {
namespace MG_Impl::GLImpl {
void SetSamplerParam_State(GLuint sampler, GLenum pname, const void* param, bool isFloat, bool isInteger) {
if (!SamplerImpl::ValidateSamplerName(sampler)) return;
namespace MobileGL::MG_Impl::GLImpl {
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);
}
auto samplerObj = MG_State::pGLContext->GetSamplerObject(sampler);
if (!samplerObj) {
samplerObj = MG_State::pGLContext->CreateSamplerObject(sampler); // for compatibility
}
if (!SamplerImpl::ValidateSamplerObject(sampler)) return;
Bool ValidateSamplerParameterValue(GLenum pname, const void* param, Bool isFloat, Bool isUnsignedInteger) {
if (param == nullptr) return false;
using namespace MG_Util;
switch (pname) {
case GL_TEXTURE_WRAP_S:
samplerObj->SetWrapS(MG_Util::ConvertGLEnumToSamplerWrapMode(*(const GLint*)param));
break;
case GL_TEXTURE_WRAP_T:
samplerObj->SetWrapT(MG_Util::ConvertGLEnumToSamplerWrapMode(*(const GLint*)param));
break;
case GL_TEXTURE_WRAP_R:
samplerObj->SetWrapR(MG_Util::ConvertGLEnumToSamplerWrapMode(*(const GLint*)param));
break;
case GL_TEXTURE_MIN_FILTER:
samplerObj->SetMinFilter(MG_Util::ConvertGLEnumToSamplerFilterMode(*(const GLint*)param));
samplerObj->SetMipmapMode(MG_Util::ConvertGLEnumToSamplerMipmapMode(*(const GLint*)param));
break;
case GL_TEXTURE_MAG_FILTER:
samplerObj->SetMagFilter(MG_Util::ConvertGLEnumToSamplerFilterMode(*(const GLint*)param));
break;
case GL_TEXTURE_MIN_LOD:
samplerObj->SetLodRange(*(const GLfloat*)param, samplerObj->GetMaxLod());
break;
case GL_TEXTURE_MAX_LOD:
samplerObj->SetLodRange(samplerObj->GetMinLod(), *(const GLfloat*)param);
break;
case GL_TEXTURE_LOD_BIAS:
samplerObj->SetLodBias(*(const GLfloat*)param);
break;
case GL_TEXTURE_COMPARE_MODE:
samplerObj->SetCompareMode(MG_Util::ConvertGLEnumToSamplerCompareMode(*(const GLint*)param));
break;
case GL_TEXTURE_COMPARE_FUNC:
samplerObj->SetSamplerCompareFunc(MG_Util::ConvertGLEnumToSamplerCompareFunc(*(const GLint*)param));
break;
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:
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum, MakeShared<GenericErrorInfo>("MG_Impl/GLImpl", "SetSamplerParam_State",
"Invalid pname for sampler parameter"));
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 GetSamplerParam_State(GLuint sampler, GLenum pname, void* params, bool isFloat, bool isInteger) {
if (!SamplerImpl::ValidateSamplerName(sampler)) return;
void SetSamplerParam_State(GLuint sampler, GLenum pname, const void* param, bool isFloat,
bool isUnsignedInteger) {
if (param == nullptr) return;
if (!SamplerImpl::ValidateSamplerName(sampler)) return;
auto samplerObj = MG_State::pGLContext->GetSamplerObject(sampler);
if (!samplerObj) {
samplerObj = MG_State::pGLContext->CreateSamplerObject(sampler); // for compatibility
}
if (!SamplerImpl::ValidateSamplerObject(sampler)) return;
using namespace MG_Util;
switch (pname) {
case GL_TEXTURE_WRAP_S:
*(GLint*)params = MG_Util::ConvertSamplerWrapModeToGLEnum(samplerObj->GetWrapS());
break;
case GL_TEXTURE_WRAP_T:
*(GLint*)params = MG_Util::ConvertSamplerWrapModeToGLEnum(samplerObj->GetWrapT());
break;
case GL_TEXTURE_WRAP_R:
*(GLint*)params = MG_Util::ConvertSamplerWrapModeToGLEnum(samplerObj->GetWrapR());
break;
case GL_TEXTURE_MIN_FILTER:
*(GLint*)params =
MG_Util::ConvertSamplerFilterModeToGLEnum(samplerObj->GetMinFilter(), samplerObj->GetMipmapMode());
break;
case GL_TEXTURE_MAG_FILTER:
*(GLint*)params =
MG_Util::ConvertSamplerFilterModeToGLEnum(samplerObj->GetMagFilter(), SamplerMipmapMode::None);
break;
case GL_TEXTURE_MIN_LOD:
*(GLfloat*)params = samplerObj->GetMinLod();
break;
case GL_TEXTURE_MAX_LOD:
*(GLfloat*)params = samplerObj->GetMaxLod();
break;
case GL_TEXTURE_LOD_BIAS:
*(GLfloat*)params = samplerObj->GetLodBias();
break;
case GL_TEXTURE_COMPARE_MODE:
*(GLint*)params = MG_Util::ConvertSamplerCompareModeToGLEnum(samplerObj->GetCompareMode());
break;
case GL_TEXTURE_COMPARE_FUNC:
*(GLint*)params = MG_Util::ConvertSamplerCompareFuncToGLEnum(samplerObj->GetSamplerCompareFunc());
break;
default:
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum, MakeShared<GenericErrorInfo>("MG_Impl/GLImpl", "GetSamplerParam_State",
"Invalid pname for sampler parameter"));
}
Bool doesSamplerObjectCreated = MG_State::pGLContext->ValidateSamplerObject(sampler);
if (!doesSamplerObjectCreated) {
// Create one for compatibility
MG_State::pGLContext->CreateSamplerObject(sampler);
}
auto& samplerObj = MG_State::pGLContext->GetSamplerObject(sampler);
if (!SamplerImpl::ValidateSamplerObject(sampler)) return;
if (!ValidateSamplerParameterValue(pname, param, isFloat, isUnsignedInteger)) return;
GLboolean IsSampler_State(GLuint sampler) {
return MG_State::pGLContext->ValidateSamplerObject(sampler) ? GL_TRUE : GL_FALSE;
using namespace MG_Util;
switch (pname) {
case GL_TEXTURE_WRAP_S:
samplerObj->SetWrapS(MG_Util::ConvertGLEnumToSamplerWrapMode(*(const GLint*)param));
break;
case GL_TEXTURE_WRAP_T:
samplerObj->SetWrapT(MG_Util::ConvertGLEnumToSamplerWrapMode(*(const GLint*)param));
break;
case GL_TEXTURE_WRAP_R:
samplerObj->SetWrapR(MG_Util::ConvertGLEnumToSamplerWrapMode(*(const GLint*)param));
break;
case GL_TEXTURE_MIN_FILTER:
samplerObj->SetMinFilter(MG_Util::ConvertGLEnumToSamplerFilterMode(*(const GLint*)param));
samplerObj->SetMipmapMode(MG_Util::ConvertGLEnumToSamplerMipmapMode(*(const GLint*)param));
break;
case GL_TEXTURE_MAG_FILTER:
samplerObj->SetMagFilter(MG_Util::ConvertGLEnumToSamplerFilterMode(*(const GLint*)param));
break;
case GL_TEXTURE_MIN_LOD:
samplerObj->SetLodRange(*(const GLfloat*)param, samplerObj->GetMaxLod());
break;
case GL_TEXTURE_MAX_LOD:
samplerObj->SetLodRange(samplerObj->GetMinLod(), *(const GLfloat*)param);
break;
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;
case GL_TEXTURE_COMPARE_FUNC:
samplerObj->SetSamplerCompareFunc(MG_Util::ConvertGLEnumToSamplerCompareFunc(*(const GLint*)param));
break;
default:
MG_State::pGLContext->RecordError(ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "SetSamplerParam_State",
"Invalid pname for sampler parameter"));
}
}
// migrate below functions without "_State" into this section
void GenSamplers_State(GLsizei count, GLuint* samplers) {
if (count < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeShared<GenericErrorInfo>("MG_Impl/GLImpl", "GenSamplers", "count must be non-negative"));
return;
}
void GetSamplerParam_State(GLuint sampler, GLenum pname, void* params, bool isFloat,
bool isUnsignedInteger) {
if (params == nullptr) return;
if (!SamplerImpl::ValidateSamplerName(sampler)) return;
auto names = MG_State::pGLContext->GenSamplerNames(count);
for (GLsizei i = 0; i < count; ++i) {
samplers[i] = names[i];
}
Bool doesSamplerObjectCreated = MG_State::pGLContext->ValidateSamplerObject(sampler);
if (!doesSamplerObjectCreated) {
// Create one for compatibility
MG_State::pGLContext->CreateSamplerObject(sampler);
}
auto& samplerObj = MG_State::pGLContext->GetSamplerObject(sampler);
if (!SamplerImpl::ValidateSamplerObject(sampler)) return;
void DeleteSamplers_State(GLsizei count, const GLuint* samplers) {
if (count < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeShared<GenericErrorInfo>("MG_Impl/GLImpl", "DeleteSamplers", "count must be non-negative"));
return;
}
for (GLsizei i = 0; i < count; ++i) {
if (samplers[i] != 0) {
MG_State::pGLContext->MarkSamplerObjectForDeletion(samplers[i]);
}
}
}
void CreateSamplers_State(GLsizei n, GLuint* samplers) {
if (n < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeShared<GenericErrorInfo>("MG_Impl/GLImpl", "GenSamplers", "count must be non-negative"));
return;
}
auto names = MG_State::pGLContext->GenSamplerNames(n);
for (GLsizei i = 0; i < n; ++i) {
samplers[i] = names[i];
MG_State::pGLContext->CreateSamplerObject(names[i]);
}
}
void BindSampler_State(GLuint unit, GLuint sampler) {
if (unit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeShared<GenericErrorInfo>("MG_Impl/GLImpl", "BindSampler", "texture unit out of range"));
return;
}
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
if (sampler == 0) {
textureUnit.SetSamplerObject(nullptr);
using namespace MG_Util;
switch (pname) {
case GL_TEXTURE_WRAP_S:
*(GLuint*)params = MG_Util::ConvertSamplerWrapModeToGLEnum(samplerObj->GetWrapS());
break;
case GL_TEXTURE_WRAP_T:
*(GLuint*)params = MG_Util::ConvertSamplerWrapModeToGLEnum(samplerObj->GetWrapT());
break;
case GL_TEXTURE_WRAP_R:
*(GLuint*)params = MG_Util::ConvertSamplerWrapModeToGLEnum(samplerObj->GetWrapR());
break;
case GL_TEXTURE_MIN_FILTER:
*(GLuint*)params =
MG_Util::ConvertSamplerFilterModeToGLEnum(samplerObj->GetMinFilter(), samplerObj->GetMipmapMode());
break;
case GL_TEXTURE_MAG_FILTER:
*(GLuint*)params =
MG_Util::ConvertSamplerFilterModeToGLEnum(samplerObj->GetMagFilter(), SamplerMipmapMode::None);
break;
case GL_TEXTURE_MIN_LOD:
*(GLfloat*)params = samplerObj->GetMinLod();
break;
case GL_TEXTURE_MAX_LOD:
*(GLfloat*)params = samplerObj->GetMaxLod();
break;
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 {
if (!SamplerImpl::ValidateSamplerName(sampler)) return;
auto samplerObject = MG_State::pGLContext->GetSamplerObject(sampler);
if (!samplerObject) {
samplerObject = MG_State::pGLContext->CreateSamplerObject(sampler);
}
*(GLint*)params = static_cast<GLint>(samplerObj->GetMaxAnisotropy());
}
break;
case GL_TEXTURE_COMPARE_MODE:
*(GLuint*)params = MG_Util::ConvertSamplerCompareModeToGLEnum(samplerObj->GetCompareMode());
break;
case GL_TEXTURE_COMPARE_FUNC:
*(GLuint*)params = MG_Util::ConvertSamplerCompareFuncToGLEnum(samplerObj->GetSamplerCompareFunc());
break;
default:
MG_State::pGLContext->RecordError(ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetSamplerParam_State",
"Invalid pname for sampler parameter"));
}
}
textureUnit.SetSamplerObject(MG_State::pGLContext->GetSamplerObject(sampler));
GLboolean IsSampler_State(GLuint sampler) {
return MG_State::pGLContext->ValidateSamplerObject(sampler) ? GL_TRUE : GL_FALSE;
}
// migrate below functions without "_State" into this section
void GenSamplers_State(GLsizei count, GLuint* samplers) {
if (count < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GenSamplers", "count must be non-negative"));
return;
}
static thread_local Vector<GLuint> names;
MG_State::pGLContext->GenSamplerNames(count, names);
Memcpy(samplers, names.data(), count * sizeof(GLuint));
}
void DeleteSamplers_State(GLsizei count, const GLuint* samplers) {
if (count < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "DeleteSamplers", "count must be non-negative"));
return;
}
for (GLsizei i = 0; i < count; ++i) {
if (samplers[i] != 0) {
MG_State::pGLContext->MarkSamplerObjectForDeletion(samplers[i]);
}
}
}
void BindSamplers_State(GLuint first, GLsizei count, const GLuint* samplers) {
if (count < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeShared<GenericErrorInfo>("MG_Impl/GLImpl", "BindSamplers", "count must be non-negative"));
return;
void CreateSamplers_State(GLsizei n, GLuint* samplers) {
if (n < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GenSamplers", "count must be non-negative"));
return;
}
static thread_local Vector<GLuint> names;
MG_State::pGLContext->GenSamplerNames(n, names);
Memcpy(samplers, names.data(), n * sizeof(GLuint));
for (GLsizei i = 0; i < n; ++i) {
samplers[i] = names[i];
MG_State::pGLContext->CreateSamplerObject(names[i]);
}
}
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,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "BindSampler", "texture unit out of range"));
return;
}
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;
Bool doesSamplerObjectCreated = MG_State::pGLContext->ValidateSamplerObject(sampler);
if (!doesSamplerObjectCreated) {
MG_State::pGLContext->CreateSamplerObject(sampler);
}
auto& samplerObject = MG_State::pGLContext->GetSamplerObject(sampler);
for (GLsizei i = 0; i < count; ++i) {
BindSampler_State(first + i, samplers ? samplers[i] : 0);
}
textureUnit.SetSamplerObject(MG_State::pGLContext->GetSamplerObject(sampler));
}
}
void BindSamplers_State(GLuint first, GLsizei count, const GLuint* samplers) {
if (count < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "BindSamplers", "count must be non-negative"));
return;
}
/* @INSERTION_POINT:FUNCTION_IMPLEMENTATION@ */
void GetSamplerParameteriv(GLuint sampler, GLenum pname, GLint* params) {
GetSamplerParam_State(sampler, pname, params, false, false);
for (GLsizei i = 0; i < count; ++i) {
BindSampler_State(first + i, samplers ? samplers[i] : 0);
}
}
void SamplerParameterIuiv(GLuint sampler, GLenum pname, const GLuint* param) {
SetSamplerParam_State(sampler, pname, param, false, true);
}
/* @INSERTION_POINT:FUNCTION_IMPLEMENTATION@ */
void GetSamplerParameteriv(GLuint sampler, GLenum pname, GLint* params) {
GetSamplerParam_State(sampler, pname, params, false, false);
}
void SamplerParameterIiv(GLuint sampler, GLenum pname, const GLint* param) {
SetSamplerParam_State(sampler, pname, param, false, true);
}
void SamplerParameterIuiv(GLuint sampler, GLenum pname, const GLuint* param) {
SetSamplerParam_State(sampler, pname, param, false, true);
}
void SamplerParameteriv(GLuint sampler, GLenum pname, const GLint* param) {
SetSamplerParam_State(sampler, pname, param, false, false);
}
void SamplerParameterIiv(GLuint sampler, GLenum pname, const GLint* param) {
SetSamplerParam_State(sampler, pname, param, false, false);
}
void SamplerParameterfv(GLuint sampler, GLenum pname, const GLfloat* param) {
SetSamplerParam_State(sampler, pname, param, true, false);
}
void SamplerParameteriv(GLuint sampler, GLenum pname, const GLint* param) {
SetSamplerParam_State(sampler, pname, param, false, false);
}
void SamplerParameteri(GLuint sampler, GLenum pname, GLint param) {
SamplerParameteriv(sampler, pname, &param);
}
void SamplerParameterfv(GLuint sampler, GLenum pname, const GLfloat* param) {
SetSamplerParam_State(sampler, pname, param, true, false);
}
void SamplerParameterf(GLuint sampler, GLenum pname, GLfloat param) {
SamplerParameterfv(sampler, pname, &param);
}
void SamplerParameteri(GLuint sampler, GLenum pname, GLint param) {
SamplerParameteriv(sampler, pname, &param);
}
GLboolean IsSampler(GLuint sampler) {
return IsSampler_State(sampler);
}
void SamplerParameterf(GLuint sampler, GLenum pname, GLfloat param) {
SamplerParameterfv(sampler, pname, &param);
}
void GetSamplerParameterIuiv(GLuint sampler, GLenum pname, GLuint* params) {
GetSamplerParam_State(sampler, pname, params, false, true);
}
GLboolean IsSampler(GLuint sampler) {
return IsSampler_State(sampler);
}
void GetSamplerParameterIiv(GLuint sampler, GLenum pname, GLint* params) {
GetSamplerParam_State(sampler, pname, params, false, true);
}
void GetSamplerParameterIuiv(GLuint sampler, GLenum pname, GLuint* params) {
GetSamplerParam_State(sampler, pname, params, false, true);
}
void GetSamplerParameterfv(GLuint sampler, GLenum pname, GLfloat* params) {
GetSamplerParam_State(sampler, pname, params, true, false);
}
void GetSamplerParameterIiv(GLuint sampler, GLenum pname, GLint* params) {
GetSamplerParam_State(sampler, pname, params, false, false);
}
void GenSamplers(GLsizei count, GLuint* samplers) {
GenSamplers_State(count, samplers);
}
void GetSamplerParameterfv(GLuint sampler, GLenum pname, GLfloat* params) {
GetSamplerParam_State(sampler, pname, params, true, false);
}
void DeleteSamplers(GLsizei count, const GLuint* samplers) {
DeleteSamplers_State(count, samplers);
}
void GenSamplers(GLsizei count, GLuint* samplers) {
GenSamplers_State(count, samplers);
}
void CreateSamplers(GLsizei n, GLuint* samplers) {
CreateSamplers_State(n, samplers);
}
void DeleteSamplers(GLsizei count, const GLuint* samplers) {
DeleteSamplers_State(count, samplers);
}
void BindSamplers(GLuint first, GLsizei count, const GLuint* samplers) {
BindSamplers_State(first, count, samplers);
}
void CreateSamplers(GLsizei n, GLuint* samplers) {
CreateSamplers_State(n, samplers);
}
void BindSampler(GLuint unit, GLuint sampler) {
BindSampler_State(unit, sampler);
}
} // namespace MG_Impl::GLImpl
} // namespace MobileGL
void BindSamplers(GLuint first, GLsizei count, const GLuint* samplers) {
BindSamplers_State(first, count, samplers);
}
void BindSampler(GLuint unit, GLuint sampler) {
BindSampler_State(unit, sampler);
}
} // namespace MobileGL::MG_Impl::GLImpl

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