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