Conversation
…ilent index truncation
The CPU base-vertex rebase wrote (index + basevertex) back into the source
index width, truncating it to mod 65536 / mod 256. GL 4.6 sec. 10.5 evaluates
that sum in the full vertex index space, so the common "one large vertex
buffer + 16-bit indices + large basevertex" layout rendered wrong geometry
with no error and no log. The rebased stream is now always widened to
GL_UNSIGNED_INT.
Eight GL 4.6 core entry points were exported no-op stubs, which compiled to an
empty body in release builds: an application calling them lost its geometry and
saw GL_NO_ERROR. glMultiDrawArrays and glMultiDraw{Arrays,Elements}Indirect are
now implemented -- the GLES backend for the latter was already being used
internally by the multiindirect mode but was never exposed -- and the
*IndirectCount pair reports once instead of vanishing silently.
Correctness:
- Widen every CPU rebase to 32-bit indices (multidraw.cpp, drawelements mode).
- Handle GL_PRIMITIVE_RESTART_FIXED_INDEX: the sentinel is not offset by
basevertex and is re-emitted as the 32-bit sentinel, but only when restart is
actually enabled -- 0xFFFF is an ordinary vertex index otherwise.
- Fall back from the compute path when restart is enabled, when the primitive
mode is not separable, or when a sub-draw count is not a whole number of
primitives; fusing would otherwise join primitives across sub-draws.
- Stop rejecting indices[i] == 0 in the compute path. An element array buffer is
bound at that point, so zero is a legal byte offset -- and it is the offset of
the first sub-draw of every arena-style batch, which meant falling back to the
slowest path on nearly every frame.
- Fall back instead of truncating on a misaligned index offset.
- Hoist the index-type switch out of the per-sub-draw loop, where its default
branch abandoned the rest of the multi-draw.
- Do not draw a sub-draw whose base vertex cannot be applied; wrong geometry is
worse than missing geometry.
State and lifetime:
- Restore the indexed SSBO bindings after the memory barrier and before the
application's draw. They are context state, so the scratch buffers were
visible to the application's own shader during the draw, and a writable block
declared on those bindings could corrupt the index buffer being consumed.
- Preserve glBindBufferRange offset/size when restoring instead of widening
every sub-range binding to the whole buffer.
- Invalidate all cached GL object names when the current EGL context changes.
- prepare_indirect_buffer now returns a status: the map result is checked, the
tracked capacity is only recorded once the allocation is verified and is
cleared on failure, and every caller falls back instead of drawing from an
unwritten command buffer.
- Clamp negative counts before they reach the unsigned command field, where they
became ~4.29e9-index draws.
- Require a bound element array buffer on the indirect paths.
- Latch compute and native failures so a rejecting driver costs one draw rather
than a full pipeline rebuild on every call.
- Replace the per-sub-draw glGenBuffers/glDeleteBuffers with one persistent
scratch buffer; move g_prefix_sum and the shader info log off globals.
Capability selection:
- AND every capability with its resolved entry point. The loader uses a plain
dlsym, so a driver can advertise GL_EXT_multi_draw_indirect while the symbol
is absent, which meant a null jump on the first frame.
- Add the missing PreferMultidrawIndirect case, which silently fell into the
Auto branch and logged the wrong mode.
- Validate Compute against ES 3.1 and the shader storage block limit instead of
accepting it unconditionally; reduce the compute shader from five shader
storage blocks to four, the GLES 3.1 guaranteed minimum.
- Add a NativeMultiDraw mode backed by glMultiDrawElementsBaseVertexEXT, which
was already loaded but had no call site. It is ordered after
MultidrawIndirect in Auto so devices that already used that path are
unaffected, and its first call is probed because the extension's multi-draw
form is conditional on EXT_multi_draw_arrays, which is not tracked.
Diagnostics: the CHECK_GL_ERROR family and LOG_D/LOG_W/LOG_E compile to nothing
in release builds, so every fallback in this file was invisible. Fallbacks now
report once through LOG_W_FORCE, and error probes that drive a fallback decision
read glGetError directly.
Verified by compilation and by host-side tests of the rebase and prefix-sum
search; no runtime or on-device verification was performed.
mg_native_multidraw kept its "already probed" flag in a function-local static while the matching failure latch lived in a file-scope global that multidraw_check_context() clears on every context change. Only one of the two was reset. On a driver whose glMultiDrawElementsBaseVertexEXT is a stub, that meant: the first context probed, failed, latched, and correctly fell back; the application then destroyed and recreated its EGL context; the latch was cleared but the probe was not re-armed, so every later call issued the stub entry point and reported success unconditionally. The caller therefore stopped falling back and the geometry was silently lost for the rest of the process, with no log left -- the one-shot warning had already been spent on the first context. Both flags are now one native_state_t tri-state, so they cannot drift apart again. Also in the native path: - Fall back to the MultidrawIndirect implementation instead of the unrolled loop. init_settings_post already declares MultidrawIndirect to be the next rung below Native, and it still folds the batch into one driver call; dropping straight to N draw calls threw that away. The handover happens before the batch is validated and prepared so the permanent cost is one pass, not two. - Route glMultiDrawElements in Compute mode through the same path. There is no base vertex to rebase on that entry point, so the compute pipeline has nothing to do, but folding the batch is still worth doing -- unrolling made Compute strictly slower than MultidrawIndirect on devices that support both. - Reuse a thread_local zero array for the synthesised base vertex parameter instead of heap-allocating one on every call. The fallback graph remains acyclic: compute -> native -> multiindirect -> indirect -> drawelements.
One "multidrawMode" key decided every multi-draw entry point at once, which
forced combinations that mean nothing. glMultiDrawElements has no base vertex,
so PreferBaseVertex, Compute and DrawElements were three names for one unrolled
loop there -- and dump_settings_string advertised them as three strategies.
glMultiDrawArrays and the two *Indirect entry points ignored the key entirely.
Each entry point that has more than one implementation now gets its own key:
multidrawModeArrays glMultiDrawArrays
multidrawModeElements glMultiDrawElements
multidrawModeElementsBaseVertex glMultiDrawElementsBaseVertex
multidrawModeArraysIndirect glMultiDrawArraysIndirect
multidrawModeElementsIndirect glMultiDrawElementsIndirect
The values are backend NAMES ("auto", "unroll", "basevertex", "indirect",
"multiindirect", "native", "nativeext", "compute") rather than indices. Names
remove the two problems the shared integer had: a value that is meaningful for
one entry point but not another, and a value space that cannot grow without
repointing something a user already wrote into config.json. Each entry point
declares which backends are a distinct implementation for it, and a value
outside that set is rejected with a message saying why, then treated as absent.
multidrawDisableBackends takes a comma-separated list and clears those backends
everywhere before resolution, so a report of "this backend is broken on this
GPU" can be worked around with one key instead of guessing a strategy per entry
point. A disabled backend is indistinguishable from one the driver lacks, so it
degrades through the same ladder.
glMultiDraw*IndirectCount get no key: they have exactly one implementation.
Also adds GL_EXT_multi_draw_arrays / GL_ANGLE_multi_draw detection. Those entry
points are the exact GLES equivalents of the GL 1.4 core commands -- one driver
call, no command buffer to build -- but the GLES loader does not carry them.
They are resolved from the `gles` library handle rather than through
eglGetProcAddress, because this layer's eglGetProcAddress forwards to
glXGetProcAddress, which resolves against RTLD_DEFAULT and would hand back
MobileGlues' own glMultiDrawArraysEXT -- an alias of glMultiDrawArrays -- and
recurse until the stack ran out.
Nothing reads the new fields yet; multidrawMode still drives behaviour and now
logs a deprecation notice. Emitted GL calls are unchanged.
… keys The two glMultiDrawElements* dispatchers now read the backend resolved for their own entry point, and multidrawMode is no longer read at all: multidraw_mode_t and global_settings.multidraw_mode are gone. A config that still sets the old key gets one message naming the replacements; the entry points fall back to auto, which resolves to the same backend the old Auto did. handle_multidraw_func_name becomes per-name. It used to mangle both names with the one shared mode; it now looks up the backend of whichever entry point was asked for, so the symbol glXGetProcAddress hands to the application and the function the dispatcher would have called can no longer disagree. The suffix table lives next to the backend table in config/settings.cpp for that reason, and its default branch returns the plain name -- resolving to the dispatcher -- rather than a symbol that does not exist. Adds the NativeExt backend for glMultiDrawElements, backed by glMultiDrawElementsEXT. It has exactly the signature and semantics of the GL 1.4 core command, so it is one driver call with no command buffer to build and no synthesised base vertex array, which the Native backend needs. Same probe-and-latch as Native, and the latch is reset on a context change with the others. NativeExt is deliberately absent from the glMultiDrawElementsBaseVertex mask: EXT_multi_draw_arrays carries no base vertex, so there is no such implementation, and the missing mg_glMultiDrawElementsBaseVertex_nativeext symbol is unreachable by construction rather than by luck. dump_settings_string now prints the resolved backend per entry point plus any disabled backends, replacing the single line that claimed BaseVertex, Compute and DrawElements were three different strategies for glMultiDrawElements.
glMultiDrawArrays was a hardcoded unrolled loop that consulted no setting at all, so a batch of 300 sub-draws cost 300 driver calls on every device and in every mode. It now selects a backend like the other entry points: nativeext one glMultiDrawArraysEXT multiindirect one glMultiDrawArraysIndirectEXT over a built command buffer unroll the previous loop GLES.glMultiDrawArraysIndirectEXT has been loaded all along and had exactly one call site, forwarding the application's own buffer inside glMultiDrawArraysIndirect; it never accelerated glMultiDrawArrays. The command buffer gets its own scratch buffer and capacity counter. DrawArraysIndirectCommand is 16 bytes against the element command's 20, and the counters are in commands rather than bytes, so sharing one buffer would let an Arrays call "grow" it enough for an Elements call to skip its own resize and then map past the end of the store. Guards on the multiindirect path, each falling back to the unrolled loop rather than reporting an error, because the loop is legal where the indirect draw is not: - a vertex array object must be bound, which GLES requires for indirect draws and which also implies every enabled array is buffer-backed; - transform feedback must not be active-and-unpaused; - first[i] and count[i] must be non-negative. first is a GLuint in the command, so a negative value would become a ~4.29e9 vertex offset rather than an error. Both native backends probe their first call and latch off if the driver rejects it, like the Elements side. Also re-arms those latches properly: every entry point that reads one now calls multidraw_check_context() before the read. The two Elements native entry points were reading theirs before mg_multidraw_enter() ran the check, so the first call after a context change saw the previous context's verdict.
… on the GPU Both entry points drew nothing and logged once. They are GL 4.6 core, and customGLVersion goes up to 46, so an application is entitled to call them. The draw count lives in the buffer bound to GL_PARAMETER_BUFFER and is normally written by the GPU -- that is the entire point of the command. Reading it back on the CPU would stall the pipeline every frame, and reading it without a fence would take a stale value and draw the wrong number of commands. So the compaction happens on the GPU: a compute shader copies maxdrawcount commands into a scratch buffer and writes instanceCount 0 into every command at or past the real count. A command with instanceCount 0 draws nothing, so one glMultiDraw*IndirectEXT over the scratch buffer produces exactly the requested draws, with no readback and no stall. Where EXT_multi_draw_indirect is missing, walking the compacted buffer with glDraw*Indirect gives the same result one call at a time. Drawing maxdrawcount commands unchanged would not have been a valid shortcut: the slots past the count hold stale or zeroed commands, and rendering them puts back exactly the geometry the application just culled. The count read in the shader is clamped to maxdrawcount. The application promises count <= maxdrawcount, but the parameter buffer is ordinary memory and a corrupt value must not make the copy read past the end of the source commands. gl/buffer.cpp now tracks GL_PARAMETER_BUFFER. binding_target_to_index() did not know the target, so set_bound_buffer_by_target() discarded the binding and glBindBuffer forwarded 0x80EE to a driver that does not recognise it. The binding is recorded and the backing object created, and the target is not forwarded. Known limitation: filling the parameter buffer THROUGH the GL_PARAMETER_BUFFER target still will not work, because glBufferSubData is a direct passthrough in gl_native.cpp rather than a wrapper here. Fill it as a shader storage buffer or via GL_COPY_WRITE_BUFFER, which is what a GPU-driven renderer does anyway. compile_compute_program is now generic. It had grown a lookup of uElementSize and returned failure when that uniform was absent, which would have rejected every other shader; the index-fusion path does that lookup itself.
glMultiDrawArraysIndirect and glMultiDrawElementsIndirect never read the keys added for them. Both resolved a backend, logged it and printed it in the settings dump, while the entry points themselves still branched on driver capability alone -- the two keys had no effect on rendering at all. Findings from the review of the preceding four commits: find_bound_buffer() takes a *_BINDING query enum, not a target, so find_bound_buffer(GL_PARAMETER_BUFFER) fell through to default and returned 0 every time. The parameter buffer therefore always looked unbound and BOTH glMultiDraw*IndirectCount entry points bailed out before the compaction ran: the shader, the scratch buffer, the barrier and the draw had never executed once. The lookup now takes GL_PARAMETER_BUFFER_BINDING and find_bound_buffer knows it. Deleting a buffer left the parameter buffer slot pointing at it. Ids are recycled by gen_buffer(), and that slot is the only source of truth for where the draw count lives -- there is no driver-side binding to cross-check it against -- so a stale slot could silently designate somebody else's buffer. The compaction saved GL_SHADER_STORAGE_BUFFER_BINDING *after* the scratch sizing block had already bound and unbound the scratch buffer, so it recorded 0 and then restored that, clearing the application's binding. Every piece of state the dispatch takes over is now captured before anything is touched. The scratch buffer was reused in place, so the next call's compute writes raced the previous draw's command fetch. It is now re-specified unconditionally, the same invariant the index fusion path documents and relies on. Neither the source commands nor the draw count were bounds-checked, and an out-of-range SSBO access is undefined in GLES rather than an error. maxdrawcount is allowed to exceed the number of commands actually stored, so both buffers are now measured before the dispatch. The scratch size is accounted in size_t: the GLsizei comparison could wrap negative and skip the allocation entirely. multidrawDisableBackends only applied during resolution. Every runtime fallback chain in gl/multidraw.cpp branched on the function pointer alone, so disabling a backend still let a chain hand control straight to it -- which is the one thing the key exists to prevent. All ten fallback points consult md_backend_disabled() now, and the two Elements multiindirect implementations also apply the extension string gate that resolution uses, since they are reachable as fallbacks without resolution having vetted them. Smaller ones: ladder exhaustion returned Unroll even for the two *Indirect entries, whose value sets do not contain it; indirect_arrays skipped the ES 3.1 gate its Elements counterpart applies; on Apple the extension resolution would have gone through a dlsym pseudo-handle and found MobileGlues' own alias; compile_compute_program builds two programs but reported both as the fusion shader through one shared warning latch; a trailing space in multidrawDisableBackends produced a bogus "not a backend name" line; and the count clamp in the shader was a no-op whose comment claimed it bounded the reads.
…ions that provide them
GLES core has no multi-draw command at all. Not even 3.2, which added the
singular glDrawElementsBaseVertex and no multi form. Every batched backend in
this file therefore comes from an extension, so calling one of them "native" and
another "nativeext" claimed a distinction that does not exist -- and the one
named "native" was the extension entry point, not a core one.
native -> multibasevertex glMultiDrawElementsBaseVertexEXT
(GL_EXT/OES_draw_elements_base_vertex)
nativeext -> multiarrays glMultiDraw{Arrays,Elements}EXT
(GL_EXT_multi_draw_arrays / GL_ANGLE_multi_draw)
The real distinction is which extension supplies the entry point and whether its
signature carries a base vertex, which the new names say directly. Ordering the
enum unroll / basevertex / indirect / multiarrays / multibasevertex /
multiindirect also makes the cost visible: the first three issue one driver call
per sub-draw, the last three issue one for the whole batch.
Renames the config values, the md_backend_t enumerators, the mg_* symbol
suffixes glx/lookup.cpp generates, the probe latches, and every comment and log
message that described these as native. native_state_t becomes md_probe_state_t,
since it never had anything to do with nativeness -- it tracks whether the first
call to an extension entry point was accepted.
The values are only reachable through the new per-entry-point keys, which have
not shipped, so nothing in the field refers to the old spellings.
LOAD_EGL was an unsynchronised double-checked initialisation. Both statics were constant-initialised, so the compiler emitted no guard, and nothing ordered the write of the function pointer against another thread's read of the flag: two threads entering any EGL wrapper for the first time could see first == false with the pointer still null. EGL is explicitly per-thread, so concurrent first entry is ordinary, not exotic. The initialiser is now a lambda call. Dynamic initialisation of a function-local static is thread-safe by the language rule, and the object file confirms the compiler emits __cxa_guard around it. Resolution failures were also invisible and then fatal: the warning used LOG_W, which compiles to nothing in release builds, and the caller went on to jump through the null pointer regardless. Failures now report through LOG_W_FORCE, and LOAD_EGL_OR gives the six platform and EXT entry points -- the ones a backend is genuinely allowed not to export -- a way to fail as EGL_NO_DISPLAY or EGL_NO_SURFACE with EGL_BAD_PARAMETER instead of crashing. The trailing semicolon lives inside the macro: the previous shape ended in a block, so none of the 56 call sites carry one.
Six GL 4.6 enable capabilities that GLES 3.2 core lacks are available on some devices through an extension that uses the SAME enum value as the desktop one: GL_EXT_multisample_compatibility GL_MULTISAMPLE, GL_SAMPLE_ALPHA_TO_ONE GL_EXT_clip_cull_distance GL_CLIP_DISTANCE0..7 GL_EXT_depth_clamp GL_DEPTH_CLAMP GL_EXT_sRGB_write_control GL_FRAMEBUFFER_SRGB GL_NV_polygon_mode GL_POLYGON_OFFSET_LINE / _POINT GL_OES_sample_shading GL_SAMPLE_SHADING before ES 3.2 Because the values match, glEnable currently succeeds on a device that exposes the extension and fails with GL_INVALID_ENUM on one that does not -- the same call behaving differently per device, with nothing in the layer able to tell the two apart. g_gles_caps tracked none of them. Tracking them is a prerequisite for the enable state table: a capability the driver really supports must keep being forwarded rather than becoming a value the layer merely remembers. GL_EXT_multi_draw_arrays is deliberately not a member here. glext.h defines a macro of exactly that name, so it cannot be a struct field, and gl/multidraw.cpp already probes it lazily because it needs the entry points as well as the string.
…ate table
glEnable, glDisable, glIsEnabled and the indexed forms were bare passthroughs.
GL 4.6 has 28 enable capabilities and 13 of them do not exist in GLES 3.2 core,
so those 13 reached the driver as GL_INVALID_ENUM and were simply lost.
Worse, the five state queries contradicted each other. glIsEnabled owes GL_FALSE
on error, glGetBooleanv/glGetFloatv/glGetIntegerv leave their output untouched
on error, and glGetDoublev was a no-op stub that never wrote params at all -- so
the same capability answered three different things in the same frame, one of
them being whatever was on the caller's stack. glGetError always reports
GL_NO_ERROR, so nothing gave the application a hint.
All of it now goes through gl/enable.cpp. The table records every capability;
whether the driver is also told is a property of the capability rather than an
accident of which enum it happens to recognise:
14 forwarded always GLES 3.2 core knows the enum
7 forwarded if the extension detected in the previous commit is present --
those use the same enum value as the desktop capability, so a device that
has the extension keeps getting the real behaviour
6 recorded only GL_COLOR_LOGIC_OP, GL_LINE_SMOOTH, GL_POLYGON_SMOOTH,
GL_PRIMITIVE_RESTART, GL_PROGRAM_POINT_SIZE,
GL_TEXTURE_CUBE_MAP_SEAMLESS
GL_CLIP_DISTANCE0..7 is a contiguous enum range rather than one value and is
held as a bitmask. GL_BLEND is tracked per draw buffer, and setting the scalar
form sets every draw buffer, as the specification requires.
GL_MULTISAMPLE is specified to start enabled, but without
GL_EXT_multisample_compatibility the application cannot turn multisampling off
anyway -- the EGL config decides it -- so claiming GL_TRUE on a single-sampled
framebuffer would just be a lie in the other direction. It is seeded from
GL_SAMPLES and tracked from there, so enable/disable still round-trips.
GL_FRAMEBUFFER_SRGB is recorded and forwarded where
GL_EXT_sRGB_write_control exists. Making it genuinely take effect otherwise
would mean injecting EGL_GL_COLORSPACE_SRGB_KHR at surface creation, which is a
one-time decision that cannot answer a later glEnable; that is deliberately not
done.
No GL error is ever raised for a bad capability or index, because glGetError
must keep answering GL_NO_ERROR. An unusable argument means the call does
nothing and one forced line reaches the log.
glGetDoublev is now implemented rather than stubbed. The ARB spellings of the
indexed forms are kept as aliases: the NATIVE_FUNCTION macro used to generate
them, and glXGetProcAddress is a bare dlsym, so a vanished symbol would have
become a silently null function pointer.
GL 4.6 has two restart features and GLES only has one of them. GL_PRIMITIVE_RESTART_FIXED_INDEX restarts on the largest value the index type can hold and forwards unchanged. GL_PRIMITIVE_RESTART restarts on whatever glPrimitiveRestartIndex chose, and GLES has no equivalent at all -- the enum was rejected, glPrimitiveRestartIndex was a no-op stub, and a strip that should have been cut into pieces was drawn as one continuous run across the whole scene. The custom form is emulated by rewriting the index stream: every index equal to the application's value becomes 0xFFFFFFFF, the stream is drawn as GL_UNSIGNED_INT, and the driver's fixed-index restart is switched on for the duration of the draw and switched back after. Widening to 32 bits is what makes the substitution unambiguous. An 8- or 16-bit source cannot contain 0xFFFFFFFF, so no ordinary index can be mistaken for a restart. A 32-bit source could in principle, but that would mean addressing vertex 4294967295. Covered: glDrawElements, glDrawElementsBaseVertex, glDrawElementsInstanced and the multidraw CPU rebase path. glDrawRangeElementsBaseVertex and the other bare passthroughs in gl_native.cpp are not, and neither is the compute fusion path, which now declines any restart mode rather than only the fixed one. An application that picks exactly the fixed value for its type gets the driver's own implementation with no rewrite, and an index buffer that cannot be read back declines the rewrite and draws without the restarts rather than inventing geometry. The three GLES.glIsEnabled(GL_PRIMITIVE_RESTART_FIXED_INDEX) queries in gl/multidraw.cpp now read the virtual table instead. They have to: the custom form is invisible to the driver, and this emulation toggles the driver's fixed-index flag behind the application's back, so the driver is no longer a truthful source for what the application asked for. The CPU rebase also takes the real restart value rather than assuming the type's fixed sentinel. The scratch index buffer is dropped when the current context changes, alongside the multidraw ones.
…nt hook eglMakeCurrent was a five-line passthrough, so MobileGlues had no way to observe a context switch at all. Everything GL scopes to a context therefore became a process-global singleton, and gl/multidraw.cpp had to poll eglGetCurrentContext() on every draw to approximate the event it could not see. MGContext is created by eglCreateContext, kept by a reference count instead of being dropped when eglDestroyContext returns -- GL permits destroying a context that is still current, so the record has to outlive that call -- and pointed at by a thread-local current pointer that only eglMakeCurrent writes, matching EGL's own per-thread scoping. MGShareGroup is created per context and shared with any context created against it, which is where the objects GL shares across a share group will live. Identity is a monotonic id, never the EGLContext pointer. EGLContext is a driver heap allocation, so destroying one and creating another frequently returns the same address; the existing polling in gl/multidraw.cpp compares addresses and therefore reports "same context" for one that never owned the cached objects. The enable state table from the previous commits is the first resident: it is per-context by definition, so two contexts no longer share one set of enable flags. A context this layer never saw created -- the bootstrap probe context, or one made before the library was loaded -- leaves the current pointer null and every consumer on its fallback, rather than inventing a record with made-up attributes. Everything else is still global. Buffer name mapping, texture metadata, framebuffer tables, gl_state and the multidraw scratch objects will move in the last step; until then multiple contexts still share those. Also fills in the queries the enable table owns: GL_MAX_CLIP_DISTANCES, which GLES has no answer for and which used to leave the caller's variable uninitialised, GL_MAX_VIEWPORTS, reported as 1 because viewport arrays are stubs, and GL_MAX_DRAW_BUFFERS, clamped to what the table can track so an index the application was told it could use is never rejected.
The desktop context gate required the requested version to EQUAL customGLVersion and every flag attribute to be EGL_FALSE. Both are stricter than EGL, and both break the loaders this layer exists to serve: LWJGL and GLFW ask for 3.2 core, which against the default customGLVersion of 4.0 returned EGL_BAD_MATCH and no context at all, and GLFW sets GLFW_OPENGL_FORWARD_COMPAT and GLFW_OPENGL_DEBUG_CONTEXT alongside it. Before the gate existed these attributes were silently ignored; adding it turned them into a hard failure. The version test is now "no higher than configured", which is what EGL and GLX allow -- a context of any version at least as high as the one requested is a valid answer. Debug, forward-compatible and no-error are accepted. They describe what the context promises the application rather than something the backend must implement: a forward-compatible core context simply must not expose deprecated functionality, which this layer does not expose anyway, and debug output is a real GLES 3.2 capability. Robust access is still refused, because EXT_robustness is a behavioural guarantee about out-of-range accesses and claiming it unchecked would be a promise the layer cannot keep. The flags that were accepted are recorded on the context and reported back through glGetIntegerv(GL_CONTEXT_FLAGS), which returned a hardcoded 0. The version is reported per context too, so an application that asked for 3.2 is told 3.2 rather than the configured maximum.
…s, query strings eglGetProcAddress forwarded every name to glXGetProcAddress, which resolves against RTLD_DEFAULT. An EGL name could therefore be answered by the system libEGL before it ever reached this layer, handing the application a driver entry point that bypasses the context records, the virtual attributes and the surface bookkeeping. Under ANGLE it is worse than a bypass: the handles that entry point receives belong to a different EGL implementation than the one holding them. EGL names now resolve through an explicit table of this layer's own wrappers, and an EGL name not in it goes to the backend's own eglGetProcAddress rather than RTLD_DEFAULT. GL names still go through glXGetProcAddress, which applies the multi-draw backend mangling. The seven EGL 1.5 sync and image entry points were never exported, so they always resolved to the driver. They are exported as passthroughs now -- there is no desktop-versus-ES semantics to translate -- purely so the frontend owns them. eglQueryString(EGL_EXTENSIONS) wrote into one thread_local string and returned c_str(). The next query for any display overwrote it in place, which can reallocate, leaving the caller holding freed memory -- and keeping that pointer is the documented contract, the string belongs to EGL and lives as long as the display. It is built once per display and kept. It also advertises EGL_KHR_get_all_proc_addresses, which is true now and is the flag GLFW and LWJGL test to decide they need not dlopen a client library of their own. EGL_CLIENT_APIS reports "OpenGL OpenGL_ES": both are reachable, since an application that binds the ES API still gets the backend unchanged. eglGetConfigAttrib stripped the ES renderable bits when adding the desktop one. That broke the standard "choose configs, then verify each with eglGetConfigAttrib" loop for any application asking for ES: the config it had just been handed came back claiming it could not render ES at all. The config really does both -- desktop GL on it is this layer's virtualisation of the very ES support being reported -- so the bit is added, not substituted. destroy_temp_egl_ctx called eglTerminate on EGL_DEFAULT_DISPLAY. EGL does not reference-count initialisation per caller, so that marked every resource on the display for destruction, including contexts and surfaces the host process created before this library was loaded. The probe now gives back only what it took. FSR1 latched the display and surface into statics on first use, so after a rotation or a surface rebuild it queried a destroyed surface every frame and the render resolution never changed again. It takes both from the swap it is hooked into.
… context gl_state was one process-wide struct, allocated once during bootstrap on the temporary pbuffer context that is destroyed moments later. Every context in the process shared one current program, one active texture unit and one draw framebuffer, so a second context silently inherited the first one's idea of them. It now lives inside MGContext, and eglMakeCurrent repoints the global at the current context's copy -- every gl_state-> reader keeps working unchanged, it simply stops being one set of values for the whole process. gl/multidraw.cpp identified its context by comparing EGLContext pointers. That was the wrong key: EGLContext is a driver heap allocation, so destroying one and creating another very often hands back the same address, and the comparison then reported "same context" for one that had never owned the cached buffers, programs or probe latches. It compares the monotonic MGContext id now. The rest of the process-global state -- the buffer name map, VAO tables, texture metadata, framebuffer tables and the FSR1 objects -- is deliberately still global. Moving it means changing object lifetime across five files at once, and with no device to test on that is not a change worth making blind. What is left is recorded rather than half-done: multiple contexts still share those tables.
Seven defects, five of them introduced by the preceding commits. The multidraw CPU rebase wrote the 0xFFFFFFFF restart sentinel into its index stream and drew it as GL_UNSIGNED_INT, but nothing ever turned the driver's fixed-index restart on. The sentinel was fetched as vertex 4294967295 and every enabled attribute array was read out of bounds. The previous code read GLES.glIsEnabled(GL_PRIMITIVE_RESTART_FIXED_INDEX), which could not disagree with the driver by construction; switching that to the virtual table introduced the gap. Worse, primitive_restart_index defaults to 0, so an application that only called glEnable(GL_PRIMITIVE_RESTART) turned every index 0 into the sentinel. The path now brackets its draws with the driver's flag. An application whose restart index happens to equal the type's fixed value got no restart at all: no rewrite is needed then, but GL_PRIMITIVE_RESTART is never forwarded either -- GLES has no such enum -- so nobody told the driver to restart. The three single-draw entry points now bracket their ordinary draw with the driver's fixed-index flag when that is the case. MGContext was only created on the desktop path, after eglCreateContext returns early for anything that did not call eglBindAPI(EGL_OPENGL_API). On a host that never binds the desktop API -- which is the normal launcher bridge -- no context was ever tracked, g_current_ctx stayed null for the whole process, and multidraw's context change detection short-circuited permanently: stale buffer and program names were reused across a destroy-and-recreate. ES contexts are recorded too now. gl_state was left pointing into a released MGContext. Nothing repointed it when a context was made non-current or when the record was erased, and it is written through on the draw hot path, so the next draw wrote to freed heap. It falls back to a default instance on both paths. GL_CONTEXT_FLAGS reported EGL bit values. The two encodings are opposite -- EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR is 0x1 and FORWARD_COMPATIBLE_BIT is 0x2, while GL_CONTEXT_FLAG_DEBUG_BIT is 0x2 and FORWARD_COMPATIBLE_BIT is 0x1 -- so each flag was reported as the other, which is precisely the confusion the commit that added it claimed to prevent. Translated at the source now. glGetDoublev passed a single stack GLfloat to glGetFloatv. GL_DEPTH_RANGE writes two and GL_VIEWPORT four, so the driver wrote past it, and the remaining components were never filled. It sizes the temporary by pname. The stub this replaced wrote nothing at all, which was useless but memory-safe. mg_restart_invalidate had no caller despite its own comment saying it is called on a context change, so gl/restart.cpp's scratch index buffer -- a real driver name, not a virtual one -- was reused across contexts. It is invalidated with the multidraw scratch objects.
GL_MAX_VIEWPORTS reports 16 rather than 1. GL 4.6 requires at least that many, and an application that reads the number and then indexes up to it now gets consistent answers: GL_SCISSOR_TEST is tracked per viewport, so glEnablei on viewport 3 is remembered and reported instead of rejected. Only viewport 0 reaches the driver, because glViewportIndexedf and glScissorIndexed are still stubs -- the state is real, its effect is not, and the log says so once. EGL_CONTEXT_OPENGL_ROBUST_ACCESS is accepted. It is a guarantee this layer cannot verify, but refusing it stopped context creation outright for loaders that ask for it as a matter of course, which is worse than granting something the driver very likely already provides. The flag is recorded and reported through GL_CONTEXT_FLAGS like the other two. eglTerminate is reference counted per display. EGL does not do this itself: whoever calls it marks every resource on the display for destruction, including resources another part of the process created. The bootstrap probe registers its own hold and releases it, so it terminates only a display it actually brought up -- and an application that calls eglTerminate while the probe still holds the display no longer tears the probe out from under itself.
The last of the process-wide GL state. A second context inherited the first one's buffer names, sizes, vertex array objects, bindings, texture objects, texture units and framebuffer table, because all of it was one set of file-level containers. Split the way GL scopes it. Buffer names, their sizes and texture objects are shared across a share group -- two contexts created against each other really do see one set of names. Vertex array objects, the current buffer bindings, the texture units, the active texture unit and the framebuffer table are container state and belong to one context even inside a share group. The containers stay private to the file that uses them. Moving them into MGContext would drag gl/texture.h and gl/framebuffer.h into egl/context.h and back out again, so each subsystem keeps its own table keyed by id and exposes a bind hook that eglMakeCurrent calls. A thread_local pointer selects the current one; std::unordered_map keeps references stable, so those pointers survive another group being inserted. The ~120 access sites are unchanged, which is the point -- a file-local macro redirects the old name at the new storage rather than rewriting every use and risking a typo in one of them. gl_state is thread_local now too, along with every one of these pointers. EGL scopes the current context per thread, so two threads each holding one must not share them. That was already wrong for gl_state after it was moved into MGContext; it just had no second pointer to be inconsistent with yet. framebuffers was a plain global that gl/gl.cpp reached by extern, which cannot work once it is per-context. It goes through mg_framebuffers() instead.
Every name in FSR1_Context is a GL object owned by whichever context created it, and gl/framebuffer.cpp redirects a bind of framebuffer 0 to g_renderFBO. In a second context that name refers to nothing, or to an unrelated object the application created -- so enabling FSR1 and then using two contexts sent the scene to whatever happened to hold that name. The values are saved and reloaded on a context switch rather than reached through a pointer, because they are declared extern and read from several translation units; swapping the storage would have meant touching every use. g_dirty, g_resolutionChanged and the pending size are deliberately not swapped: they describe work queued for the frame in flight, not the context's objects. This was the last of the process-global GL state.
The library initialises itself from a static constructor, and that constructor now reaches egl/context.cpp through mg_display_initialised. g_contexts and g_display_refs were namespace-scope std::unordered_map, which are dynamically initialised, so whether they existed by then came down to the order the linker emitted the translation units in. It did not. init.cpp's constructor ran first, so the first insertion hit an all-zero map; its zero max_load_factor asked for an infinite bucket count, __next_prime threw std::overflow_error, and the process aborted during dlopen -- before main, before any application code. Every host loading the library would have died on the spot. Constructing on first use removes the ordering question rather than relying on it. Access sites are unchanged. Found by running the branch on a device for the first time.
Running the branch on a Mali-G77 (r32p1) turned up three ways the multi-draw path drew the wrong thing or nothing at all. All three are invisible to a build and to host-side logic tests. glMultiDrawElementsBaseVertexEXT was gated on the resolved symbol alone. It is not part of EXT/OES_draw_elements_base_vertex by itself: both specs define the multi-draw form only when EXT_multi_draw_arrays is also supported, and a driver with base vertex but without multi_draw_arrays is an ordinary configuration. Android's EGL wrapper resolves the symbol anyway, and such a driver accepts the call, draws nothing, and raises no error -- so the probe-and-latch latched Working and every sub-draw was dropped for the rest of the process. On this device that silently killed glMultiDrawElements and glMultiDrawElementsBaseVertex under the Auto setting. The extension string is the only thing that can tell, so it is now checked, in resolution and in the runtime fallback alike. GL_PARAMETER_BUFFER reached GLES verbatim from glBufferData and friends, came back GL_INVALID_ENUM, and left the buffer with no storage -- glMultiDraw*IndirectCount then found a zero-byte parameter buffer and refused to draw. GLES has no such target, so the calls borrow GL_COPY_WRITE_BUFFER for their duration and put it back; GLES defines it as a generic target with no meaning of its own, so nothing observes the swap. glBufferSubData and glGetBufferParameteriv move out of the native pass-through list for the same reason. No multi-draw entry point honoured GL_PRIMITIVE_RESTART. Restart is per-index state and applies to every sub-draw, but GLES only implements the fixed-index form and nothing here accounted for either half: a batch drawn with restart enabled came out with its strips joined end to end. Backends that hand the application's indices to the driver now defer to the one that rewrites the stream when a custom index is in use, and bracket the batch with the driver's fixed-index restart when the chosen value is the fixed one. The indirect entry points can only do the second half and say so once. Verified on device across every backend the driver can reach -- unroll, basevertex, indirect and compute -- each producing an identical, correct picture. multiarrays, multibasevertex and multiindirect now correctly report themselves unsupported here and fall back, so they remain unexercised on this hardware.
glBufferData passed a bind target to find_bound_buffer, which answers the *_BINDING query enums instead. A target falls through its switch and comes back 0 -- a valid buffer name, so nothing complained -- and every size the process ever recorded landed on buffer 0. Nothing reads the table today: get_buffer_data_size has no callers, so this changes no behaviour. It is a trap rather than a bug, and the trap is the pair of near-identical lookups, so the target-side one now exists under its own name with both documented as taking the other's enums badly. glBufferStorage allocates storage the same way and owed the same record; it never made one. Introduced by 77b4357 (2025-09-20), unrelated to the multi-draw work.
Testing the entry points next to the multi-draw ones found four more that were wrong in two already-familiar ways. glDrawRangeElements, glDrawRangeElementsBaseVertex and glDrawElementsInstancedBaseVertex were pass-throughs in gl/gl_native.cpp. They are indexed draws like glDrawElements, but only glDrawElements, glDrawElementsInstanced and glDrawElementsBaseVertex ever applied GL_PRIMITIVE_RESTART; on the other three the application's chosen restart index went to a driver with no such feature and was rasterised as an ordinary vertex, joining every strip in the batch end to end. Measured on device, 1152 of 4096 pixels came out wrong on a nine-index draw. They now use the same mg_draw_elements_restart / fixed-index bracket as their neighbours, via a scoped guard so an early return cannot leave the driver's restart enabled behind the application's back. glDrawArraysInstancedBaseInstance, glDrawElementsInstancedBaseInstance and glDrawElementsInstancedBaseVertexBaseInstance were stubs in gl/gl_stub.cpp: called, they drew nothing at all. GLES has no base instance in core and no extension for it on the drivers this layer targets, but baseinstance is 0 in the overwhelming majority of calls, and with 0 these commands are exactly the ones GLES already implements. They now forward, and a non-zero base instance is reported once and then ignored -- the instanced attribute fetch starts at the wrong element, which is wrong for that case alone and stays visible, where a silently empty screen was neither. All six move into gl/drawing.cpp beside the entry points they share machinery with, and are declared in gl/drawing.h so they keep C linkage. Verified on device: the ten checks that cover them all pass, including restart combined with a base vertex and a non-zero base instance still drawing. The multi-draw, GLSL, enable-table and two-context suites are unchanged at 31 x 8 backends, 11, 102 and 14.
1.3.5 was tagged VERSION_RELEASE, so every build off this branch reported itself as a release. It is not one: the branch carries seven defect fixes verified on exactly one device and one driver, and three multi-draw backends on it have never executed at all. VERSION_DEVELOPMENT makes glGetString(GL_VERSION) report "1.3.6·Dev", which is what a bug report from this build should carry.
Twenty-five commits covering GL 4.6 multi-draw, a virtual enable state table, per-context state, and the EGL frontend. Merged as one unit rather than fast-forwarded so the integration point stays revertible. Multi-draw. All seven GL 4.6 entry points are implemented, including glMultiDraw*IndirectCount, whose command compaction runs on the GPU rather than stalling on a readback. Each entry point gets its own backend setting instead of one shared key, and backends are named after the extensions that actually provide them -- GLES has no native multi-draw at all, only EXT, which the old naming implied otherwise. Silent index truncation on a base vertex past 65535 is fixed by widening the rebased stream to 32 bits. State. glEnable/glDisable/glIsEnabled now answer from one virtual table covering all 28 GL 4.6 capabilities, so the five query entry points can no longer contradict each other on the thirteen GLES lacks. GL_PRIMITIVE_RESTART with a custom index is emulated by rewriting the index stream. Buffer names, texture and framebuffer tables, FSR1 objects and gl_state move per context or per share group, so two contexts stop sharing what GL says they must not. EGL. Context attributes real GL loaders send are accepted instead of rejected, eglGetProcAddress has an explicit name table, sync and image entry points are exported, and eglTerminate is reference counted. Verified on device (Mali-G77 MC9, r32p1, Android 14, arm64-v8a): 168 distinct assertions, 385 executions, all passing. The multi-draw suite runs once per backend and every backend produces a pixel-identical frame. Known not covered: the multiarrays, multibasevertex and multiindirect backends have never executed -- this driver has neither extension, and Auto selects multiindirect on hardware that does. No real application has run against this. One device, one driver; armeabi-v7a and x86_64 build clean but were never executed. Version is marked 1.3.6·Dev accordingly.
…ffle
Both defects this branch had to be told about twice were in code that is pure
enough to run on the host, and neither was caught by reading it. These link the
real translation units -- gl/pixel.cpp and gl/framebuffer.cpp, not copies -- so
they cannot drift away from what they check.
The pixel test walks the rebuilt tables and compares 408 row strides against GL
4.6 sec. 8.4.4.1's own formula, k = n*l or (a/s)*ceil(s*n*l/a), across six
format/type pairs, four alignments and seventeen row lengths, plus the enums that
must NOT be in each table and the degenerate alignments widthalign now guards.
The framebuffer test drives glDrawBuffers against a fake driver that records what
is physically attached where, and asks the only question that matters: when the
application draws through slot j, does the texture it named in bufs[j] receive
the pixels? Seven scenarios, including the two regressions this branch shipped --
identity order after a swap, and the single-target shuffle whose destination slot
was a victim rather than a source. Reverting restore_home_attachments to the
first attempt makes scenario 4 fail and the rest pass, which is what makes the
suite worth having.
sh MobileGlues-cpp/tests/run.sh
Off by default -- MG_EGL_TRACE is 0, and the whole thing compiles out; the binary carries none of its strings. It exists because the EGL wrappers are the seam between the host, this layer's virtual context records and whichever driver is behind them, and nearly everything that goes wrong there is a sequencing problem: which thread made what current, which display is still held by whom, whether a handle the driver just returned is one it handed out before. An argument dump per entry point answers none of that, so every line carries the calling thread and says what came back. LOG_I rather than LOG_D so it can be read from a release build without GLOBAL_DEBUG turning the renderer into a firehose that buries the interesting lines -- which is exactly what happened while hunting the NeoForge early-window black screen, and why this got written.
…d of giving up Xaero's world map rendered black. Its tiles arrive through a pixel unpack buffer, the conversion path has to read that buffer back to repack the pixels, and glMapBufferRange with GL_MAP_READ_BIT was refused -- so the whole upload was dropped and the tile texture was never defined. A mobile driver routinely refuses a read mapping of a buffer whose usage hint is one of the _DRAW ones, and _DRAW is the natural hint for a buffer whose only purpose is uploading. What those drivers do serve is a copy into a buffer asked for with _READ, which is a buffer this layer can make for itself: allocate one, glCopyBufferSubData the range across, map that. Only when the copy is refused too is the upload dropped, and the warning now says which of the two failed. Both exits -- the finished conversion and the overflow drop -- release the source through one function, so neither can forget to unmap or to put the copy-write binding back. Predates this branch: the drop path came in with 96a26ca, an ancestor of it. The sixteen commits before this one are unrelated to the black map, and the same device runs 1.21.4 with Sodium at 92 fps with them in.
… as a pointer Two things the plugin app needs from this side to tell the truth about ANGLE. The resolved AngleMode was the only thing exported, and it cannot answer the question that matters. Under EnableIfPossible on a device the probe rejects it reads Disabled -- indistinguishable from the user not wanting ANGLE at all, so a borrow that was silently ignored looked exactly like a borrow that was never asked for. The raw configured choice and the device verdict ride along now, in the benchmark JSON and through a mg_angle_in_use() the info query can dlsym; the loader is the only honest source for the latter, since a system driver that is itself ANGLE defeats every other test. And the unrolled glMultiDrawElementsBaseVertex read indices[i] as a client pointer whenever no element buffer was bound. A sub-draw's indices there is a buffer offset, and offset zero is a null pointer, so that was a segfault at address zero -- reachable from the in-process benchmark the moment borrowing ANGLE started working, which is to say it had never been reachable before. The binding is what decides which of the two meanings applies, so a zero binding with offset-shaped indices cannot be repaired here: it says so once and skips the sub-draw.
… say so GL_CONTEXT_LOST is not a complaint about the call that returned it. After it, every query answers zero and every draw is a no-op -- so a batch timed past that point is not fast, it is fiction, and the backends that check their preconditions see those zeros and fall back, which this harness wrote down as "this device cannot do it". The result was a confident ranking assembled out of nothing: unroll timing 14 microseconds for work it never did, and half the candidates marked unsupported on hardware that supports them. Caught after the glFinish that ends a batch, so the check costs nothing in the timed region, and it names whoever was being measured -- the one clue to why the driver gave up. On this Mali-G77 under a borrowed ANGLE that is consistently glMultiDrawElementsBaseVertex/indirect. The whole run is then reported as failed rather than partially ranked: what was measured before the loss cannot be told apart from what came after.
…ugh a mapping A map with GL_MAP_INVALIDATE_BUFFER_BIT states the intent more precisely, and on a driver that honours it there is nothing to choose between the two. What it costs is two ways to fail on every draw call: a map that returns null, and an unmap that reports the contents lost. Either one abandoned the backend for the rest of the process, and falling back to unrolled draws forever is far worse than the upload it was avoiding -- a driver declining to hand out a pointer for a buffer it is still reading is ordinary, not a reason to give up on indirect drawing. The staging vector is thread_local for the same reason mg_zero_basevertex is: nothing in this file takes a lock, and two threads can each have a current context.
…mb back A tiler bins every primitive into the tiles it covers, and that list lives in a per-context heap that grows on demand and then stops. Past that point the job does not slow down, it fails: Mali answers BASE_JD_EVENT_OUT_OF_MEMORY, the kernel kills the context, and ANGLE turns the VK_ERROR_DEVICE_LOST into GL_CONTEXT_LOST. A Mali-G77 lost the context on every single run. What was pushing it over was the answer to a shaky pass. Frames-per-batch was multiplied by up to eight per retry, to a ceiling of 64 -- 64 frames of geometry queued between two glFinish calls, on a scene sized at compile time for no device in particular. So the scene size is discovered instead, once per run and never written down. The run opens at 256 sections; a pass that comes back too noisy doubles it, because the steadier reading is the one with more real work behind it. The first lost context ends the climb for good -- the caller comes back in a fresh process with a ceiling, and this run never reaches for it again. Frames-per-batch is left wherever the probe calibrated it: it was the one knob that raised peak load without being subject to any ceiling. At the ceiling the budget grows instead, which buys rounds, which is more samples under the median at a load the device has already survived. Measured on the G77 that used to fail every time: the ladder now climbs to 1024 sections and finishes, and 4096 is fine too. Each entry point reports the scene its numbers came from, since a function that settles early keeps a smaller one.
robin-map 1.4.1 (Tessil/robin-map @ 91362aa, MIT), copied in as headers the way include/ankerl was rather than added as a submodule -- it is four headers and no build system, and the tree already has more submodules than it has dependencies worth tracking that way. LICENSE alongside them.
There were four: a hand-written open addressing map behind the UnorderedMap
alias, ankerl::unordered_dense, std::unordered_map and khash. Four sets of
iterator-invalidation rules to hold in your head while reading code that mixes
them in one function, and no way to tell whether a container had been chosen or
merely copied from the neighbouring file. All of them are robin_map now, and the
two that were dead afterwards -- the FastSTL submodule and the ankerl header --
are gone with them.
The part that is not a rename: five of these tables hand the address of a mapped
value out and keep using it past a later insert, and robin_map is open
addressing, so growing it moves the elements. Those hold a unique_ptr now, and
the pointee is what stays put:
* egl.cpp ext_strings -- eglQueryString hands the application a c_str() that
belongs to the display, and the code says so: it already had this bug once,
with a thread_local string;
* buffer.cpp, texture.cpp, framebuffer.cpp per-context and per-group tables --
a thread_local pointer into an entry is the design, and the ~90 access sites
read through it rather than looking anything up;
* framebuffer.cpp's inner table -- get_framebuffer() returns a reference that
callers hold across further calls that can insert;
* gl.cpp depth-clear objects -- returns a reference, and drops the lock doing
it.
They are per context, looked up once at eglMakeCurrent, so the extra allocation
and the indirection cost nothing measurable. egl/context.h explains where the
guarantee now comes from, since it no longer comes from the map.
Two API differences worth knowing when reading the diff. robin_map stores
pair<Key, T> and hands it out const through operator->, so mutating a mapped
value goes through it.value(); and the default allocator on the alias changes
shape to match.
Also fixed while in DSAWrapper.cpp: restoreTemporaryBufferBinding,
restoreTemporaryFramebufferBinding and restoreTemporaryRenderbufferBinding each
had their early return commented out and then dereferenced an end() iterator on
the very next line. The texture one was fixed earlier; these three were missed.
… not prove Every scenario in this file used one framebuffer name, so the table never grew and nothing exercised a rehash at all. Sixty-four more names take it through several, and fbo 7 still has to know its home attachments afterwards. The comment is explicit that this does not reproduce a reference held across an insert -- the case the unique_ptr in that table exists for. It was written for that and does not do it: it passes with the unique_ptr removed, because no path in framebuffer.cpp keeps a framebuffer_t& while asking for another name. Saying so in the test is better than leaving a name that claims more than it checks.
Same shape as before -- one map everywhere, open addressing, and the unique_ptr where a mapped value's address has to survive an insert -- with tsl::robin_map swapped out for ska::flat_hash_map. The five tables that hold their records by pointer keep doing so: this map moves its elements when it grows too, so the reason has not changed. What does change is how a mapped value is written. robin_map hands the pair out as const through operator-> and makes you go via value(); flat_hash_map's is mutable, so those five sites are back to the ordinary it->second and the comments explaining the detour are gone. The other side of that coin is that it->first is writable as well, which silently corrupts the table -- nothing does it, and includes.h now says so. It is consumed as a submodule of MobileGL-Dev/flat_hash_map rather than copied in, because the header needed a fix to be usable here at all: its prime list is a static constexpr size_t[] two thirds of which is above 2^32, and a braced initialiser makes narrowing an error, so armeabi-v7a and x86 could not include the header -- never mind instantiate the policy, which flat_hash_map does not even use. Upstream last moved in 2018, so the fix lives in the fork and the submodule pins it, instead of a local edit that the next re-vendor would drop.
VERSION_TYPE is not bookkeeping: getter.cpp puts it straight into glGetString(GL_VERSION), which is what a player reads in F3. It said ·Dev3 while the plugin around it had already been named Alpha, so the same build described itself two ways depending on where you looked. VERSION_DEV_NUMBER stays at 3 behind its #if. It is inert now, and if this ever goes back to a development series the next one is 4, not 1.
The Alpha cut is a branch, not the end of development; the performance batch that follows starts a new development series on top of it. Dev numbering continues from where it left off -- the comment in this file says why 4, not 1.
…o-op binds The audit's recurring pattern was code asking the driver for values this layer already knows. This commit builds the supply side: mg_driver_bound_buffer() returns what GLES.glGetIntegerv would for a buffer target, computed from the tracked bindings through the same frontend-to-real mapping glBindBuffer uses; mg_driver_texture_binding*() and mg_driver_active_texture_unit() answer from a new driver-side shadow in texture_ctx_state_t, written wherever this file issues GLES.glBindTexture/glActiveTexture. The texture accessors return bool for a reason: the shadow refuses to answer for the shared fallback record (a context this layer never saw created), and while FSR1 is enabled -- ApplyFSR leaves a binding on unit 0 the guard does not restore, so consumers must keep their glGetIntegerv fallback for the false case. The gates are documented where they live. On top of that supply: glBindTexture and glActiveTexture now skip the driver call when both the frontend object and the driver-side name already match -- the bookkeeping below still runs -- and glCopyTexSubImage2D reads the recorded internal format instead of a per-call glGetTexLevelParameteriv, falling back to the driver only for an object it never saw.
glEnable/glDisable forwarded to the driver even when the shadow already held the requested value; Minecraft toggles BLEND/DEPTH_TEST/CULL_FACE thousands of times a frame and most of them are repeats. The filter checks the shadow first and is gated on the context actually being tracked. The 27-entry linear find_cap scan -- which also sat under every glGetIntegerv default branch -- is now a switch.
…ampler-buffer setup The glDrawElementsBaseVertex emulation created and destroyed a buffer object on every draw call and read-mapped the source IBO -- a CPU-GPU sync per draw, times one draw per chunk section under Sodium. It now uses a persistent scratch IBO with the same context-change handling as gl/restart.cpp's, and a grow-only thread_local staging vector in place of the per-call malloc. prepareForDraw's sampler-buffer half did three hash lookups per indexed draw -- one of them operator[], which inserts on miss -- and asked the driver for unit 15's texture binding it had itself established. Lookups are find() now, the binding comes from the tracked state (with the glGetIntegerv fallback the accessor's contract requires), and the resolved program state is cached behind an identity check that survives program-name recycling: same map entry, same address, same name, or it rebuilds.
… driver what we know Every temporarilyBind/restore pair paid a malloc and a hash insert because the stack entry was erased the moment it emptied -- and DSA calls do not nest, so that was every call. Empty entries now keep their vector and its capacity; the map holds a handful of targets, so nothing grows without bound. The saved binding comes from the tracked state instead of a glGetIntegerv round-trip, with the fallback the texture accessor's contract requires. glBindTextureUnit no longer queries GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS -- a process constant -- per call, reads the active unit from the shadow, and skips the glActiveTexture pair when the unit is already current. glBindTextures (multi_bind) reads the tracked active unit too. GetTexTarget is computed once per operation and threaded through instead of three times. mgRejectUnknownTexture fired two log lines per DSA call on a name this layer never saw; it now latches per name, consistent with the file's WARN_ONCE style.
…atch vectors The indirect paths asked the driver for GL_ELEMENT_ARRAY_BUFFER_BINDING and GL_DRAW_INDIRECT_BUFFER_BINDING on every call -- two to five round-trips per multi-draw -- although buffer.cpp tracks both. Every site that later feeds the value back to GLES.glBindBuffer now calls mg_driver_bound_buffer() instead, and each reads it at function entry, before the backends' own GLES-direct rebinding makes the shadow momentarily stale. Queries for state this layer does not track stay on the driver. The per-call std::vector in mg_draw_elements_restart -- value-initialised, zeroed, then fully overwritten -- and the one in the drawelements backend are now the same grow-only thread_local scratch the prepare_indirect_buffer path already used.
…warning get_framebuffer used operator[] on every glBindFramebuffer, walking the insertion path even for names it had seen a thousand times; it probes with find() first now, and the unique_ptr-held records keep their address stability either way. The unrecorded-slot warning in glDrawBuffers fired every call while its condition held -- a logcat syscall and an fflush per pass under Iris -- and now latches like every other warning in this file.
ApplyFSR queried three uniform locations by name every frame; they are fixed at link and now live in the FSR state. The state guard's eight glGetIntegerv reads shrink to the ones this layer genuinely does not track, and it now survives RecreateFSRFBO deleting the framebuffer it had snapshotted: recreation rewrites the guard's saved names and republishes the tracked draw-FBO field, so the guard always restores a live name -- the old code restored a deleted one after any resolution change. The redundant per-frame glViewport is gone with the cached surface size.
put() rewrote the whole cache file on every insert -- O(n^2) disk writes while a shader pack loads its hundreds of shaders, felt as world-load stutter. Saves are amortised now (dirty counter plus a coarse monotonic-clock threshold), the on-disk format is unchanged, and the crash window is explicitly bounded to the last few entries -- a cold recompile, not a corrupt cache. SHA-256 no longer copies the full source into a scratch vector, and the digest get() computes rides along to put(), so each compile hashes its source once instead of twice.
… driver The converted-upload path opened with six glGetIntegerv calls for unpack pixel-store state and the unpack-buffer binding. The pixel-store shadow that already carried the GLES-absent parameters now carries the GLES-native unpack set too, initialised to the spec defaults and written where glPixelStorei passes through; the buffer binding comes from mg_driver_bound_buffer(). Only the read side moves -- the restore half still talks to the driver, since it is driver state being put back. Plain RGBA uploads never entered this path and are unaffected.
The type and the GL_VERSION branch for it have existed all along; this flips the switch. The Dev4 performance batch is what graduates.
Beta is a branch now -- the plugin's beta branch pins the commit that flipped the type, and that is where 2.0.0.Beta lives. This branch keeps developing, so it keeps saying so.
The frontend entry points are exported, so an internal call to one of them went through a preemptible PLT slot -- and in a game process the system libGLESv2 is in the global symbol scope and can win that lookup. The DSA wrappers call the frontend glBindBuffer and glBindTexture by design; resolved to the driver they would bypass every bit of state tracking, and nothing would say so. Five such relocations existed; -Bsymbolic-functions leaves none. The symbols stay exported, so dlsym and eglGetProcAddress are unaffected. Attached to the target rather than to the compiler-id block above it, because that block's condition is `MATCHES "GNU|Clang" AND NOT MATCHES "AppleClang"` -- the second half has no variable and the whole thing has never been true, which is separately why -O3 and --gc-sections never reached a build. Left as found; turning those on changes the shipped binary and is not this commit's business.
The harness created its buffers, VAO, textures and framebuffer with GLES.* directly. That was invisible while the multidraw backends asked the driver for the current bindings, and became a wrong benchmark the moment they started reading the frontend's tracked state instead: the tracking had never heard of these names, so the indirect paths saw "no element buffer bound" and fell back, and the unroll paths skipped every sub-draw -- glMultiDrawElementsBaseVertex reported unroll at 30us against basevertex at 3813us and would have been adopted, a ranking won by drawing nothing. Every call that establishes tracked state now goes through the frontend. The backends are still invoked directly: what this harness bypasses is the dispatcher's choice of backend, never the state machine. Shaders and the program stay on GLES.* deliberately -- these sources are ESSL and the frontend's shader path is a desktop-GLSL-to-ESSL translator -- and that is safe because no backend consults tracked program state. It also measures the right thing now. The game reaches these entry points through the frontend, so a frame that pays the frontend's bind path is the frame being predicted, and every candidate pays it alike.
Whether the tracked state still describes the driver cannot be decided at run time: the shadow always has an answer and has no way to know it is stale, and the only thing that could tell it is the driver round-trip these accessors exist to remove. So the check moves to where it can be paid. Under GLOBAL_DEBUG both accessors ask the driver as well and log the disagreement with the target, the tracked name and the driver's; release builds trust the tracking, unchanged. This is what the benchmark needed to fail loudly instead of quietly: it would have printed tracked 0, driver 7 on the first frame rather than surfacing three calls later as a skipped sub-draw. The texture check covers the active unit only -- reaching another unit means switching the active one, which is exactly the disturbance a verification pass must not cause.
…thing else Two holes the benchmark had, both found by asking what it actually proves. It never ended a frame. Rendering goes to an FBO, so no swap ever closes the pass, and a glClear does not close one that is open -- so a whole batch merged into a single render pass. That is a shape no game produces: the per-pass work is paid once instead of once per frame, and the tiler bins the batch's entire geometry into one polygon list. bench_frame_end unbinds the framebuffer, which is the boundary that cannot be deferred, and flushes. Neither call waits, so the CPU/GPU overlap the batch timing depends on survives. (The scene ladder's comment now says its 512-section measurement predates this and that the ceiling sits higher with per-frame passes.) It never checked that the backends drew the same picture. Five backends of one entry point are five implementations of one draw call, so with the camera pinned they must agree bit for bit -- and when one silently skips work, the ranking is won by drawing nothing. That is not hypothetical: glMultiDrawElementsBaseVertex reported unroll at 30us against basevertex at 3813us, and it would have been adopted. Each candidate now renders one fixed-camera frame at probe time, the readback is hashed, and a candidate whose hash differs from the reference (unroll where it exists, the simplest semantics) is named in the log, recorded in quality.wrongOutput and excluded from the ranking. GL_DITHER goes off with it: it is the one default-on state that can perturb identical draws into different bytes. The readback is a full sync, which is why it happens once per candidate at probe time and never inside a timed batch.
The type reaches players through glGetString(GL_VERSION), and the release case is the one the branch chain in getter.cpp has no arm for -- so the string is just the number, with nothing appended. That is the intent: a release does not announce that it is one. VERSION_DEV_NUMBER stays at 4 behind its #if, inert. If a development series follows this, the next one is 5.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.