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