Wave 2 of the advertised-extension conformance campaign.
PROGRAM INTERFACE QUERIES (the load-bearing piece). glGetProgramInterfaceiv
and the five glGetProgramResource* entry points were answered by the
BACKENDS - Espryt asked the real driver about SPIRV-Cross-generated ESSL
whose namespace is not the GL one (default-block uniforms live in
MGL_GLOBAL_UBO there), and Magma kept a second, partial reflection that
hardcoded types and diverged from the frontend. Both are now deleted; a new
frontend resource-model layer (ProgramInterface.{h,cpp}) answers every
interface - uniforms, uniform blocks, atomic-counter buffers (recovered
from glslang's synthesized gl_AtomicCounterBlock_<binding> lowering),
buffer variables, shader-storage blocks (classified by TType storage
qualifier since glslang reflects them as uniform blocks), program inputs/
outputs (built-ins' layoutLocationEnd sentinel mapped to -1), and
transform-feedback varyings including the gl_NextBuffer/gl_SkipComponentsN
pseudo-varyings - from the glslang reflection the frontend already trusts
for glGetActiveUniform. Name/index round-tripping, the "[0]" array
spelling, and the GL 4.6 table 7.2 prop/error matrix live in the new layer
only; GetActiveUniform*/GetActiveAttrib* are untouched.
glShaderStorageBlockBinding now takes the interface-layer index (the one
GetProgramResourceIndex returns, with a range check it never had), records
the binding on the program keyed by block NAME - the one coordinate all
three index spaces agree on - and delegates by name across the backend
boundary. Both backends reseed the recorded bindings on their own program
rebuilds, so an unrelated resync can no longer silently revert a rebound
block, and GL_BUFFER_BINDING reports the live binding, not the declared
one. The Espryt delegate applies only to an already-synced twin and can no
longer trigger SyncToBackend from a getter; the sync path's GL query
out-params are initialized and clamped (a load-dependent stack-garbage
Vector size crash caught by the gate, reproduced 3/50 pre-fix, 100/100
post-fix under saturating load).
ESPRYT RENDER-STATE SHADOW RESET (rides along because it shares
DirectGLES.cpp): the render-state shadow is file-static and survives
MobileGL context switches, so GL_FRAMEBUFFER_SRGB (and the whole synced-cap
class) leaked between contexts - the cross-test leakage class the CTS maps
have carried for a week. MakeCurrent now invalidates the shadow like it
already invalidates the program/FBO/buffer caches, and the resync resolves
a never-set scissor box to the current surface instead of pushing the
(0,0,0,0) sentinel verbatim (which scissored everything away - caught by
the retrace gate, bisected to the exact field via a bitmask probe, and
fixed by resolving like the viewport path rather than reverting).
Frontend riders exposed by the layer: glGetUniformLocation resolves
arrays-of-arrays element addressing ("a[2][1]"); transform-feedback capture
accepts element-addressed varying names ("b[1]") and snapshots the request
verbatim for the interface (Magma's decorate pass logs loudly that element
capture is unimplemented there - follow-up).
KNOWN GAPS, documented in code and tests: the 6 subroutines-* cases
(glslang refuses subroutine for SPIR-V; wave 3), the 5 separate-programs-*
cases (glslang's pipe-I/O reflection cannot see a separable non-vertex
stage's own inputs; needs stage-aware output validation first), and
uniform-block-types' per-instance stage masks (not derivable from the
reflection).
Gate: 593/593 unit at default and kill-switch, x10 each, plus the SSB race
case 100/100 under 20-way CPU load; ext caselist Espryt 77.87% -> 80.42%,
Magma 77.58% -> 79.39% (+212 fixed, 0 newly broken); program_interface_query
2/43 -> 31/43 unique on Espryt, 9/43 -> 31/43 on Magma, backends now
byte-identical; KHR-GL45.direct_state_access 370/371 + 371/371 with the 4
sRGB leak victims recovered in cross-test ordering; KHR-GL33 held at
9884/9886; full 39x2 CI retrace with zero wave-attributable failures (the
3 failing newly-added fixtures are bit-identical on the pristine baseline).
First wave of the advertised-extension CTS campaign (targeted caselist: the
glcts groups of every extension both backends advertise, 4867 cases across the
KHR-GL41..46 namespaces). All frontend, shared by both backends:
- Non-square float matrix uniforms actually upload: glUniformMatrix{2x3,3x2,
2x4,4x2,3x4,4x3}fv and the six glProgramUniformMatrix* twins were
validate-only no-ops; they now write column-at-a-time at the global UBO's
16-byte std140 column stride, honouring transpose. glUniformMatrix2fv had
the sibling bug - mat2 written as 4 contiguous floats put column 1 at byte
8 instead of 16. The readback path only ever un-padded mat3, so
glGetUniformfv is fixed for mat2, mat3x2 (previously mis-gathered) and
every non-square shape, with the bounds check widened to the padded span.
- glBindBufferRange validates offset/size at last: size <= 0, offset < 0,
SSBO and UBO offset alignment, transform-feedback offset AND size
multiples of 4 - all before any state write (a negative offset used to
reach Range1D unchecked). glBindBuffersRange inherits per element, with
the ARB_multi_bind up-front [first, first+count) checks added to the
BindBuffersBase/Range and BindSamplers prologues.
- BufferSubData's second, wrong mapped-overlap test deleted (it rejected
every write at or after a mapped range's start, mapped or not); the state
layer's assert relaxed to the same half-open intersection the frontend
checks. BufferStorage error precedence fixed: no-bound-buffer now beats
bad-size/flags.
- glSamplerParameteri accepts the full GL_NEVER..GL_ALWAYS compare-func
range (NEVER/LESS/EQUAL were rejected by a wrong lower bound).
glBindSampler's unit gate uses GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS instead
of the frontend array capacity, shared with glBindSamplers by construction.
- Getters: GL_MAX_SHADER_STORAGE_BLOCK_SIZE in glGetIntegerv; atomic-counter
buffer limits; all 11 per-unit GL_TEXTURE_BINDING_* plus GL_SAMPLER_BINDING
in glGetIntegeri_v; GL_VERTEX_ATTRIB_BINDING/_RELATIVE_OFFSET across the
vertex-attrib query family; glGetFloati_v/glGetDoublei_v implemented (were
stubs); KHR_debug limits raised to spec floors.
- glCreateShader records INVALID_ENUM for an unknown type (it previously
handed out a usable name with no error at all); glCreateShaderProgramv
validates count up front. glDispatchCompute/Indirect validate work-group
counts, offset alignment and indirect-buffer presence.
- glVertexAttribIFormat & friends take a positive integer-type whitelist -
GL_FLOAT/GL_HALF_FLOAT/GL_DOUBLE/GL_FIXED no longer slip through as
integer attributes.
Gate (headless Mesa, default config = async on): 570/570 unit at default and
with the kill switch; ext caselist Espryt 76.29% -> 77.87% (+81 fixed, 6
crashes -> 0, the whole list now runs in one glcts process), Magma 75.94% ->
77.58% (+80 fixed, 0 newly broken); KHR-GL33 full mustpass lost nothing
(9884/9886, the 2 known Mesa-drift failures); retrace smoke clean (the
bsl-GLES miss is the documented golden drift, bit-identical on the pristine
baseline). The 4 DirectGLES direct_state_access.renderbuffers_storage* cases
that turned red are a PRE-EXISTING GL_FRAMEBUFFER_SRGB cross-test leak,
A/B-proven on an unpatched 2e6fc1ff build - wave 1 removed the two accidental
maskers (a crash partition and a failing case whose error path reset the
state). Fixing the leak itself is queued.
A destroyed VertexArrayObject's heap address is handed straight back by the
next allocation of its size, and so is a destroyed BufferObject's. DirectVulkan
keyed its per-VAO draw memo on the VAO POINTER and folded the bound buffer's
ADDRESS into the content hash that validates the memoised bindings, so a
delete/recreate pair under a byte-identical attribute layout reproduced both
the key and its validating hash at once. The successor VAO then inherited the
dead one's resolved bindings and the draw fetched from a destroyed VkBuffer.
Both stated defences failed together, because both reduce to the content hash
and the hash's buffer-identity component was itself a recycled address.
VertexArrayObject and BufferObject now carry a globally-unique, never-reused
GetLifetimeId() - the same contract as ProgramObject's, minted from an atomic
starting at 1 so a zero-initialised slot can never name a live object.
VaoDrawMemo matches on (address, lifetime id) and stores the id on recycle,
SetupDrawSnapshot's "the VAO did not move" test compares the id alongside the
config version, and VertexInputStateFactory::ComputeHash hashes the bound
buffer's id instead of its pointer (0 for client memory).
Proven: the use-after-free reproduces at 100% incidence headless on lavapipe,
including a SEGV whose backtrace is the driver dereferencing a destroyed vertex
buffer inside lvp_queue_submit, and it is gone with the fix. New coverage -
MG_Test/State/ObjectLifetimeIdTest (deterministic, GPU-free, no context: it
waits for the real allocator to repeat an address and asserts the id differs,
and skips loudly rather than passing quietly if it never gets the chance), and
MG_IntegrationTest XfbAfterClipDistanceScenario, registered for DirectGLES,
DirectVulkan, and a third DirectVulkan run with async shader compilation pinned
on because that is a second allocation pattern. Gates: 553/553 unit green at
async=0 and async=1; the scenario 5/5 headless at both flag states; 71/72 CI
trace-replay fixtures over both backends, the one failure a pre-existing
lavapipe crash proven not a regression (identical SIGSEGV at the identical
call number under the pre-fix library).
Pending NVIDIA/X11 confirmation: the KHR-GL{32,40} transform_feedback failures
that opened this investigation never reproduced on lavapipe - the -2/-101
pre-fill signature appears in zero pre-fix runs there - so whether this clears
them is UNPROVEN and must be re-measured on the NVIDIA rig against a freshly
re-run pre-fix baseline. The residual suspect is deliberately untouched here:
m_xfbCounterSlotByObject keys its counter slot on the raw GL transform-feedback
name, so a recycled name whose generation check happens to pass would RESUME
instead of BEGIN. That path was never exercised on lavapipe and is neither
confirmed nor exonerated.
~21% of a shaderpack's glCompileShader calls hand different shader objects
byte-identical source; the P0b cache only helps after one finishes, so under
async two workers would run the whole pipeline twice. Now the GL thread
consults a per-context (stage, hash, length, envFingerprint) -> weak-node
map at enqueue and ADOPTS the in-flight (or completed) node instead of
posting a duplicate - a hit is honored only after a full byte comparison
(the hash never decides), a cancel-requested or settled-cancelled node is
never adopted, and no worker ever waits.
Sharing a node makes the unconditional cancel wrong, so release is now
adopter-counted: a plain GL-thread Int (every mutation site is a GL entry
point; the single-threadedness argument and the terminal-early-out that
keeps the count exact are in the header), and the cancel fires only at
count zero AND with no pending link pinning the node (the stage-4
MarkLinkReferenced precedence). Adoption also re-points the object's source
at the node's snapshot so the layer-1 memo's pointer compare stays armed -
without that, an adopter's next glCompileShader would re-enqueue the very
duplicate this stage removes. Both guards are negative-control-proven: each
removed guard fails exactly its own tests. Count discipline was proven with
a temporary hard-abort on underflow/leak across the full suite and retrace
corpus - zero hits.
18 new tests (13 GL-surface incl. shared-node re-source/delete/orphan-sweep
isolation, shared failure logs, 48-over-6 stress with a deterministic
adoption count, flag-off and KHR-suspended zero-adoption guards; 5 direct
map cases incl. fingerprint mismatch and cancelled/expired pruning).
Gates: 538/538 unit both flag states, async suites x5 no flakes, NVIDIA
DirectGLES retrace identical sets both states. Timing: 2-worker
(Android-shaped) 1-3% faster consistently on complementary and BSL;
4-worker unchanged - the win this stage exists for lands where CPU is
scarce.
GL_COMPLETION_STATUS_KHR in both object getters, reading the non-joining
node-direct state - the one query that must never block is asserted never
to reach a join gate. glMaxShaderCompilerThreadsKHR/ARB share one
implementation: a zero count suspends async FIRST and then joins every
outstanding compile and link this context owns (suspend-before-join is the
only order whose post-condition is 'nothing in flight'), a nonzero count
restores; the suspension is a process latch the extension controls, kept
distinct from the configuration flag that gates the ADVERTISEMENT - an app
that turned threading off has not made the extension disappear.
GL_MAX_SHADER_COMPILER_THREADS_KHR reports the thread count. DriverPost
gains the MobileGL-side async row (PASS/INFO naming the env knob) and an
informational host-driver row backed by a new GLES capability probe.
The extension string itself lands per backend in the two follow-up
commits, keeping this one green stand-alone.
glLinkProgram with the flag on snapshots its inputs in a GL-thread prologue
(stage-sorted shaders with their compile nodes taken without joining, env,
explicit locations/fragdata/xfb, draw-buffer count), then runs the whole
link body - glslang link/mapIO, SPIR-V, reflection, routing tables - as a
ProgramLinkTask that auto-posts when its last compile dependency settles
(+1-guarded countdown; no worker ever waits on another job). The publish is
one move of the LinkArtifacts block at the join, with the second version
bump so nothing memoized during the pending window survives.
The consume-once TShader claim moved onto the shared compile node as a CAS:
two link jobs racing for one shader resolve to winner-takes-the-parse,
loser re-parses the preprocessed source against the node's own env -
identical SPIR-V pinned by test for 2 and for 12 sharing programs.
Two deliberate corrections to the design's cancel matrix, both test-proven:
attach/detach do NOT cancel a pending link (the snapshot isolates it, and
glCreateShaderProgramv's link-then-detach would otherwise discard its own
result before anyone read it); and a compile node a pending link depends on
is pinned against the orphan-name sweep - the ordinary LWJGL teardown
compile/attach/link/detach/delete used to cancel the dependency and turn a
must-pass link into GL_FALSE.
Continuations are now throw-contained per-item (a stage-3 leftover made
load-bearing by the first real continuation), and the review's deadlock
find is fixed: the dispatch loop no longer cancels a node while holding the
pool mutex, since that cancel can run OnDepSettled -> Post -> same mutex.
Explicit joins: the draw path (GetProgramForDraw, both the pipeline stage
loop and the plain-UseProgram half) and the composite-link site; destroy
paths cancel-not-join; COMPLETION_STATUS readers stay non-joining.
Gates: 506/506 unit both flag states; AsyncCompile/AsyncLink/AsyncTeardown
suites x10 repeats clean both states (teardown with 128 jobs in flight,
then re-Initialize); full NVIDIA DirectGLES retrace flag on twice - result
sets identical to flag off, zero new deltas. Compile-phase prefix-diff,
flag on vs off: complementary-reimagined 5.21s -> 2.16s, BSL 1.72s ->
0.90s - past the design's final acceptance targets before the KHR
extension is even advertised. Default remains OFF until stage 5+7.
glCompileShader with MOBILEGL_ASYNC_SHADER_COMPILE=1 snapshots its inputs on
the GL thread (source SharedPtr, CompileEnv, cache handle) and runs the whole
pure pipeline - preprocess, validators, extractors, glslang parse - as a
ShaderCompileTask on the worker pool, returning immediately. Every read of
compile-produced state joins through the single Compiled() gate; links stay
synchronous this stage and join their attached shaders at the top of the
body. Flag off, the path is the same code run inline.
Mechanics: the job node owns all its inputs (no back-pointer, no lifetime
tie to the shader object), so re-sourcing or deleting a pending shader is
cancel-and-drop, never a wait; glslang worker hygiene is a TLS-allocator
scope guard plus GL-thread builtin prewarm (gated on the flag, latch reset
on Destroy so re-initialization re-warms); worker-side diagnostics defer
through the job and replay on the GL thread at the join, enforced by
IsPoolThread asserts in RecordError and an empty-deferred-errors tripwire.
A body that throws publishes a COMPLETE failed compile (status false, real
info log) rather than an abandoned node, and never memoizes away the retry;
a failed enqueue (OOM) cancels the node instead of stranding the joiner -
including inside the dispatch loop, where the in-flight slot is repaid.
The pool StopAndDrains from an atexit sentinel too: workers still inside
glslang parse while exit() ran static destructors was a real 2-in-5 SIGSEGV,
reproduced and fixed (15/15 clean after).
Backend-internal shader objects (default FS, DirectVulkan blit/mipmap) are
cache-less and always compile inline - compile-and-read-in-one-breath needs
no round trip.
Gates: unit suite 488/488 with the flag off AND on (x5); AsyncCompileTest
(12 e2e cases: pending re-source/delete/recompile, byte-identical failure
logs across modes, 48-compile cache stress) x10 repeats clean both modes;
full NVIDIA DirectGLES retrace identical result sets flag off/on (zero new
deltas); compile-phase timing flat as designed (links still serial - the
parallel win arrives with stage 4's async link + stage 5's
KHR_parallel_shader_compile).
Still fully synchronous - EnsureLinkJoined()/EnsureCompileJoined() are empty
inline no-ops (verified to fold away at every one of the ~1200 call sites;
this project builds without LTO) - but every read of link- or compile-produced
state now goes through a private accessor the compiler enforces, so when
stage 4 moves the bodies onto pool workers, 'which reads must join' is a
type-system fact instead of a 400-line audit.
- ProgramObject: the 31 fields ResetLinkArtifacts clears plus the 5 link
outputs it forgot (infoLog, linkedFragData{Location,Index}, the geometry
strip-capture pair) move into a nested LinkArtifacts behind Artifacts().
ResetLinkArtifacts is now a worker-safe pure clear; the link-observable
version bumps (backendState/link/uboContent) move to a GL-thread-only
BumpLinkObservableVersions() called once from Link()'s prologue and from
glProgramBinary's mandated failure - the link body never writes them, so
a stage-4 worker cannot lose an invalidation against the draw path.
- ShaderObject: compile artifacts (TShader, preprocessed source, side-channel
maps, status/log, consume-once flag) behind Compiled(); the P0b layer-1
memo trio deliberately stays outside as the future non-joining
COMPLETION_STATUS_KHR fast path.
- CompileEnv (new): a GL-thread snapshot of everything the compile pipeline
used to read live from the backend mid-parse - compute limits (the
GetIntegeri_v reach-back is gone from the worker path), advertised
extensions, device quirks, TBuiltInResource inputs. Captured lazily per
backend activation; the consume-once re-parse now runs against the same
env as the original parse.
- The GL-thread prologue / worker-body boundary is marked in Link() where
the stage sort ends; everything below is a pure function of the snapshot.
Public getter signatures unchanged - MG_Impl and both backends compile
untouched. Unit 476/476, Program suites 117/117, DirectGLES retrace 38/39 on
llvmpipe (the one failure is the known pre-existing non-CI iterationrp case;
the NVIDIA userspace driver was updated out from under the running kernel
module mid-session, so GLX there is down until a reboot).
- SpvcSession's move constructor and move assignment dropped the parsed
metadata, so a moved-to session silently reported empty reflection.
- ParseComputeLocalSize used std::stoull, whose std::out_of_range escaped
glCompileShader on an oversized local_size literal; now std::from_chars
saturating to UINT_MAX, pinned by a regression test that reproduced the
escaping exception.
- The compute local_size std::regex was rebuilt on every compile; hoisted.
- LinkProgram dumped every shader's full source through MGLOG_D per link.
- glslang::FinalizeProcess ran before the GL context tore down, leaving the
context's live TShaders pointing at freed builtin symbol tables.
Iris-style packs hand MobileGL the same source text repeatedly: probed across
three shaderpack traces, 28-32% of all glCompileShader work was redundant -
~9% same-object recompiles with byte-identical source, ~21% distinct shader
objects sharing identical source (the same common GLSL chunk glued into many
program stages). Two layers, both keyed by XXH64 + length with a full byte
compare on every hit (correctness never rides on the hash):
- Per-object: a successful (or failed) compile remembers its source hash;
glShaderSource with byte-identical text keeps the compiled state and
glCompileShader on unchanged source returns immediately. Deterministic
(stage, source) pipeline makes the memo observationally identical to
recompiling; the consume-once TakeShaderForLink re-parse path is untouched.
- Cross-object: a per-context bounded cache (ProgramState-owned, declared to
outlive every shader object) shares the preprocessed source, both explicit
side-channel maps, and the validation verdicts between objects with equal
source; only the glslang parse stays per-object. Single-GL-thread today;
flagged for a mutex when compiles go async (P1).
Interleaved A/B on the iterationrp trace (the recompile-heavy pack):
5.65s -> 5.46s median total replay, every round faster; BSL/complementary
stay flat (their duplicate sources are the small common shaders, so calls
drop but wall time is parse-bound on unique sources). Full DirectGLES
retrace, 445-test unit suite, and dedupe-semantics tests (no-op recompile,
invalidation on new source, failed-compile memo, cache bounds) all green.
glCompileShader used to parse every source twice: once under the GL client
(reflection only) and once under the relaxed Vulkan client (SPIR-V + the
plain-uniform global UBO), with GenerateBinary re-preprocessing, re-parsing
and re-linking every attached shader on every glLinkProgram. The GL-client
pass is gone: Compile() performs the one link-compatible relaxed parse and
the linked TProgram serves reflection and codegen both. Measured on the BSL
shaderpack compile phase: Espryt 2.80s -> 2.14s, Magma 3.78s -> 3.07s.
What the relaxed parse cannot provide is restored explicitly:
- explicit layout(location/binding) qualifiers on default-block uniforms and
samplers are extracted lexically at Compile() (the relaxed parse strips
them) and merged per link with cross-stage conflict checks;
- uniforms the relaxed parse sweeps into MGL_GLOBAL_UBO but no stage reads
are filtered from the GL reflection surface through GL<->TProgram index
translation maps (dead uniforms stay inactive, the synthesized block stays
hidden, builtins reflect under their GL spellings);
- SPIR-V is generated BEFORE buildReflection touches the program (its
live-variable analysis perturbs GlslangToSpv output - generated modules
stay bit-identical to the old pipeline's), while the glUniform*-to-scratch
routing tables are built strictly AFTER reflection, whose results size and
key them;
- a TShader feeds exactly one link (mapIO mutates the intermediate); relinks
and multi-program attachments re-parse the stored preprocessed source.
Validated: DirectGLES retrace suite green (two pre-existing local-driver
failures unchanged old vs new), KHR-GL30 877/878 on Espryt/NVIDIA (the one
failure pre-exists this change), unit tests green, per-module SPIR-V hashes
identical across a full DirectVulkan replay.
GL 3.3 core 2.11 puts program and shader names in one name space: a shader
name passed where a program is expected must fail with INVALID_OPERATION,
and vice versa. Two independent IndexGenerators handed out colliding names
(shader 2 and program 2 could coexist), so CheckProgramNameValidity resolved
a shader handle to an unrelated linked program and the error checks in
KHR-GL30.get_uniform_tests.get_uniform were silently swallowed - the case
only ever passed because the collided program happened to reject the queried
location. One shared generator keeps the names disjoint; the per-kind object
tables are unchanged.
A Minecraft frame updates ~95 scattered 16x16 sprites in a 1024x512
atlas; MipmapStorage's single union dirty box turned ~95KB of changed
texels into a ~2MB upload on every backend. The storage now keeps a
bounded (96-slot) list of pairwise-disjoint dirty rects BEHIND the
untouched union box: rects cascade-merge on touch or overlap, overflow
folds the pair with minimum enlargement and re-cascades, whole-level
dirties and respecifies just clear the list (empty list = "union box
tells all"). GetDirtyRects hands the list out only when it has 2+
rects, fits the caller's capacity, and its summed area is under 75% of
the union box - fewer driver calls beat equal bytes - so consumers can
never stage more than the union box did.
The list is maintained inside the same four mutation funnels every
texel writer already goes through (MarkDirty, MarkDirtyRegion,
AllocateLevel, TruncateToLevelCount - callers enumerated at the
declaration), so list and union box cannot disagree. Backends OPT IN:
the union-box API and its update order are byte-identical, and an
unmodified backend keeps rendering exactly as before.
96 slots is measured, not guessed: on the bench's 95-sprite lattice a
16-slot list collapses to >93% of the union box, 96 slots reach 4.8%
(~2MB -> ~95KB staged per frame). Verified by a 2859-check fuzz run
against a reference dirty bitmap (union exactness, full coverage,
disjointness, bounds, profitability). Unit tests 421/421.
perf annotate put 94% of VertexArrayState::Bind's 10.5% self time on the
two lock-prefixed shared_ptr refcount RMWs each bind performs. The bound
VAO is now stored as a slot index into m_vertexArrays - no SharedPtr
copy, no atomics on the bind path. The lifetime invariant (the bound
object is kept alive by its slot; any cold path that clobbers a bound
slot - delete-while-bound including slot 0, create-over-bound-slot -
detaches the old object into m_boundDetached so GetBoundVertexArray
keeps answering with it) is enforced in MarkVertexArrayForDeletion /
CreateVertexArrayObject rather than assumed, and documented at the
change. Out-of-range binds and null slots keep their exact old
semantics.
VertexArrayObject also gains two opaque config-version-guarded backend
aux memo words, letting a backend answer "same vertex-input layout?"
from the frontend object instead of chasing its own cold cache entry.
After this change the frontend Bind drops out of the DirectVulkan draw
profile entirely (11.5% -> 0.5%). Measured jointly with the two backend
rounds that land on top: quiet-box 6-round order-alternating A/B,
all nine cases, no case worse than noise on either backend. Unit tests
421/421.
DirectGLES re-derived the whole texture binding state for every draw: for each
touched unit, two alias-resolution passes over all binding slots, then a third
walk to unbind native targets nothing claimed, then the sampler. With the
Minecraft-shaped bench that was 13.2% of the render thread in BindCurrentTextures
alone, plus 4.6% in SyncNeccessaryTextures deciding which textures to consider.
The answer is identical across a whole terrain batch.
The resolution is now memoised, and what makes replaying it as a no-op legitimate
is that the memo does not merely trust a key: it compares the backend's own bound
texture shadow against the one resolution left behind. Every path that binds a
texture behind this function's back already maintains that shadow - the scratch
bind an upload does on the temp unit, CopyTexSubImage2D and GenerateMipmap
binding on the active unit, the glBindTextures fast path, the scrub a backend
texture performs when it is destroyed or respecified - so a memcmp catches all of
them without having to enumerate them. On top of that the key covers the texture
bind generation, the program that arbitrates aliased targets (pointer, lifetime
id, backend state version, link status), and the ES context generation.
Two invalidation sources had no signal at all and needed one. Mipmap completeness
decides whether a texture is bound in the first place, and it moves with texture
shape and with the effective sampler's filter - so a sampling-resolution
generation now moves with both, routed through single choke points
(TextureObjectBase::BumpShapeVersion, SamplerObject::BumpVersion) so a future
bump site cannot forget it. A texture context id was needed because both
generations restart at zero in a new GLContext, which can land on the old heap
address.
This also closes a pre-existing hole rather than working around it:
glDeleteSamplers unbinds the sampler from every unit straight through
TextureUnit::SetSamplerObject, bypassing the touch bookkeeping, so that setter now
bumps the bind generation on a real change. The sampler bind step itself stays
outside the memo and runs every draw - the program's raw-depth-fetch substitution
rewrites unit samplers immediately afterwards, so a memo there could never hit.
ns per draw, DriverBench on a GTX 1660 SUPER (native / Espryt):
mc_vanilla_draw 253 / 2037->1315, mc_ubo_range 202 / 1684->955,
mc_sodium_multidraw 739 / 3939->3150. Espryt goes from 8.3x to 4.7x the native
driver on the per-draw uniform-range case. Magma is unaffected (the MG_State
additions are counter bumps), and no case regressed.
Unit tests 421/421.
Every draw asks, for every bound texture, whether it is mipmap-complete for the
filter in use, and the answer was recomputed from scratch each time: walk the
level chain, read each level's texel size, verify each is half the previous.
With the Minecraft-shaped bench that walk plus the GetTexelSize calls under it
measured about 8% of the render thread on both backends.
The answer depends only on the texture's shape - internal format, stored level
set, level sizes, level range - and never on its texel content, which is the
thing that actually changes between draws. A shape version now moves on exactly
those four mutations (SetInternalFormat, SetBaseLevel/SetMaxLevel, and the
AllocateStorage/TruncateMipmapLevels pair on both mipmap storage classes), and
the completeness answer is memoised against it, one slot for the mipmapped
question and one for the plain one. An upload leaves the memo standing, which is
the whole point; anything that could change the answer invalidates it.
ns per draw, DriverBench on a GTX 1660 SUPER (native / Espryt / Magma):
mc_vanilla_draw 257 / 2201->2037 / 1550->1346, mc_ubo_range 203 / 1832->1684 /
1089->934, mc_sampler_churn 272 / 2349->2325 / 1533->1396. Texture-upload cases
are unchanged, as expected - they were never asking this question in a loop.
Unit tests 421/421.
A per-draw CPU profile of a real Minecraft frame (perf on the render thread,
which sits at 100% of one core on both backends) said the deficit is translation
overhead, not the GPU, and named where it goes. This removes the largest items
it found, on both backends and in the shared frontend they both feed.
The single biggest one was not translation at all: IsBackendContextCurrentOnThisThread
called eglGetCurrentContext on every invocation, and glvnd answers that with a
getpid() fork check - a real syscall. The predicate sits two and three deep in
every draw (the deferred-release drain, the global-UBO ring availability check,
and the ring allocation), so it accounted for 16.3% of the render thread. EGL is
still the ground truth, but re-verifying it once per thread per frame catches an
external migration at the next frame boundary rather than the next call, which
recovers the same bookkeeping.
Texture uploads now carry a dirty region instead of a per-level flag. Minecraft
animates atlas sprites with 16x16 glTexSubImage2D calls into a 1024x512 atlas
and respecifies the lightmap every frame; a per-level flag turned each of those
into a full-level re-upload - about 3.6 MB a frame of texels nobody changed.
MipmapStorage accumulates the written box, Espryt uploads it with
UNPACK_ROW_LENGTH striding into the level shadow, and Magma stages just that box.
The box is a union, not a range list: repeated writes to one level widen it and
it degrades to exactly the old whole-level upload, which is the honest worst case.
glBufferData(NULL) is the orphaning idiom, and the backend was answering it by
uploading the stale CPU shadow - turning a rename the driver does for free into
a full synchronized upload. BufferObject now records that a NULL respecify leaves
the store undefined, and the upload is skipped until content is actually written.
The rest are smaller and of a kind: the deferred-release queue is probed without
taking its mutex, the UBO ring waits on the frame fence that frees the space it
needs instead of draining the whole pipeline with glFinish at the size cap, VAO
binds go through a shadow so a draw's second bind of the same object does not
reach the driver, the per-draw clean-texture probe short-circuits on the content
version before rebuilding shape info, glUniform drops byte-identical writes
(which otherwise dirty the whole UBO for the next draw), re-binding the texture
or VAO a slot already holds no longer bumps the generation counters a backend
fast path is keyed on, and the texture validators stopped taking shared_ptr by
value.
On Magma: descriptor-set reuse keeps four entries instead of one, because draws
alternating between two programs - the chunk/entity ping-pong - thrashed a single
slot into a full re-allocate and re-write every draw; a DynamicDraw buffer whose
contents survive two frame boundaries is promoted to resident storage instead of
being re-copied into the per-frame arena forever; and sampled-read barriers name
only the shader stages whose device feature is enabled, which also removes a
latent VUID violation (ALL_GRAPHICS names geometry and tessellation stages a
device need not have).
Measured with the Minecraft rig (render distance 32, p50 fps, same machine,
single sample each): vanilla 1.21.1 Espryt 10.8 -> 36.3 and Magma 31.3 -> 44.6;
26.2 snapshot Magma 114.5 -> 210.5. Fabric+Sodium moved inside noise on Magma
(854 -> 766) with the native baseline itself moving 838 -> 1031 between the two
sessions, so treat that cell as unresolved rather than a regression measured.
Unit tests 421/421. The CTS A/B was not run: these numbers and the test suite are
the whole of the evidence, and a conformance regression would not have been
caught here.
RenderState kept one version counter for all render state, and DirectVulkan read
it in three places: the pipeline memo key, the SetupDrawSnapshot fast-path guard,
and that guard's store. So glViewport, glScissor, glBlendColor, glStencilMask,
glClearColor, glPolygonOffset, glLineWidth and the point-size family - none of
which can alter a VkPipeline, all of which an application changes between draws -
knocked the next draw off both fast paths and made it rebuild a pipeline lookup
that was already correct.
The counter is now split. m_version still moves on every state change, because
the draw snapshot really does depend on all of it. m_pipelineStateVersion moves
only for the state a backend bakes into a pipeline object, and it is what the
three DirectVulkan sites read.
The exclusion list is the eight VkDynamicState entries PipelineFactory declares
plus the state that is not pipeline state at all (the clear values, hints, the
point-size family, clamp read colour, the primitive restart index). glStencilFunc
is the one setter that had to be split rather than classified: Func is in the
pipeline payload but Ref and ValueMask are dynamic state, so it bumps the
pipeline version only when Func actually changes.
Capabilities are deliberately NOT in the exclusion list even though several look
like dynamic state: GL_FRAMEBUFFER_SRGB feeds the render-pass hash, depth and
stencil test feed drawUsesDepthStencil, and scissor test, blend, cull face,
polygon offset fill, primitive restart, colour logic op and rasterizer discard
all feed the pipeline payload.
Two smaller draw-path wins ride along, both removing work whose answer was
already in hand. UploadAndBindVertexStreams searched all 32 VAO attribute slots
for the SharedPtr matching a binding's buffer key, once per binding per draw -
but VertexInputStateFactory writes bindingBufferKeys[b] and
bindingAttributeLocations[b] from the same loop iteration, one binding per
attribute with no merging, so the attribute at that location IS the buffer, by
construction. UploadAndBindIndexBuffer round-tripped the element-array buffer's
raw pointer back through the GL name table on every indexed draw, costing a map
lookup and an atomic refcount pair, when the binding slot's SharedPtr was already
in scope forty lines above - where a comment says exactly that about the vertex
path.
Behaviour-neutral by construction and verified as such: a 13355-case subset of
GL30-GL45 covering viewport, scissor, blend, stencil, depth, polygon offset,
clear, multisample, cull, logic op, line width and point state, plus the whole
direct_state_access suite, is identical before and after on both backends - in
the failure list and in the crashed-case set. direct_state_access stays at
Espryt 370/371 and Magma 371/371.
glCompressedTexImage2D rejected every internalformat with GL_INVALID_ENUM, so
direct_state_access.textures_get_image threw at its first compressed call and
reported InternalError with nothing in the log at all - the uncompressed half of
the case had already passed.
The compressed bytes are now kept verbatim, in a side-channel beside the texel
shadow rather than in place of it. That placement is the load-bearing decision:
both backends pair MapMipmapData with GetMipmapByteSize while sizing their copy
regions from GetMipmapTexelSize, and DirectGLES additionally divides the byte
size by the texel count to recover bytes-per-texel, so putting 16 bytes where a
4x4 RGBA8 extent says 64 would be an out-of-bounds read on both. The texel
storage therefore stays uncompressed and correctly sized - the image samples as
zeros, which is the same deviation the RGTC/BPTC/ETC2 arms of
ConvertGLEnumToTextureInternalFormat already document - while
glGetCompressedTexImage returns the image *as stored*, which GL 4.6 core 8.11
requires and which no re-encode could satisfy byte for byte. Nothing ever hands
the compressed bytes to GLES or Vulkan, so the shadow is authoritative rather
than potentially stale, which is why the readback never asks a backend.
The accepted set is exactly the RGTC/BPTC/ETC2-EAC formats core GL requires, and
it is deliberately the same set ConvertGLEnumToTextureInternalFormat can back
with uncompressed storage, so the upload can never accept a format whose texel
shadow it cannot allocate. imageSize is checked against the block arithmetic,
which is also what keeps the copy in bounds.
Three things the shape depends on. AllocateStorage clears the compressed tag, so
a glTexImage2D or glTexStorage2D over the level un-compresses it - without that,
textures_compressed_subimage would flip branches and start asking for data
MobileGL cannot produce. GL_TEXTURE_COMPRESSED and
GL_TEXTURE_COMPRESSED_IMAGE_SIZE are answered per level rather than per texture,
because a compressed internalformat handed to glTexImage2D resolves to
uncompressed storage and must keep reading as uncompressed. And
GL_TEXTURE_INTERNAL_FORMAT now reports the compressed token for such a level, or
it would claim GL_RGBA8 while GL_TEXTURE_COMPRESSED said true.
Still rejected on purpose: glCompressedTexImage1D/3D and every
glCompressedTexSubImage*, which caps the blast radius.
Fixes textures_get_image on both backends (Espryt 370/371, Magma 369/371). A/B
over a 1210-case compressed/texture-storage/texture-view/buffer-storage subset of
KHR-GL45 is identical before and after on both backends but for
get_texture_sub_image.errors_test, which stops throwing and fails on a value
instead.
glVertexAttribLFormat validated its arguments and then refused unconditionally
with "64-bit vertex attributes are not supported", so
direct_state_access.vertex_arrays_attribute_format failed every GL_DOUBLE
subcase on both backends - the format never landed, the draw fetched whatever
the attribute held before, and the captured values came back as reinterpreted
garbage.
The attribute is now real state. IsLong is its own bit rather than being
inferred from Float64, because glVertexAttribFormat(GL_DOUBLE) also reads
doubles - it just asks for them converted to float - so the type alone cannot
tell the two apart. It participates in the format comparison, so an L-format
call over a plain one still bumps the version, and glVertexAttribPointer clears
it inside the mutation block so the clear and the bump stay atomic.
GL_VERTEX_ATTRIB_ARRAY_LONG stops being hardcoded false, and the pname is now
accepted by the attribute queries at all.
Support is detected, never assumed. SupportsFloat64VertexAttributes comes from
VkPhysicalDeviceFeatures::shaderFloat64 on DirectVulkan and is false on
DirectGLES - not a driver question there and never will be, since ES has no
GL_DOUBLE vertex format and ESSL has no fp64 type to consume one with. A backend
without it declines in the entry point, with the GL error and a log line naming
the reason, rather than accepting state no draw could honour. Both cases get a
DriverPost row so the loss is named at startup instead of at draw setup.
On DirectVulkan the attribute deliberately does not use VK_FORMAT_R64*_SFLOAT:
those are optional and lavapipe advertises zero features for all four of them.
It is fetched as its 32-bit word pair (R32G32_UINT / R32G32B32A32_UINT) and
bitcast back to double in the shader by a new SPIR-V pass, which is bit-exact
and needs no format capability at all. The pass re-declares the input as uvec2 /
uvec4, demotes the original variable to a Private global and seeds it once at
the top of the entry point, so every existing load keeps its id and its double
type and no other instruction is rewritten. Both halves branch on nothing but
"is this attribute long", so they cannot disagree - and if the pass ever fails,
the assertion fires rather than letting a UINT format sit under a double input.
The pointer types are all created before any variable that names them and the
demoted variable is moved after them, since the types-and-variables section may
not forward-reference a type.
dvec3/dvec4 are declined rather than fetched wrong: six or eight uint32
components have no single VkFormat, and GL spreads such an input over two
attribute locations, which the location-per-index model here does not express.
Fixes vertex_arrays_attribute_format on Magma (369/371). On Espryt it stays
failing, now as a detected and explained decline rather than a blanket refusal.
The pipeline object bookkeeping landed already - names, stage slots, queries -
but nothing consumed it. Every draw asked the context for the current program,
got null because a pipeline is used with program zero, and drew nothing;
glCreateShaderProgramv was still a stub returning zero, so
direct_state_access.program_pipelines_functional could not even build its stage
programs and reported InternalError on both backends.
glCreateShaderProgramv is written as the exact call sequence the spec defines it
to be, with one deviation that matters: the link goes straight to
ProgramObject::Link(false) rather than through LinkProgram, because LinkProgram
injects a default fragment shader into a program that has none - correct for a
whole program, wrong for a separable vertex-stage one whose fragment stage comes
from the pipeline. glDetachShader defers removal to the next link, so the program
keeps the shader object it was built from while correctly no longer reporting it
attached. GL_PROGRAM_SEPARABLE joins glProgramParameteri and glGetProgramiv.
Everything downstream of a draw - both backends, the uniform plumbing, the draw
validation - is written against one linked program, so rather than teach all of
it about stages, the pipeline is flattened: GetProgramForDraw() composites the
stage programs' shaders into a single hidden program object and caches it against
a signature of each stage program's lifetime id and link generation, so it is
rebuilt exactly when a stage or a stage's link changes. The composite carries no
GL name - it must not answer glIsProgram, and it must not consume a name the
application could be handed.
Uniform entry points get their own resolver rather than sharing that one:
glUniform* addresses the pipeline's active program, not the composited draw
program. GL_CURRENT_PROGRAM still reads the program in use, which is zero here.
Fixes program_pipelines_functional on both backends.
ResolveXfbSymbolType accepted only float, int and uint, and its caller reports
anything it rejects as "Transform feedback varying 'x' is not an output of the
vertex stage" - which is a misleading thing to say about a varying that is right
there in the shader, just declared `double`. Program linkage failed outright.
Doubles are now resolved to the GL_DOUBLE* types, in vector and matrix form, and
the per-element size is computed from an 8-byte component rather than a hardcoded
4 (GL 4.6 core 11.1.2.1), so the byte-based limit checks charge a double what GL
says it costs.
direct_state_access.vertex_arrays_attribute_format stops throwing on both
backends and fails on the captured values instead: the capture layout still owes
the 8-byte alignment doubles require, and neither backend feeds a 64-bit vertex
attribute yet - DirectGLES cannot at all, ESSL having no double.
Every program pipeline entry point was an export stub, and the stub macro's
`return (type)1` made glIsProgramPipeline answer GL_TRUE for anything - including
the names glGenProgramPipelines had never written. All four
direct_state_access.program_pipelines cases failed.
ProgramPipelineObject holds what GL 4.6 core 7.4 says a pipeline is: a program
reference per shader stage, the active program glProgramUniform* addresses, a
validate status and an info log. Its validate status starts false, unlike
ProgramObject's, because a pipeline that has never been validated must report
GL_VALIDATE_STATUS as 0.
The name rules follow the shape queries and transform feedbacks already use, and
which the CTS checks first: glGenProgramPipelines only RESERVES a name and
glIsProgramPipeline answers GL_FALSE for it; the object appears on first bind, or
immediately from glCreateProgramPipelines. Map membership is object existence -
a pipeline, unlike a transform feedback, has no stateful default object zero, so
no everBound flag is needed.
glGet(GL_PROGRAM_PIPELINE_BINDING) reports the real binding now instead of a
hardcoded zero whose comment said the entry points were stubbed.
This is the state half only. program_pipelines_functional needs mixed-stage
rendering - a vertex-only and a fragment-only program drawn together - and stays
failing; glCreateShaderProgramv is deliberately left stubbed until that lands, so
nothing can half-work in between.
Takes program_pipelines_creation, _defaults and _errors from failing to passing
on both backends.
glFramebufferParameteri, glGetFramebufferParameteriv and their two by-name
siblings were all export stubs - the GL_ARB_framebuffer_no_attachments entry
points. The stub raises no error and writes nothing, so
direct_state_access.framebuffers_get_parameter_errors saw GL_NO_ERROR for all
three conditions it checks.
FramebufferObject gains the five DEFAULT_* parameters as real state, initialised
to GL 4.6 core table 23.24 and bumping the object version on a write like the
read buffer does. The getter answers those plus the six derived names -
GL_SAMPLES and GL_SAMPLE_BUFFERS from the attachments' sample counts,
GL_IMPLEMENTATION_COLOR_READ_FORMAT/_TYPE from the read buffer's internal format,
GL_DOUBLEBUFFER true only for the window-system framebuffer, GL_STEREO false
because stereo surfaces are not exposed - which is what glGetIntegerv already
reports for the bound framebuffer.
The pname rules live in ValidateFramebufferParameterPname, and their ORDER is
load-bearing: a name outside the table is INVALID_ENUM, and only a name that IS
in the table but that the default framebuffer cannot answer is INVALID_OPERATION.
Testing the framebuffer kind first would answer INVALID_ENUM for
GL_FRAMEBUFFER_DEFAULT_WIDTH on framebuffer zero, which is exactly the third
thing the case checks. The by-name forms take zero as the default framebuffer,
like the other DSA framebuffer entry points.
Rendering to a framebuffer with no attachments is deliberately NOT enabled by
this: CheckCompleteness still reports INCOMPLETE_MISSING_ATTACHMENT, because no
backend can rasterize one. The state is real and the queries are honest; the
draw path is a separate piece of work.
Takes framebuffers_get_parameter_errors from failing to passing on both backends,
with framebuffers_get_parameters - which passed only because both getters were
stubs leaving the CTS's zero-initialised comparands untouched - still passing.
glGetSamplerParameterfv(sampler, GL_TEXTURE_BORDER_COLOR) raised INVALID_ENUM,
because MobileGL kept the border colour on the texture object and
GetSamplerParam_State had no case for it at all. That is the first thing
direct_state_access.samplers_defaults asks, so the case threw before reaching
any of the defaults it was written to check.
GL 4.6 core table 23.18 lists TEXTURE_BORDER_COLOR as sampler state, so it moves
to SamplerParameters and TextureObjectBase reaches it through the SamplerObject
it already owns - one source of truth, and a sampler object bound over a texture
now supplies its own border colour, which is what GL says should happen. The
texture params version still moves on a write, because the DirectGLES texture
sync memoises on it. glSamplerParameter{fv,Iiv,Iuiv} and their getters read and
write all four components in whichever representation the caller used, and the
three representations are kept in step so any getter has an answer. The bogus
[0,1] and [0,255] range checks are gone: GL clamps a border colour when a
fixed-point format is sampled, it does not reject it.
DirectVulkan's ResolveVkBorderColor now reads the sampler rather than the
texture. DirectGLES gained a glSamplerParameterfv in its sampler sync, and both
that and the pre-existing glTexParameterfv are gated on a new
SupportsTextureBorderClamp capability - ES 3.2 core, or EXT/OES_texture_border_clamp
before it - since without the extension every such call is INVALID_ENUM on the
driver. DriverPost gains the matching row per the POST rule, saying what a user
actually loses when it is missing.
Takes direct_state_access.samplers_defaults from failing to passing on both
backends.
SamplerParameters defaulted compareFunc to ALWAYS, but GL 4.6 core table 23.18
and GLES 3.2 table 21.16 both say the initial value is LEQUAL - for sampler
objects and for the sampler state a texture object carries alike. Every freshly
created texture and sampler therefore answered GL_ALWAYS to
glGetTextureParameteriv(GL_TEXTURE_COMPARE_FUNC).
The Vulkan backend had been papering over it: ResolveCompareFunc substituted
LESS_EQUAL whenever a depth texture was sampled in compare mode and the func
still read ALWAYS, which fixed the rendering but also made an explicitly
requested GL_ALWAYS unreachable. With the default corrected that special case is
both unnecessary and wrong, so it is gone and the compare op is taken straight
from the sampler.
Takes direct_state_access.textures_defaults from failing to passing on both
backends.
glGetTexParameter and its by-name form rejected several parameters GL 4.6 core table 8.20
lists, with INVALID_ENUM as if the application had made them up. GL_DEPTH_STENCIL_TEXTURE_MODE
was the worst of them: the float setter accepted it, validated it and then threw the value
away, the integer setter did not accept it at all, and neither getter could report it - so
the mode could be set and never read back, and setting it through glTextureParameteri was an
error.
It is real state now, defaulting to DEPTH_COMPONENT, set by both setters and readable from
both getters. GL_TEXTURE_LOD_BIAS was in the same position: settable, not gettable.
The by-name getters reach the target-based ones through a temporary binding rather than the
per-object path, so both had to learn these; the per-object path gained the swizzle
components, the target, the image format compatibility type and the texture-view parameters
at the same time, since they were missing there for the same reason.
direct_state_access.textures_get_set_parameter passes on both backends, and textures_defaults
stops raising an internal error and reports an ordinary failure it can be diagnosed from.
glCreateTransformFeedbacks, glTransformFeedbackBufferBase, glTransformFeedbackBufferRange
and the three glGetTransformFeedback* queries were all stubs, so a transform feedback
object could only be configured and inspected by binding it first - the exact thing
direct state access exists to avoid. The queries were the worse half: they returned
nothing and raised no error, so an application could not tell that it had learned
nothing.
glCreateTransformFeedbacks creates the objects outright. glGenTransformFeedbacks only
reserves names, and a reserved name becomes an object when it is first bound
(GL 4.6 core 13.2.1); the DSA form has no bind step to create them from.
The queries and the buffer bindings read and write a named object's state. That state
lives in two places: the context keeps one live copy of the capture bindings and the
active/paused flags for whichever object is bound, and every other object's copy sits in
its saved state until a bind swaps it in. The by-name accessors added to the context
resolve that, so a query for the bound object reads the live copy rather than a stale
save.
GL_TRANSFORM_FEEDBACK_BUFFER_START and _SIZE are answered as zero unless the binding was
made by the range form, matching what the buffer object binding points already do.
Takes direct_state_access.xfb_* from 0 to 4 of 5 on both backends; xfb_functional still
fails on the capture itself, which is a separate defect.
The binding-point half of ARB_vertex_attrib_binding was implemented, but nothing
outside it could see the result. glGetIntegerv answered GL_MAX_VERTEX_ATTRIB_BINDINGS,
GL_MAX_VERTEX_ATTRIB_RELATIVE_OFFSET and GL_MAX_VERTEX_ATTRIB_STRIDE with a hardcoded
0 and a comment saying the entry points were stubs, which they no longer are. An
application that sizes its loops off those limits therefore saw none, and every
"bindingindex must be less than MAX_VERTEX_ATTRIB_BINDINGS" check silently accepted
everything because the limit it validated against was not the one it reported.
The indexed getters answer GL_VERTEX_BINDING_{BUFFER,DIVISOR,OFFSET,STRIDE} from the
bound vertex array now, and the non-indexed getter reports them as indexed-only rather
than returning a fabricated 0.
glVertexAttribPointer is defined in terms of the binding model: it also points the
attribute at its own binding point and gives that point the buffer, the pointer as the
offset and the effective (never zero) stride. MobileGL resolved the pointer form
straight into the flat attribute view and left the binding point untouched, so
GL_VERTEX_BINDING_OFFSET read back 0 for every attribute set up the classic way. The
flat view keeps the raw stride, because GL_VERTEX_ATTRIB_ARRAY_STRIDE reports that
argument verbatim, so the binding point is recorded alongside it rather than resolved
from it. glVertexAttribDivisor likewise now moves the binding point's divisor.
The by-name entry points reject vertex array 0. MobileGL keeps a real object at index 0
for the compatibility paths, so the name validation used to let the default vertex array
through a direct-state-access call that has no such thing.
glVertexAttribFormat and friends validated with the pointer-only subset, which reports
GL_BGRA as an out-of-range size instead of applying the BGRA rules, and never saw
relativeoffset at all. They share the full format validation now, which also grew the
GL_UNSIGNED_INT_10F_11F_11F_REV rules - that type has no DataType of its own, so it has
to be recognised before the conversion turns it into Unknown and reports the wrong error.
glVertexAttribLFormat and glVertexArrayAttribLFormat were stubs. They validate their
arguments now and then report that 64-bit vertex attributes are unsupported, which is
honest; silently accepting a format that can never be used is not.
Takes direct_state_access.vertex_arrays_* from 12 to 17 of 19 on both backends.