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.
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.
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%.
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%.
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>
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.
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.
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.)
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).