Commit Graph
1557 Commits
Author SHA1 Message Date
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