mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
libfork-async-compile
100
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3025284a6e |
[Perf, Test] (MG_Util): libfork execution engine for the shader compile pool, runtime-selectable
Adds libfork v3.8.0 (3rdparty submodule, header-only, wired like the asio
precedent) as a second execution engine behind ShaderCompilePool, selected
per process by MOBILEGL_ASYNC_POOL=asio|libfork (default asio; unknown
values warn and fall back). The engine boundary is deliberately tiny: the
queue, the concurrency budget and its clamping, the suspension latch,
cancel request-vs-outcome, the stopped-is-synchronous fallback and the
drain all stay in the shared Impl - an engine only answers how a
budget-cleared job reaches a worker.
The libfork engine runs detached root tasks as CHAINS: a finished body
takes the next queued job in the same coroutine on the same worker, so
the refills a worker posts are absorbed without a scheduler round trip
(the naive dispatch-thread shape measured 4x worse than asio on short
jobs). Absorption is bounded at one job per live chain - unbounded
absorption serialized bursts posted from inside the pool, which is the
shipped shape (one compile settling fans out link jobs via SubmitAfter
and the adoption map), caught by the review and pinned by a permanent
peak-concurrency regression test (pre-fix: libfork peak 1 vs asio peak 4
on a 16-job worker-posted burst). External submissions go through a
round-robin adaptor instead of lf::lazy_pool::schedule, which both
avoids a data race on lazy_pool's unsynchronized xoshiro under
concurrent submits and beats birthday-collision placement by ~1.3x at
budget == thread count.
The measured answer to "does asio scale poorly": no - the executor was
never the bottleneck. On real pack corpora extracted from the trace
fixtures (BSL 61 shaders, Complementary 277), interleaved best-of-5 per
cell, the engines are within noise of each other at every thread count
(complementary: 4965/2506/1376/824 ms at 1/2/4/8 threads for asio;
libfork within 1%), both ~6x at 8 threads. perf counters show the
flattening past 4 threads is machine-level (instructions flat at 22.1e9
from 1 to 16 threads - no added work, no lock spinning - while cycles
and LLC misses double: memory-stall bound), and the separating control
- N fully independent single-threaded processes with no shared
scheduler at all - scales WORSE than the pool (5.27x vs 5.94x at 8).
The pool microbenchmark does favor libfork on pure dispatch (518 vs
530 ns/job at 1 worker, growing with worker count), but a real compile
body is 1-100 ms, so dispatch is under 0.1% either way. asio therefore
stays the default; this branch exists to make the comparison
reproducible (MG_Test/Util/AsyncPoolBench drives either engine over a
corpus directory) and to keep the alternative viable.
613/613 unit tests in all four combos ({asio, libfork} x {async default
on, kill switch}), integration scenarios byte-identical between engines
on both backends.
|
||
|
|
d8d7530011 |
[Fix, Test] (MG_Backend/DirectGLES, MG_Util, MG_Impl): widen three-channel render targets wherever the driver refuses them
Complementary Reimagined would not load through Espryt on Mali: Iris got GL_FRAMEBUFFER_UNSUPPORTED building its composite framebuffer, because colortex1 is RGB8_SNORM and colortex2 is RGB16F - three-channel formats that no real ES driver can render to (EXT_render_snorm covers R/RG/RGBA only, and the float extensions exclude the RGB forms). The frontend's probe cache diagnosed this correctly and then had nothing to offer: the NoThreeChannelRenderTarget widening machinery existed but was gated to multisample targets alone. llvmpipe turns out to refuse most of the same attachments - CI retrace stayed green only because a replay never branches on glCheckFramebufferStatus - so this was never a desktop-vs- device split, just an unlit path. The widening now applies to every color-attachable image, renderbuffers included, riding the driver-probe branch so the native format is still tried first and substituted only on refusal. One ThreeChannelWidening table owns the widened (internalformat, format, type) triple per source format - the previous per-case branches disagreed with each other and could emit an unuploadable (RGBA16F, GL_RGB, GL_BYTE) combination or widen into another three-channel format the driver refuses just the same. Uploads repack three-component client data to four with the format's own one in the alpha channel (127 is not 1 for RGB8I - the integer arms carry integer ones); readback drops the synthetic alpha, derived from the actual image being read, not the bound framebuffer, so glGetTexImage through a scratch FBO cannot be confused by an unrelated widened attachment. Stored alpha on a widened attachment is now an invariant 1.0 rather than an accident: the color-mask sync clears the alpha bit per draw buffer (glColorMaski for MRT mixes), and clears route through glClearBufferfv with alpha substituted on widened slots only - scissored clears inherit the discipline for free, integer color buffers keep their explicit integer-clear path, and glGet still answers the application's own mask. GL_DST_ALPHA blending, blits and readback therefore all see 1.0 without further interception. DriverPost grows the rows this bug earned: EXT_color_buffer_float detection (previously unreferenced anywhere) with a FAIL row when absent, the missing EXT_render_snorm row, and a three-channel- attachment row that reports one representative per widening class - graded so a half-float-only driver warns about the 32-bit float gap instead of being declared unsupported. Gates: 606/606 unit at default and with the async kill switch; full retrace, both backends - the complementary fixtures now run with the widening ACTIVE on llvmpipe and pass with a slightly better SSIM than before; ext caselist DirectGLES holds 3914/4867 with zero set drift while 54 cases move from NotSupported to genuinely passing; on the Mali-G77 device, Complementary Reimagined builds its pipeline and renders in-world through Espryt (md5-verified build), BSL still green. A new ThreeChannelAttachmentScenario pins the frontend answer - COMPLETE where it used to say UNSUPPORTED - on the real driver. |
||
|
|
0f394fa46f |
[Test] (tools/trace_replay): pin the Iris glyph-death bug with a BSL pause-menu fixture
minecraft-1.21.4-fabric-iris-bsl-esc-menu-854: Minecraft 1.21.4 Fabric + Sodium + Iris + BSL 10.1.3 through Espryt on Mali-G77, paused at the ESC menu over the BSL-blurred world, captured at 854x480 through the FCL trace-capture skill. The trimmed trace keeps the whole session preamble deliberately: the |
||
|
|
107669b3db |
[Fix, Test] (MG_Impl, MG_Backend/DirectGLES): DSA by-name texture calls corrupted borrowed-slot memo pairings - process-wide glyph death under Iris
Field report: on Espryt with a BSL Iris pipeline built, every glyph in the game died - HUD, menu labels, even the vanilla title screen after leaving the world - while sprites kept rendering. Captured on-device (FCL apitrace rig), reproduced headlessly on llvmpipe, and pinned with a three-way replay: the same trace renders full text on raw Mesa desktop GL and on Magma, so the stream was intact and the execution was Espryt's. MECHANISM. WithTemporarilyBoundNamedTexture implements the by-name (DSA) texture entry points by binding the named texture onto the active unit's real slot, running the bound-texture code, and restoring - without moving the texture bind generation on either edge. DirectGLES's per-draw texture sync memo keys on that generation and BORROWS the slot pointer, so a memo built for texture A kept passing every key while a by-name call had texture B sitting in the slot: A's backend twin was driven with B's frontend object, and SyncMipmapsToBackend re-specified A's storage with B's shape. In the trace, a by-name upload to a BSL 2048x2048 map while the 16x16 lightmap was bound re-specified the lightmap's GL texture 2048x2048-NULL and back 16x16-NULL. The lightmap exists only as render output - no glTexSubImage2D ever touches it - so it stayed zero forever, and rendertype_text (vertexColor = Color * texelFetch(lightmap, ...)), alpha-discards every glyph. Background quads never sample the lightmap, which is why only text died. FIX, class-level, two layers: - Frontend (shared, closes the same hole for DirectVulkan's generation- keyed memos): the temporary bind and the restore each bump the texture bind generation (only when the slot actually changed), and the restore is an RAII scope guard so a throwing body can no longer leak the temporary binding - a second latent bug of the same class. Deliberately a generation bump and not a touched-unit note: the high-water mark must not chase by-name calls, and a completed bind/restore pair leaves the content epoch unchanged, so the cost is an owner-compare re-walk, not a memo rebuild. - DirectGLES defense in depth: both borrowed-pair memos (g_unitTextureSyncList, g_fboTextureSyncList) record which frontend texture each backend twin was paired with and re-check it before any replay (last in the key conjunction, behind the context-id compare). A stale pairing now costs a list rebuild instead of silent cross-texture storage corruption. Tests, both red with their own layer reverted: TextureTest.NamedTextureCallKeepsUnitBindingAccountingCoherent (the accounting contract) and DirectGLESTextureSync.UnitMemoRefusesToDriveA- TwinFromAnotherTexture (the corrupting sequence shape against a mock GLES table, asserting the resident texture's storage is never re-specified). 595/595 unit at default and with the async kill switch. Replay evidence: the captured BSL ESC-menu trace renders all text through Espryt post-fix, byte-comparable to the Mesa-direct and Magma replays; the no-shaderpack control is unchanged. A trace fixture wiring this scene into CI follows in a separate commit. |
||
|
|
3e0460e472 |
[Feat, Fix] (MG_Impl, MG_State, MG_Backend): program interface queries from frontend reflection; Espryt state-shadow reset
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).
|
||
|
|
33ff177bb2 |
[Fix, Test] (MG_Impl, MG_State): advertised-extension conformance wave 1 - uniforms, validators, getters
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
|
||
|
|
2e6fc1ffc0 |
[Feat] (MG_Util, MG_Test): enable asynchronous shader compilation by default (P1 stage 7)
kAsyncShaderCompileDefault flips to true, which also advertises
GL_KHR_parallel_shader_compile by default on both backends. Unset
MOBILEGL_ASYNC_SHADER_COMPILE now resolves to ON; =0 remains the complete kill
switch (reverts the threading and withdraws the extension together).
The gate behind the flip (headless Mesa - llvmpipe for Espryt, lavapipe for
Magma - at
|
||
|
|
c6299f754f |
[Fix] (MG_Backend/DirectVulkan, MG_State): key per-object memos on lifetime ids, not heap addresses
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.
|
||
|
|
dcf918b9ee |
[Perf] (MG_State): adopt in-flight compile jobs across shader objects (P1 stage 6)
~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. |
||
|
|
d98f72447d |
[Feat, Test] (MG_Backend/DirectVulkan, MG_Test): Magma advertisement + the parallel-compile test net
Same gated push as the Espryt commit. The tests ride here because they exercise both backends' advertisement paths and every piece of the extension: ParallelShaderCompileTest (13 unit cases - the held-job proof that GL_FALSE is observable and a second poll still shows outstanding work, program equivalent, untouched objects read TRUE, always-TRUE with async off, unknown pnames still INVALID_ENUM, zero-count join+inline for compiles AND links, nonzero restores while Initialize() does not, clamping and 0xFFFFFFFF and KHR/ARB sharing one state, the getter vs the budget, the string tracking configuration through glGetString AND glGetStringi) and AsyncCompileScenario (5 real-GPU cases per the design: 64-compile polling, forced-join correctness, string/thread-count checks against a live driver, zero-count synchronous settlement, and async-vs-sync frames rendered byte-identical with quadrant signatures so two identically-wrong images cannot pass). Verified 5/5 on NVIDIA in the full 2x2 backend x flag matrix with MOBILEGL_ITEST_REQUIRE_GPU=1. |
||
|
|
f15cb8900f |
[Feat] (MG_Backend/DirectGLES): advertise GL_KHR_parallel_shader_compile when async is enabled
Gated on the configuration flag on purpose: the string is the one change a retrace can never cover (Iris/Sodium pipeline their submissions differently once they see it), so the kill switch has to withdraw the app-visible behaviour along with the threading. |
||
|
|
bd0def6133 |
[Feat] (MG_Impl, MG_State, MG_Util): the GL_KHR_parallel_shader_compile surface (P1 stage 5)
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. |
||
|
|
6f8b7fbc40 |
[Feat] (MG_State, MG_Util): async program linking on the job graph (P1 stage 4)
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. |
||
|
|
e5fb57f7eb |
[Feat] (MG_State, MG_Util): async shader compilation behind the default-off flag (P1 stage 3)
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). |
||
|
|
c93e5fa409 |
[Refactor] (MG_State, MG_Util): join-by-construction link/compile artifacts (P1 stage 2)
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).
|
||
|
|
8191075133 |
[Feat] (MG_Util): the async-compile pool skeleton behind a default-off flag (P1 stage 1)
Standalone Asio (submodule, asio-1-38-2 @ 8806a680, ASIO_STANDALONE + ASIO_NO_DEPRECATED, header-only - no linked artifact) and the job machinery the async shader pipeline will run on: JobNode (state machine with deferred errors, continuations firing exactly once, dependency counters, cancel semantics split into request vs outcome) and ShaderCompilePool (asio::thread_pool behind a pimpl so no header leaks asio; big-core count via cpufreq at >=85% of peak clamped to [1,4]; lazily constructed, so with the flag off no worker thread ever exists; StopAndDrain leads DestroyImpl). MOBILEGL_ASYNC_SHADER_COMPILE / _THREADS config knobs, default OFF. Nothing in the GL pipeline references the pool yet - grep-verified; the full DirectGLES retrace and compile benches are byte- and time-identical. 25 threaded unit tests, clean across 20x gtest_repeat. |
||
|
|
d6caed7822 |
[Fix] (MG_Util, MG_State): five latent frontend bugs the async work made load-bearing
- 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. |
||
|
|
9152e88734 |
[Perf] (MG_State): dedupe shader compiles by source hash
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. |
||
|
|
2406e2d219 |
[Perf, Fix] (MG_Util): preprocessing cleanups - dead scanners, quote-mask bug, one version inspection per compile
Three scoped changes to ShaderSourceProcessor, none altering any transform's
output (pinned by a byte-stability test across the legacy-shader anchor path):
- Delete BlankBlockComments and RemoveDefineForIdentifier - dead since their
callers left; the former's newline-terminated quote handling moves into
MaskCommentsAndQuotedText (below) together with its rationale comment.
- Fix MaskCommentsAndQuotedText treating a quote as running past end-of-line.
GLSL has no multi-line literals, but a stray apostrophe in a directive or
comment tail ("#pragma message can't") blanked the REST OF THE FILE for
every masked consumer - the tokenizer, the version inspection, and the P0a
explicit-location/binding extractors silently lost everything after it.
- Inspect the shader language once per PreprocessShaderSource run instead of
up to five times: NormalizeVersionDirective now takes the already-computed
ShaderLanguageInfo, and the two after-version injections share one
AfterVersionAnchor instead of re-running a full masked sweep each
(FindAfterVersionDirective -> InspectShaderLanguage) to find the same spot.
Compile-phase timings hold (BSL 1.848s, complementary-reimagined ~5.7s);
retraces and the 435-test unit suite unchanged.
|
||
|
|
b228f813c0 |
[Perf] (MG_Util): replace the builtin-shadowing string scans with one tokenize and a SPIR-V OpName pass
RenameBuiltinShadowingFunction probed the whole source ten times per compile (5 names x mask + scan, each a full-text pass) and still had two blind spots: a 5-name list and single-line-definition-only detection. On Complementary-scale packs (4.5MB of sources) that was ~68% of the compile phase. The rename is now split by FAILURE LAYER, both halves sharing one name table header so they cannot drift: - A SPIR-V OpName pass in SanitizeAndOptimizeBinary covers the full ESSL 3.20 builtin set (~146 names). Renaming a function id is safe by construction: builtin calls are GLSL.std.450 instructions and can never resolve to a user OpFunction, overloads are distinct ids (a helper overload delegating to the real builtin keeps working), dead preprocessor branches never reach SPIR-V, and macro-expanded definitions are covered. ESSL 3.x is the only consumer that forbids the redefinitions, and this pass runs before its transpile. - A lexical pass covers only the 5 names whose exact-signature redefinitions glslang's relaxed parse rejects outright (never producing SPIR-V for the backstop): the historical fma/max3/min3/round/tanh. One TokenizeCode pass; definition detection requires brace depth 0, a type-identifier previous token that is neither a statement keyword nor a directive tail, and skips files whose token-level braces do not balance (preprocessor-asymmetric arms) - over-detection is unrecoverable, so every ambiguity falls through to the backstop. Measured on the compile phase (prefix-diff, 3-run medians, Espryt/NVIDIA): complementary-reimagined 20.0s -> 5.5s, BSL 2.14s -> 1.85s. bliss (the pack that ships from-scratch fma/tanh helpers) stays at SSIM 0.999962. Tests: end-to-end ESSL assertions for the multiline-definition and new-overload shapes, the three adversarial-review reproductions (statement- keyword call under asymmetric braces, dead-#if compat shim, overload delegating to the shadowed builtin), and a source-level assertion pinning the lexical half specifically. |
||
|
|
0d0527192a |
[Perf] (MG_State, MG_Util): compile shaders with a single relaxed parse
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. |
||
|
|
81bcbd6c14 |
[Fix] (MG_State): allocate program and shader names from one shared name space
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. |
||
|
|
867fe3e0ef |
[Feat] (MG_Util, MG_IntegrationTest): POST rows for the Espryt multi-draw tier, and the scenario that pins it
Three DriverPost rows per the POST rule, since the ladder took on two new driver dependencies: glDrawElementsBaseVertex (WARN when absent - every base-vertex draw then costs a CPU index rewrite and an upload) and compute shaders (INFO - the default tiers never use them). The third names the tier that will actually run, with the full set the driver supports, resolved by the same function the backend calls so the two can not drift. The existing "Multi-draw base vertex" row stopped saying the fallback is a per-draw loop, which is no longer the whole truth. Scenario D asserts the one contract every tier shares: a multi-draw paints exactly what the unrolled single draws paint. The reference side is a loop of glDrawElementsBaseVertex and never enters the emulation, so a tier cannot make itself look right by breaking both sides alike, and a blank-frame pair is rejected outright - drawing nothing is the failure mode this path actually has. Nine cases, chosen for the shapes the Minecraft retraces contain none of: narrow index types, a base vertex past the index type's range, primitive restart inside a strip on two index types, client-memory index arrays, and a batch with zero-count sub-draws (whose prefix sums the flattening tier's binary search has to skip). Each of the six tiers passes all nine on NVIDIA, and ext/auto/compute also pass on Mesa where the ext tier is reachable. The suite is falsifiable, not merely green: rewriting the rebase the way MobileGlues does it - truncate to the source width, no restart passthrough - fails exactly three cases on the drawelements tier (both restart cases and the out-of-range base vertex) and leaves basevertex, which rewrites nothing, passing. That control is also what turned up the restart hole in the flattening tier fixed in the previous commit. |
||
|
|
0ec487c993 |
[Feat] (MG_Backend): port MobileGlues' multi-draw emulation to DirectGLES as a tier ladder
ES has neither glMultiDrawElements nor glMultiDrawElementsBaseVertex, so both are emulated. DirectGLES had two ways of doing it - one glMultiDrawElementsBaseVertexEXT where the driver has the extension interaction, otherwise a per-draw loop. This adds the five MobileGlues uses (gl/multidraw.cpp), so the ladder is now: one glMultiDrawElementsBaseVertexEXT; one glMultiDrawElementsIndirectEXT over a synthesized command buffer; one glDrawElementsIndirect per command over that same buffer; the base-vertex replay; plain glDrawElements over a CPU-rewritten index stream, for drivers with no base-vertex draw at all; and a compute shader that flattens the whole batch into one rebased index buffer drawn by a single glDrawElements. They live in their own translation unit that owns the entry point outright, preparation included - the compute tier has to dispatch BEFORE PrepareForDraw, or it would have to unpick the program, storage-block and index bindings the preparation just made, and a dispatch inside an open transform-feedback span is not legal at all. The auto ladder is ext -> basevertex -> multiindirect -> indirect -> drawelements, which is NOT MobileGlues' order (it puts the indirect tiers first). Measured on mc_sodium_multidraw, ns/op, median of three: NVIDIA ES 3.2 basevertex 2500 vs multiindirect 5700 and indirect 5800; Mesa llvmpipe ext 19300, basevertex 25200, multiindirect 27600, drawelements 28700, indirect 31000. Ring-allocating the command staging instead of respecifying per batch was tried first and moved the indirect tiers by less than noise, so the cost is the indirect draw path itself, not the upload; only a real multi-draw entry point beats replaying the sub-draws. auto therefore resolves to basevertex on this box - byte for byte the behaviour that shipped - and the new tiers are what a driver with the ext interaction, or without base vertex at all, now gets. compute is never chosen by auto (nor by MobileGlues'): it rewrites the primitive stream rather than replaying it, and it measured slowest here. Four places this deliberately does not follow MobileGlues, each a correctness bug there. A rewritten stream is emitted as GL_UNSIGNED_INT whatever came in, because GL adds baseVertex at full precision and folding it into ushort indices wraps. The restart sentinel is carried across a rebase unrebased, or an enabled primitive restart is lost. The flattening tier declines strip/loop/fan modes, any sub-draw whose count is not a whole number of primitives, and any batch at all while primitive restart is enabled - a restart ends a primitive, so leftover vertices would find a third vertex in the next sub-draw and become a triangle GL never draws. And the indirect tiers decline client-memory index arrays, which have no buffer to address. gl_DrawID gets better rather than worse: the unrolled tiers now feed each sub-draw its index (the spec's value, where the old loop left the uniform untouched), and a program that actually reads it demotes the batched tiers, which can only hold one value for the whole batch. The per-batch cost is nil for the programs that do not read it. Verified: the five DirectGLES retraces are byte-identical (md5) across all six tiers on NVIDIA and on Mesa, each tier proven to have really executed rather than silently demoted, via a per-tier announcement in the log. Unit suite 421/421. The full retrace suite's five failures all reproduce unchanged on a stashed tree, so none are new. |
||
|
|
23b880c8be |
[Feat] (MG_Config, MG_Util): a tier knob and two capability flags for Espryt multi-draw
MOBILEGL_ESPRYT_MULTIDRAW_MODE=ext|multiindirect|indirect|basevertex| drawelements|compute|auto names the DirectGLES emulation tier for glMultiDrawElements(BaseVertex). Same contract as the Magma knob: a preference, not a demand, clamped at resolution time to what the driver actually has, and invalid values keep auto. Nothing reads it yet. The two capability flags the ladder selects on are new because neither existed in the shape the choice needs. SupportsDrawElementsBaseVertex is the weaker sibling of SupportsMultiDrawElementsBaseVertex - ES 3.2 core or EXT/OES_draw_elements_base_vertex, with no GL_EXT_multi_draw_arrays requirement - and it decides whether a batch can replay its sub-draws with their own base vertices or has to fold them into rewritten indices. SupportsComputeShader is ES 3.1 core plus the dispatch, barrier and shader-object entry points. Both keep the house rule the multi-draw flags already follow: the extension/version check is what proves support, the resolved pointer only confirms it, because eglGetProcAddress may hand back a live-looking stub for a function the context does not implement. |
||
|
|
ebc5bff9b1 |
[Fix, Feat] (MG_Backend): make DirectVulkan multi-draw actually draw, then pick its best tier
The bug: DirectVulkan.cpp::MultiDrawElements had its entire body
commented out - plain glMultiDrawElements on Magma recorded NOTHING,
no error, no pixels (readback shows the deferred clear never even
materialized). It now shares the tuned base-vertex implementation, and
both plain entries are pixel-proven by a 4-sub-draw harness.
The feature: every CPU-side multi-draw form dispatches through three
tiers after round-9's contiguous-run merge (restructured to merge into
a span BEFORE dispatch, so every tier consumes the shrunken array):
1. VK_EXT_multi_draw: one vkCmdDrawMulti(Indexed)EXT, chunked by
maxMultiDrawCount; per-draw vertexOffset rides in the struct. The
extension is requested only when enumerated and its feature bit
confirmed, entry points via vkGetDeviceProcAddr, demoted if
missing.
2. multiDrawIndirect: the param span uploads DIRECTLY as a transient
INDIRECT-usage buffer - DrawIndexedCmdParam is layout-identical
to VkDrawIndexedIndirectCommand and DrawCmdParam's head is a
legal 24-byte-stride VkDrawIndirectCommand, both static_asserted,
so no repacking - then one vkCmdDraw(Indexed)Indirect per
maxDrawIndirectCount chunk. firstInstance!=0 additionally
requires drawIndirectFirstInstance or the batch drops a tier.
3. The byte-identical unroll.
gl_DrawID: tiers 1-2 are spec-correct (0,1,2,3 across a probe's
sub-draws); the unroll tier keeps the pre-existing always-0 contract.
The default tiers strictly improve DrawID correctness.
Adversarially verified: the five real DirectVulkan retrace images are
BIT-IDENTICAL (md5) across auto/ext/indirect/unroll; zero validation
VUIDs on every tier; a simulated no-EXT device resolves to indirect
and renders the same bytes; the known-red create-indirect fixture
crashes at the identical call before and after (not worse, not fixed).
Unit suite 423/423 on the rebased tree, retrace subset 10/10. Bench:
mc_sodium_multidraw's contiguous shape merges 32->1 before dispatch,
so no bench delta - the tiers' beneficiaries are non-contiguous real
streams (the sodium RETRACE pushes ~58-sub-draw batches, in=out
243101 with zero merges) and mobile drivers. A reproducible +3-4%
code-layout drift on mc_use_program (zero shared code, I-cache
displacement from +400 lines) stays under the action gate and is
booked here rather than hidden.
|
||
|
|
231d5c90e4 |
[Feat] (MG_Config, MG_Util): a preference knob and POST rows for Magma's multi-draw tiers
MOBILEGL_MAGMA_MULTIDRAW_MODE=ext|indirect|unroll|auto selects the DirectVulkan multi-draw dispatch tier, clamped to what the device supports with one INFO line when it falls back; auto (and unset) picks the best supported tier. Invalid values keep auto. Magma-only: the variable has no effect on DirectGLES. Note for the escape hatch: mode=unroll also forces the GL indirect multi-draw paths onto their per-command loop, where gl_DrawID reads 0 for every sub-draw - Flywheel-style content that keys on flw_drawId renders accordingly. Three DriverPost rows per the POST rule: VK_EXT_multi_draw (PASS/INFO), the multiDrawIndirect feature (WARN downgraded to INFO - there is always a fallback tier), and the resolved dispatch tier with the full chain. drawIndirectFirstInstance gains a row too, since the indirect tier's legality check now relies on it. |
||
|
|
d5f5e6405b |
[Perf] (MG_Backend): batch DirectGLES multi-draw base-vertex where the driver really has it
When SupportsMultiDrawElementsBaseVertex is true, glMultiDrawElements- BaseVertex issues one glMultiDrawElementsBaseVertexEXT instead of a per-draw loop; the fallback loop is byte-identical otherwise. The local NVIDIA ES driver lacks GL_EXT_multi_draw_arrays, so the batch cannot engage here and no local win is claimed (counter-proven: batched=0 / fallback=264329 across a sodium retrace). On Mesa llvmpipe, which implements the full interaction, the batch engages (batched=4566, ~58 sub-draws per call) and is pixel-identical to a forced-fallback control (same SSIM to the last digit). The beneficiaries are mobile drivers advertising the interaction - the Sodium chunk path collapses 32 driver entries into one - and the DriverPost row shows which side any device falls on. A/B on both backends: every case inside the 5% bar. Unit suite 423/423, retrace subset 10/10. |
||
|
|
ac3a83b207 |
[Fix] (MG_Util, MG_Test): never take a non-null eglGetProcAddress result as support
On GLVND Linux eglGetProcAddress returns a non-NULL trampoline for ANY name - including a fabricated one - so pointer-nullness can never signal driver support. The three EXT multi-draw entry points were registered as required (spurious error logs on drivers without them) and their pointers were trusted; the NVIDIA ES driver hands back a stub for glMultiDrawElementsBaseVertexEXT that SILENTLY DROPS draws, which once made a "77% faster" multi-draw batch that rendered nothing. The three entries are optional now, and two extension-derived capability flags follow the established Supports* pattern - each is an extension-string check AND a resolved pointer, so a flag alone is sufficient at a call site: SupportsMultiDrawIndirect: GL_EXT_multi_draw_indirect + both entry points resolved. SupportsMultiDrawElementsBaseVertex: (GL_EXT or GL_OES_draw_elements_base_vertex) + GL_EXT_multi_draw_arrays + the entry point resolved. The multi_draw_arrays conjunct is the registry fact the stub exploited: glMultiDrawElementsBaseVertexEXT exists only in interaction with GL_EXT_multi_draw_arrays, and this NVIDIA driver advertises everything else EXCEPT that one - so the entry point is genuinely unsupported while eglGetProcAddress still "resolves" it. Two DriverPost rows report both capabilities (INFO when absent - a fallback always exists). Unit tests pin the stub shape, the exact NVIDIA shape, the supported shape and extension-without-pointer. Proven load-bearing: forcing the old pointer-only condition on the NVIDIA ES driver reproduces the silent drop exactly (sodium retrace SSIM 1.000000 -> 0.329522, no crash, no GL error); with the gate the same run is a literal 1.000000. Unit suite 423/423 (two new tests), retrace subset 10/10, integration suite 52/52. |
||
|
|
335f2decbd |
[Perf] (MG_Backend): stop DirectVulkan re-proving sampler sets and re-walking render passes
Two per-draw costs from the round-10 profiles. A per-program sampled-set epoch inside UniformManager skips the per-binding descriptor proof walk when no texture or sampler API ran since that program's previous draw - the mc_sampler_churn/mc_tex_param pattern. The pass-switch path stops re-deriving render-pass state that its own value hash already pins. Load-gated 6-round order-alternating A/B (medians): magma tex_param -13.3%, pass_switch -10.9%, state_toggle -5.4%; espryt untouched and unmoved. The two matrix flags (sodium +7.5%, tex_stream +6.2%) reversed under 10-pair isolated alternating re-runs (-5.5% and +2.5%) - the same position-bias artifact every previous round's flags showed. Unit tests 421/421; retrace subset and the 52-entry integration suite pass. Landing note: this diff was authored by a round-10 agent whose session died before adjudication; the A/B data survived (r10bmag_ab_raw.csv) and the flags were adjudicated before landing. Its relink also exposed the pre-existing exit-teardown SIGSEGV fixed in the previous commit. |
||
|
|
fb1ad96c04 |
[Fix] (MG_Backend): stop DirectGLES twin destructors calling a dead driver at exit
The static twin registries destroy their backend objects from __run_exit_handlers, and a twin destructor then jumps through g_GLESFuncs into a driver library that exit() may already have torn down - a latent SIGSEGV that DriverBench has been dumping core with on every exit, and that any relink shuffling static destructor order can hand to the trace-replay binary (a byte-perfect replay then "fails with status Segmentation fault"). A process-teardown flag now short-circuits the program, VAO and texture twin destructors: past exit() the driver reclaims every GPU object anyway, so the skip is a deliberate leak of nothing. The flag is set by a std::atexit handler registered lazily on first registry use - by then every static everywhere has finished constructing, so the handler runs BEFORE any static destructor. A registry-destructor hook was tried first and is wrong: tests and cache resets destroy temporary registry instances mid-run, which latched the flag while the process was alive (caught by DirectGLESBackendTexture.DestructorDeletesIdAndScrubsBindingCache). 421/421 unit tests, the retrace subset exits cleanly on both backends, and the 52-entry integration suite passes. |
||
|
|
313b75a7c0 |
[Test] (MG_IntegrationTest): pin the two shipped memo bugs with rendered pixels
Both |
||
|
|
d7976326fa |
[Fix] (MG_Backend): two DirectVulkan draw memos trusted more than they proved
Two correctness holes from the round-7/8 fast-path work, found by bisecting the retrace matrix after corruption reports on device. Cross-frame slice trust: the vertex-binding and EBO memos skipped the acquire - the frame's content-sync point - whenever their recorded slice epochs still matched, trusting the BumpSliceEpoch inventory to cover every way a buffer's GPU copy can go stale. At least one mutation path escapes it: journeymap and common-mods retraces shipped visibly corrupted, and Sodium on an Adreno device rendered random triangles from stale vertex data. A memo recorded in an earlier frame now declines, so the first draw of each (VAO, frame) re-runs the full acquire; the same-frame paths (layout memo, factory-chase elimination, one-compare rescue) are untouched. The cross-frame idea can return once the bump-site inventory is proven complete against exactly these traces. Transform-flags memo key: GetShaderTransformFlags reads the swapchain pre-transform AND whether the bound draw framebuffer is the default one - only a presenting pass gets the Y-flip/rotation bits. The memo declared it pure in the pre-transform, so after any render-to-texture pass the next default-framebuffer pass inherited the FBO's unflipped flags: 1.17-main-menu retraced as a perfectly rendered, perfectly upside-down frame (SSIM 0.052, deterministic), and cloud passes flickered on device. The memo now keys on (preTransform, isDefaultFbo). DirectVulkan retraces for 1.17-main-menu, journeymap, common-mods, sodium and xaero-world-map all pass on lavapipe; unit tests 421/421. |
||
|
|
72ee7c439c |
[Perf] (MG_Backend): merge DirectVulkan's contiguous sub-draws, remember four programs
61% of mc_sodium_multidraw's steady-state CPU sat inside the driver encoding one vkCmdDrawIndexed per sub-draw. MultiDrawElements now collapses contiguous runs: merge only when the topology is a list (POINTS/LINES/TRIANGLES), the accumulated count sits on a primitive boundary, primitive restart is off, baseVertex/instanceCount/ firstInstance are identical and firstIndex is adjacent, with a count-overflow guard - the bench's 132x32 sub-draws become 132x1. Dangling-index discard semantics for list topologies are what the GL spec already mandates per draw. No new Vulkan feature, so no DriverPost gate; VK_EXT_multi_draw stays a gated follow-up. The draw fast path's single SetupDraw snapshot died on every program ping-pong (use_program's A/B pattern sent every other draw down the full path, CollectSampledTextures alone 6.2% self). A 4-entry program-keyed snapshot table (MRU by program lifetime id, per-entry sampled-set copies, per-entry invalidation on decline or full-path start, all entries still cleared at command-buffer boundary, pipeline age-out and swapchain recreate) keeps all cycling programs hot. Load-gated 6-round order-alternating A/B, sha1-fingerprinted pair: sodium_multidraw -41.3%, use_program -23.9%, pass_switch -12.7%, tex_param -3.8%, vanilla -2.1%; the one flag (tex_stream +5.4%) reversed to -0.5% across 10 isolated alternating pairs. Espryt untouched and unmoved. Unit tests 421/421. |
||
|
|
cdea275227 |
[Perf] (MG_Backend): stage only the rects DirectVulkan actually dirtied
Consume MipmapStorage's new dirty-rect list: pack each rect tightly into the staging block and issue ONE vkCmdCopyBufferToImage with N regions instead of staging the whole union box. Offsets are computed identically in the pack and copy loops; disjoint rects mean no overlapping copy destinations; the combined depth-stencil and RGB-expand/depth-convert paths keep their single-box route (gated to the color-aspect, no-conversion case). 54% of mc_tex_stream's steady-state CPU was the one shadow->staging memmove of the union box; staged bytes drop to 4.8% (~2MB -> ~95KB per frame) and the case improves ~-49% (5945 -> 3048 ns/op, ~2.2x native to ~1.2x). Zero validation-layer findings on the 95-region copy. Unit tests 421/421. |
||
|
|
6a02c5fea0 |
[Perf] (MG_Backend): upload only the rects DirectGLES actually dirtied
Consume MipmapStorage's new dirty-rect list: when a level offers a profitable rect list, the sync path issues one glTexSubImage2D/3D per rect under a single UNPACK_ROW_LENGTH set/reset instead of one call covering the union box. Striding is the exact scheme the single-box path already uses (UNPACK_ALIGNMENT pinned to 1 by ScopedDefaultUnpackState, so every bpp is stride-exact); levels without a profitable list take the old path unchanged. On the atlas-streaming case this trades one ~2MB upload for ~95 small ones totalling ~95KB - roughly a wash in driver-call overhead on desktop NVIDIA GL (mc_tex_stream ~-3%), a clear byte-volume win for tiled/mobile GLES where the driver shadow-copies every upload. Unit tests 421/421. |
||
|
|
7db5b35a3e |
[Perf] (MG_State): remember every dirty rect, not just their union
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. |
||
|
|
990e518e33 |
[Perf] (MG_Backend): give DirectVulkan's draw memo a table that fits in cache lines
The per-VAO resolved-bindings map probe was ~45% of UploadAndBindVertexBuffers' self time, and the aux-memo pointer chase was the single hottest instruction left in TrySetupDrawFastPath. Both die together: a fixed 2048-slot two-probe 64B-aligned VaoDrawMemo table embeds the VAO key, content-hash-validated layout facts and the bindings payload reordered hot-to-cold. Layout facts hold exactly while the slot's content hash equals the live VAO's own config-guarded hash; a recycled VAO address either misses or reproduces a byte-identical config, for which the facts are correct by construction. Bindings keep their full per-draw revalidation; recycled slots zero their frame serials so half-filled entries can never match. ComputePipelineStateHash, the depth/stencil probe and the primitive-restart probe now take one bulk GetRenderStateParameters() fetch instead of ~17 cross-TU accessor calls (verified pure field reads, identical bit packing). The EBO slice memo gained the same manager-wide epoch one-compare rescue the vertex half uses. GetShaderTransformFlags is memoized on pre-transform. Sodium's MultiDrawElementsBaseVertex hoists GetGLTypeSize out of the per-sub-draw loop, replaces the division with a shift, and skips unsupported index types loudly instead of dividing by zero. Also verified: a GL_BLEND toggle recompiles nothing in steady state - the glslang frames in earlier state_toggle profiles were startup contamination. Quiet-box load-gated 6-round A/B: sodium_multidraw -8.0%, tex_param -4.1%, use_program -3.3%; steady-state vanilla_draw CPU -20% ns/op at 4096 frames (the 80-frame matrix compresses CPU wins under GPU boost clocks; profiles confirm UploadAndBindVertexBuffers 6.3% -> 4.4% including the table probe, and the aux cold-line load gone). The one matrix flag (pass_switch +7.5%) reversed to -3.2% in 10-pair isolated re-runs. Unit tests 421/421. |
||
|
|
25a8f51db5 |
[Perf] (MG_Backend): make DirectGLES program switches remember their own bindings
mc_use_program cycles programs whose texture bindings never change, yet every switch re-walked the units. Six fixes, one theme: a switch back to a known program should find its own state waiting. Per-program 4-entry resolved-texture-binding memo (round-robin, shadow memcmp on hit) skips the unit walk when a program returns with its bindings intact. The whole sampler-uniform pass in BindCurrentProgramWithResources is memoized per program twin behind (context, unitBindingsEpoch, samplingGeneration, backendStateVersion, textureContextGeneration) plus a per-sampled-unit sampler-shadow row compare, invalidated on relink/backend rebuild; the BindCurrentUnitSamplers walk sits behind the same keys. Every unit assignment, sampler-parameter change and bind path was verified to bump one of those inputs. UboRingAllocate's common path is now a generation check, a power-of-two mask, an overrun check and a head bump - the duplicate availability probe, frame-mark retirement and divisions moved to the wrap slow path. The per-context framebuffer binding slots (the frontend getter linear-scans per call) are cached as direct pointers - slots are by-value members of GLContext, so the pointers are stable by construction - feeding SyncCurrentFBO, SyncNeccessaryTextures and the broadcast memo; BindCurrentFBO's per-draw registry hash Find became a TwinLookupMemo probe. The VAO config-version cold-line load is hoisted to the top of PrepareForDraw to overlap its miss. Quiet-box load-gated 6-round order-alternating A/B, all nine cases, both backends: use_program -27.7%, vanilla_draw -16.2%, ubo_range -13.4%, pass_switch -11.1%, sampler_churn -10.7%, state_toggle -9.2%, sodium_multidraw -5.3%, rest flat. No regression on either backend (magma's one matrix flag disproved by isolated re-runs against byte-identical DirectVulkan sources). Unit tests 421/421. |
||
|
|
8f2b766b56 |
[Perf] (MG_Backend): let DirectVulkan trust across frames what it proved once
The draw fast path still paid for its own proofs: the hottest single load (20% of TrySetupDrawFastPath) was chasing the cold VertexInputStateFactory heap entry just to answer "same vertex-input layout?". That answer now comes from the frontend VAO's config-guarded aux memo (layout hash + attribute masks), and a VAO-cycling stream with a stable layout skips the pre-flight AND pipeline re-resolution entirely. The VkProgramObject* is memoized on the snapshot behind a new ProgramFactory cache-structure epoch (bumped on every insert/erase; use is re-stamped so the idle sweep can never evict a live entry). A render-state version move no longer forces the full path: the pipeline value hash is refreshed in place and the 8-entry memo probed directly (the GL_BLEND-toggle case). The resolved-vertex-bindings memo now revalidates all-resident unmapped entries ACROSS frames via per-binding slice epochs - minted from a process-lifetime counter so a recycled address can never revalidate, with every mutation path funnelled through BumpSliceEpoch - while stamping each resource's GPU-use serial exactly as the skipped acquire would, preserving the busy-tracking that glBufferSubData's host-write-vs-staged-copy choice depends on. Resident index buffers get the same treatment through an EBO slice memo. The six-part dynamic-state tail (viewport/scissor/blend constants/depth bias/line width/stencil) is gated behind one render-state-parameters version + pass-geometry compare per command buffer. GetSlice is inlined; SampledBindingsUnchanged walks only the program's declared bindings. Quiet-box 6-round order-alternating A/B (on top of the frontend VAO-bind commit): vanilla_draw -20.8% (790 -> 626 ns/op, 3.4x native to 2.5x), sampler_churn -28.2%, ubo_range -8.4%, state_toggle -3.8%; tex_param's matrix flag (+10%) was adjudicated by an isolated alternating re-run at +1.0% - position bias, not regression. Unit tests 421/421. |
||
|
|
b9d8ad0421 |
[Perf] (MG_Backend): give DirectGLES one epoch that says no buffer moved
Four draw-path costs, one theme: re-proving what nothing invalidated. A manager-wide buffer-mutation epoch (atomic; bumped with release AFTER every mutation lands: all six BufferBackendOps via tracking wrappers, every backend-initiated writeback - XFB readback/scatter, the five pack-PBO readbacks - registry registration changes, and backend context destruction; the full site inventory lives in a comment at the accessor) lets the per-VAO resolved-buffers memo stamp the epoch after one all-clean probe pass and skip every IsBufferDrawClean probe while it holds. The IBO keeps its bound-object identity compare - only the probe is elided. Non-bumping paths are enumerated with why they are safe: GPU-authoritative writes are ignored by the probe, persistent-mapped resources are clean by construction, and draws on non-persistent maps are frontend-rejected GL errors. GetProgramForDraw is hoisted to one call per PrepareForDraw and handed to the four consumers that each re-derived it. The enabled-draw-buffers walk feeding the fragColor broadcast count is memoized on the (FBO, slot version, object version) trio. The UBO-binding loop probes IsBufferDrawClean before falling back to EnsureBufferResource. The texture chain captures (context, maxTouchedUnit, samplingGeneration, unitBindingsEpoch) once per draw - shared by SyncNeccessaryTextures and BindCurrentTextures, halving the epoch computations - and an aggregate gate that is the exact conjunction of the three Sync*ToBackend early-outs skips the per-texture cross-TU calls. The t_egl* thread_local verification pair became owner-thread-guarded atomics reset by MakeCurrent/ReleaseCurrent, removing __tls_get_addr from the draw loop. Quiet-box 6-round order-alternating A/B (with the frontend VAO-bind commit): all NINE Espryt cases improved - sampler_churn -11.0%, state_toggle -9.3%, ubo_range -9.1%, vanilla_draw -5.4%, pass_switch -3.5%, the rest -1% to -2.5%. Unit tests 421/421. |
||
|
|
f8069c0624 |
[Perf] (MG_State): stop paying two atomic refcounts for every glBindVertexArray
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. |
||
|
|
d0aae85da2 |
[Perf] (MG_Backend): let DirectVulkan's draw fast path survive a VAO swap
TrySetupDrawFastPath declined on its VAO pointer check for every draw of a 512-VAO cycle - the Blaze3D chunk-render shape - so the fast path was dead exactly where it mattered: full SetupDraw, per-draw ResolveSamplerDescriptor, SyncTextureAndGetDescriptor and render-pass re-fetch, for draws whose only change was the VAO. Three fixes. A moved VAO now re-runs only the vertex-input pre-flight and re-resolves the pipeline instead of declining to the full path. That resolution probes the value-keyed pipeline memo directly off a cached pipeline-state hash and snapshot render-pass hash, skipping GetOrCreateRenderPass and its GetPendingRenderbufferClear probes per draw; a stale cached hash can only miss, never false-hit. And when the sampler-descriptor hint holds and the program's single dynamic UBO re-resolves to the same VkBuffer and range - only the dynamic offset moved, the per-draw glUniform case - the descriptor walk collapses to one offset recompute and a vkCmdBindDescriptorSets of the same recorded set with new pDynamicOffsets. The rebind memo is invalidated at BeginFrame, layout destruction and override walks; the program lifetime id never repeats, and per-frame descriptor sets are never rewritten within their frame. mc_vanilla_draw -36.9% (1260 -> 795 ns/op, 4.6x native to 3.4x), sodium_multidraw -18.0%, state_toggle -14.9%, sampler_churn -7.8%, use_program -7.7%, ubo_range -7.3%, tex_param -7.3%. All nine cases on both backends, interleaved A/B; no attributable regression. Unit tests 421/421. |
||
|
|
b904658b10 |
[Perf] (MG_Backend): stop DirectGLES re-resolving the same VAO's buffers and twins every draw
Four per-draw costs, all lookups that re-answer the same question. SyncNeccessaryBuffers walked all 32 attribute slots cold and ran EnsureBufferResource per buffer on every draw. The backend VAO twin now hosts a resolved-draw-buffers memo: the deduped enabled-attribute buffers and the index buffer resolve once per VAO config version, and each hit re-validates every entry with IsBufferDrawClean - a shadow probe mirroring every no-op branch of EnsureBufferResource (resource identity, context generation, pending ops, change serial) - falling back to the full path for just the dirty entries. The IBO entry is checked against the live bound object each draw, so slot-version wrap cannot false-hit. The registry hash Finds that resolve state objects to their backend twins ran several times per draw. TwinLookupMemo - a direct-mapped, Fibonacci-hashed table (4096 VAO / 256 program slots) with weak-ptr owner equality against address reuse - answers them in one probe; collisions fall back to the registry. A live entry's twin is never replaced once set, so owner equality proves the raw pointer. SyncCurrentVertexAttributeValues' pending-mask memo was a function-static single entry that missed every draw once the app cycled VAOs; it now lives on the twin. CurrentXfb()'s per-draw FastSTL map lookup became a cached pointer invalidated at every map mutation (open addressing moves values on any insert/erase/clear). mc_vanilla_draw -12.6% (3.4x native to 3.0x), ubo_range -9.8%, sampler_churn -7.5%, pass_switch -6.5%, sodium_multidraw -6.0%, state_toggle -4.6%. All nine cases measured on both backends, interleaved A/B; no case regressed. Unit tests 421/421. |
||
|
|
4b3fd11462 |
[Perf] (MG_Backend): key DirectVulkan's pipeline memo on state values, not a version that never repeats
Two per-draw churn costs, one cause each. A blend toggle switched pipelines through a memo keyed on a monotonic pipeline-state version - which never repeats, so flipping GL_BLEND off and back on produced a "new" key both times, forced the full SetupDraw and rebuilt the whole pipeline payload for a pipeline the cache already held. The memo now keys on a value hash of the pipeline-relevant fixed-function state, recomputed only when the state version moved, and the consecutive-draw fast path re-resolves just the pipeline through it when nothing but render state changed. Blaze3D brackets every batch with exactly this toggle; mc_state_toggle drops 36% (6629 -> 4230 ns/op, 4.8x native to 3.7x). The sampler-churn cost had the same shape as the Espryt side fixed separately: glBindSampler bumps the frontend texture-bind generation even when it re-binds the sampler the unit already holds, so the per-draw fast path died every draw. The fast path now proves each binding's descriptor inputs unchanged - texture and sampler lifetime ids, parameter and content sums, the sampling-resolution generation, image epochs and exact layouts - and reuses the binding's cached VkDescriptorImageInfo instead of re-running the resolve chain. mc_sampler_churn drops 30% (1597 -> 1125), and the proof machinery pays for itself on the uniform-range case too (-17%). mc_tex_param stays where it is on this backend deliberately: profiling shows its remaining cost is frontend validation with zero backend work, unreachable from Renderer/. All nine cases measured on both backends, interleaved A/B, no case worse than noise. Unit tests 421/421. |
||
|
|
9be5d95440 |
[Perf] (MG_Backend): give DirectGLES unit bindings an epoch the sampler churn cannot fake
The texture-binding memos added earlier keyed on the frontend texture-bind generation, and 26.2-style unit switching defeats them: glBindSampler bumps the generation even when it re-binds the sampler the unit already carries, so a frame that cycles active units re-ran the full two-pass, eleven-slot alias resolution and the unbind walks on every draw. mc_sampler_churn sat at 1674 ns/op against the native driver's 239 - the worst multiplier left on this backend - with about half the time in two virtual calls per binding slot. The units now carry an epoch: a snapshot of each touched unit's slot objects and sampler object, compared by weak_ptr OWNERSHIP rather than raw pointer - a held weak_ptr pins its control block, so a freed-and-recycled object can never owner-equal its predecessor, which is the ABA hole a pointer key would have and the reason version keying was rejected (WithTemporarilyBoundNamedTexture bumps slot versions without touching the bind generation). The (context id, bind generation, high-water mark) triple gates the snapshot walk to at most once per draw; the epoch moves only when a binding really changed. Both per-draw memos key on the epoch plus the sampling-resolution generation, which carries what the epoch cannot see: a default texture's image appearing, and every completeness input. Two smaller memos ride along: the per-unit sampler-registry lookup (owner-keyed, misses never cached - the backend object may be created later in the same draw), and the pending-vertex-attribute mask, whose first version scanned all 32 slots and put +10% on the VAO-cycling case before being restricted to the program's active locations. ns per op, DriverBench on a GTX 1660 SUPER, isolated A/B, all nine cases on both backends: mc_sampler_churn 1673 -> 732, mc_use_program 4513 -> 4279, mc_state_toggle 2365 -> 2247, everything else within noise and nothing worse. 7.0x native to 3.1x on the churn case. Unit tests 421/421. |
||
|
|
d49d79a64b |
[Perf] (MG_Backend): pool DirectVulkan's upload staging and batch its submits
Every dirty texture bought itself a fresh staging buffer (vmaCreateBuffer + vmaMapMemory), a fresh command buffer, a fresh fence, and its own vkQueueSubmit. A perf profile of the sprite-animation case put 41% of the whole run in the kernel on the resulting ioctl traffic; the reclaim list already avoided waiting on the fences, so the cost was the allocation and submission machinery itself, paid per texture per frame. Staging now comes from a pool of persistently-mapped blocks (1 MiB minimum, exact-size beyond that, bump-allocated, 32 MiB idle cap), and uploads record into one shared batch command buffer from a dedicated command pool, going out as one submit with one pooled fence per flush. Fences, command buffers and blocks all recycle through the existing fence-list reclaim instead of being destroyed. Flush points: before every frame command buffer submission (which is what preserves the old ordering argument - the batch reaches the queue strictly before anything that could sample its images), on the glFlush finite-time path, when a batch would outgrow its staging bound, and eagerly at 128 KiB, which measured faster because the GPU overlaps the copy with the rest of the frame's CPU recording. The mid-frame upload-draw-upload-again sequence detects itself through the batch image list and flushes first, reproducing the old two-submit granularity exactly; a deferred image release flushes any open batch that still references the image, because drain proofs only cover submitted work. ns per op, DriverBench on a GTX 1660 SUPER: mc_tex_stream 9405 -> 5373 (2.3x the native driver, from 3.9x), atlas_sprite -57%, lightmap -89%, chunk_upload -10%; draw-path cases unchanged. The suite's sampler-churn number reads a few percent worse right after the now-much-faster upload case, which was chased to schedutil downclocking during the newly-blocking-free frames - isolated and frequency-pinned runs measure parity; noted here so the next person does not re-chase it. Unit tests 421/421; Vulkan validation layer clean across draw and upload cases. |
||
|
|
f5761ea1f3 |
[Perf] (MG_Backend): diff only the render-state span that moved, and gate the per-draw walks
Four per-draw costs in DirectGLES, all of the same species: work re-done for an answer that had not changed. SyncRenderState was guarded by a single version compare, so one blend toggle - the way Blaze3D brackets every batch - re-diffed the whole ~40-field render state block and copied the full struct back into the shadow, every draw. The parameter struct is now split into three contiguous byte spans, each gated by a memcmp against the backend shadow; a per-draw blend flip touches only the blend span. The shadow is byte-cloned after each sync so the span compares stay exact, padding included. Blocks whose inputs live outside the parameter struct (the surface-size viewport fallback, the sRGB context capability) stay ungated, and the dual-source-blend hard-fail still fires every draw because a throwing sync never stamps the shadow. SyncMipmapsToBackend gained a first-level clean gate on (context id, sampling-resolution generation, content version, params version) that skips the IsComplete walk and the eight-field shape probe outright; every shape mutation funnels through BumpShapeVersion, which is what makes the gate sound. SyncToBackend for vertex arrays compares one aggregate config version instead of three stamps per attribute slot. And SyncNeccessaryTextures memoises the draw-framebuffer attachment list, keyed the same way the framebuffer sync memo already is, instead of re-walking attachments per draw. ns per draw, DriverBench on a GTX 1660 SUPER, isolated A/B: mc_state_toggle 3151 -> 2397, mc_ubo_range 792 -> 579, mc_vanilla_draw 1111 -> 881, mc_sampler_churn 2019 -> 1676, mc_use_program 5132 -> 4356; every one of the nine cases improved. Against the native driver Espryt now stands at 3.6x on the plain draw path, 2.8x on the per-draw uniform-range path and 2.1x on the blend toggle, from 8.7x / 9.1x / 7.2x when this effort began. Unit tests 421/421. |
||
|
|
b3f774d2c0 |
[Fix] (CI): name the EGL vendor library the benchmark job runs on
The benchmark job is the only one that brings a real GL context up - DriverBench dlopens libEGL.so.1 and renders through it - but its apt list only asks for libegl1, which is glvnd's dispatch layer and nothing more. The vendor library behind it, libegl-mesa0, has been arriving as a Recommends of libegl1 rather than because anything asked for it. That is too quiet a dependency for the one job whose whole purpose is running a driver: a base image change, or --no-install-recommends turning up anywhere upstream, would leave eglInitialize with no vendor to dispatch to and fail the job for a reason nothing in the workflow explains. Name it, next to libgl1-mesa-dri, which is listed for exactly the same reason. Verified with a full headless ctest -C Release -L benchmark - no $DISPLAY, no $EGL_PLATFORM, mesa as the only EGL vendor: SanityBench, ProgramBench, BufferBench and DriverBench all pass. |
||
|
|
d524330032 |
[Test] (MG_Benchmark, MG_Util): model four more Minecraft frame patterns in the driver bench
The captured traces contain per-frame patterns the bench did not exercise, and first measurements show two of them are now the worst remaining multipliers - which is exactly what the missing cases were hiding. mc_pass_switch: the 26.2 snapshot switches render targets 132 times a frame and re-declares draw buffers 198 times. Render-target churn is where a Vulkan backend pays for render-pass breaks and where a tiler pays most on device, and no case measured it. mc_state_toggle: Blaze3D brackets batches with blend toggles - 46 enable/disable pairs and 28 blend-func changes per vanilla frame. mc_tex_param: 26.2 re-sets texture parameters 612 times a frame, almost always to the value already in place, so this measures redundant-parameter filtering. mc_use_program: Sodium switches programs 62 times a frame with a mat4 upload on each, roughly one switch per multi-draw. All four live in the shared case file at the measured per-frame rates, so the desktop harness, the on-device harness and the POST screen's Run Bench report comparable numbers. First desktop measurements (ns/op, native / Espryt / Magma): pass_switch 8877 / 18502 / 13896, state_toggle 1182 / 8526 / 8305, tex_param 42 / 102 / 197, use_program 2182 / 10648 / 5096. The state-toggle multiplier - 7x on both backends - is the largest newly exposed gap and the next optimization target. Unit tests 421/421; the Android JNI translation unit compiles against the extended case set. |
||
|
|
f2d210b12d |
[Perf] (MG_Backend): memoise DirectVulkan's per-draw vertex binding resolution
Every draw re-resolved its whole vertex binding array: for each enabled binding, look up the buffer, acquire a slice from the buffer manager, apply the binding's base offset, fill the VkBuffer and offset arrays, bind. In the Minecraft-shaped benchmark the same few hundred vertex array objects cycle for the whole run and each one's answer is stable, so UploadAndBindVertexBuffers was the single largest cost in the backend at 7.9% of the render thread, with AcquireResidentSlice another 3.8% underneath it. The resolved array is now kept per vertex array object and revalidated instead of rebuilt. Validation is two-tier. The vertex array's own configuration version already invalidates its backend vertex-input state, so a changed attribute, format, buffer or base offset yields a different state object - the memo compares both that object's address and its hash, which mixes the bound buffers and the whole layout. What that does not cover is the slice moving underneath an unchanged configuration, so the buffer manager now carries a monotonic epoch that every writer of slice-deciding state bumps: resident storage creation, respecify, sub-data, flush of a mapped range, the promotion and demotion between streamed and resident storage, each fresh arena allocation, and bulk release. The counter is manager-wide and never reset, so a resource created at a recycled address cannot reproduce a value some memo still holds. The miss path was the thing to get right, because the previous attempt in this area regressed the texture-upload and sampler-churn cases by 60-85%: it added a verification pass that re-ran the resolution work it was trying to skip, so every miss paid for it twice. Here a miss is one pointer-keyed lookup and a few stores, and nothing else runs that the full path would not have run anyway. ns per draw, DriverBench on a GTX 1660 SUPER: mc_ubo_range 924 -> 767, mc_vanilla_draw 1346 -> 1227, mc_sampler_churn 1397 -> 1279, mc_sodium_multidraw 3365 -> 3266. Magma is now 4.1x the native driver on the per-draw uniform-range case, from 5.4x when this round started. No case regressed on either backend. Unit tests 421/421. |
||
|
|
fd40960f70 |
[Perf] (MG_Backend): revive DirectGLES's dead framebuffer-sync guard, and stop probing twice
SyncCurrentFBO has an early-out that compares three memos, and it could never fire. One of the three, g_fboBindVersions, was only ever stamped by ForceBindCurrentFBO - which runs from glBlitFramebuffer and the DSA glClearNamedFramebuffer* paths and nowhere else. An application that touches neither leaves that memo at 0 while the binding slot's version is at least 1 from its first glBindFramebuffer, so the first term mismatched forever and the guard was dead code rather than merely too coarse. Every draw therefore re-walked all 40-odd attachment slots and rebuilt the 8-slot snorm/unorm clamp mask for a framebuffer that had not changed since the previous draw. SyncCurrentFBO now stamps all three memos itself, through one helper, on every path that leaves the target synced - including the default-framebuffer "nothing to do" path, which previously returned without stamping anything. The memo is renamed to say what it now records (a sync, not a bind). Instrumenting a throwaway build put it at 539998 hits against 2 misses, the misses being the first bind of each target; it was 0 hits before. Skipping the sync also skips the Bind() inside it, so all eleven call sites were checked: every one issues its own bind afterwards (PrepareForDraw and the glClearBuffer* paths bind Draw, ReadPixels and the CopyTexSubImage paths bind Read, BlitFramebuffer binds both, GetTexImage uses its own scoped binder). The global snorm/unorm clamp masks written inside the sync stay correct because they can only be stale if a different framebuffer was synced as Draw in between, which moves the pointer or slot version and forces the re-sync that rewrites them. InvalidateFramebufferBindingCache now also clears these memos: both its callers mean the ES context may have been reset, and a live early-out must not survive that. Two smaller items in the same pass. StateBackendObjectRegistry kept the backend twin and its liveness weak_ptr in two maps, so every lookup cost two hash probes and the draw path does ten to twenty of them; they are one map with one entry type now, one probe. The weak_ptr check itself is load-bearing and stays - glDeleteVertexArrays followed by glGenVertexArrays recycles heap addresses readily. And SyncNeccessaryBuffers ran the full EnsureBufferResource check once per enabled vertex attribute, which on an interleaved Minecraft-shaped VAO means four to eight times over the same VBO; it is deduplicated per distinct buffer now. ns per draw, DriverBench on a GTX 1660 SUPER, A/B against a build differing only by this diff: mc_vanilla_draw 1403 -> 1113, mc_ubo_range 983 -> 797, mc_sampler_churn 2309 -> 2003, mc_sodium_multidraw 3232 -> 3023. Against the native driver Espryt is now 4.3x on both the plain draw and the per-draw uniform-range case, from 8.7x and 9.1x at the start of this work. Unit tests 421/421. Also replayed all 38 locally-available DirectGLES trace fixtures against a baseline library: every one produced bit-identical ssim and mismatched-pixel counts, including the improved-transparency OIT trace whose scratch clear framebuffer is exactly the draw-buffer hazard the code comments warn about. |
||
|
|
49aab57f03 |
[Perf] (MG_Backend, MG_State): stop re-resolving texture unit bindings on every draw
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. |
||
|
|
62dea3bea4 |
[Perf] (MG_State): answer texture sampling completeness from a memo
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. |
||
|
|
57aeeec053 |
[Perf] (MG_State, MG_Impl, MG_Backend): stop paying per draw and per upload for work already known
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. |
||
|
|
9c0144d24a |
[Test] (MG_Benchmark, MG_Util, MG_Backend, android-plugin): run the driver benchmark on a phone
The Minecraft-shaped driver benchmark could only be run from a desktop shell against a desktop driver, which is the wrong machine: MobileGL exists to run on mobile GPUs, and nothing said what its translation costs there. This puts the same cases on an Android device, both in the plugin's POST screen and from a shell, and adds the native-driver baseline they have to be read against. The cases move into DriverBenchCases.inc so both harnesses run byte-identical bodies - the desktop program resolving entry points from one EGL provider, and DriverBenchJni.cpp calling MobileGL's frontend in-process. The JNI file binds every gl*/egl* name to MG_Impl by macro rather than by linkage: this library legitimately has the platform libEGL and libGLESv3 in its own lookup scope, and a benchmark that quietly measured the device driver instead of the translation layer would have looked like very good news. Frames are now closed with a fence wait instead of glFinish. MobileGL implements glFinish and glFlush as no-ops, so the old loop timed submit-plus-GPU on a native driver and submit-only on a MobileGL backend, and the two numbers did not describe the same work. To measure a device's own driver the cases needed to be expressible in GLES: ESSL 3.20 twins of the four shaders (chosen at runtime from GL_VERSION, since MobileGL is deliberately still fed desktop GLSL - translating it is the thing under test), a multi-draw hook that loops DrawElementsBaseVertex where the multi-draw entry point does not exist, and an EGL bootstrap that falls back from desktop GL to GLES 3. The binary cross-compiles for arm64 unchanged. BenchService hosts each run in its own process and exits afterwards. That is not caution: the backend is latched from MOBILEGL_BACKEND_TYPE at initialization, so Espryt and Magma can never share a process, and Espryt's teardown terminates the process-default EGL display, which would take the POST activity's own EGL objects with it. Running it found that Magma could not create a windowless context on Mali at all - CreateInstance required VK_EXT_headless_surface, which no mobile driver here exposes, and aborted the process. The Xlib path already probes and falls back to a hidden window for the same reason on NVIDIA; Android now probes too and hands the WSI an AImageReader's ANativeWindow, a real producer surface attached to no display whose images are never acquired. DriverPost reports the extension's absence as a WARN so the fallback is visible rather than silent. Measured on a Mali-G77 MC9 (native / Espryt / Magma, ns per operation): 5495 chunk draws 14397 / 36934 / 33763, the 26.2 per-draw uniform-range pattern 13710 / 31205 / 21252, sodium-style multi-draw 256956 / 238389 / 209527. The translation costs about 2.4x per draw here against 5-9x on the desktop, because the mobile driver's own per-call cost dwarfs it - and both backends beat the native driver on multi-draw, which it has to emulate. Desktop unit tests 421/421; the POST screen and both Run Bench buttons verified on the device. |
||
|
|
1e45958e01 |
[Test] (MG_Benchmark): measure the driver work a real Minecraft frame asks for
The benchmark tree had nothing that exercised a driver: SanityBench times std::vector, and the Buffer/Program benches call into MobileGL_s directly, so neither can say what a backend costs against the native driver. This adds a headless EGL client that can, and shapes its cases from measured traces rather than guesses. DriverBench dlopens exactly one EGL provider - the system libEGL.so.1, or a libMobileGL.so with MOBILEGL_BACKEND_TYPE selecting Espryt or Magma - so the same binary measures all three stacks with no LD_LIBRARY_PATH shadowing, which matters because MobileGL's own loader has to keep finding the real driver underneath. It renders into its own renderbuffer FBO on a 64x64 pbuffer and paces frames with glFinish, so it needs no window and no compositor. The six mc_* cases replay the per-frame call mix of 30-second render-distance-32 captures of three Minecraft versions, at the rates those captures measured: vanilla 1.21.1 issues 5495 glDrawElements per frame, each preceded by its own glBindVertexArray and glUniform3fv; Fabric+Sodium collapses the same scene into 132 glMultiDrawElementsBaseVertex; the 26.2 snapshot issues 3401 glDrawElementsBaseVertex, each preceded by glBindBufferRange + glBindBuffer. The texture case wraps every 16x16 atlas upload in the four glPixelStorei and two glTexParameteri calls Blaze3D re-sets around it, because that wrapper is a large part of what an upload costs a translation layer. One bench frame therefore costs what one real frame of that version costs, and ns_per_op is directly comparable across renderers. run_driver_bench.sh pins __EGL_VENDOR_LIBRARY_FILENAMES and VK_ICD_FILENAMES. Without that, eglGetDisplay(EGL_DEFAULT_DISPLAY) on this glvnd system resolves to Mesa llvmpipe and the "native" numbers silently describe a software rasteriser - the first run of this bench reported 11 us per draw before the pin, versus 250 ns on the real GPU. Verified against the NVIDIA 610.43.03 driver, Espryt and Magma on a GTX 1660 SUPER; the CMake target builds and runs from a clean configure. |
||
|
|
6e6f5268fb |
[Fix] (MG_Backend): let a default-visual X11 window match an alpha-free config
ChooseConfigForSurface prefilters candidate configs with eglChooseConfig requiring EGL_ALPHA_SIZE 8, then tries to match the window's X visual. On NVIDIA's X11 EGL every alpha-8 config lives on the 32-bit ARGB visual, and the default depth-24 TrueColor visual only appears on alpha-0 configs - so for any window created with the default visual the match loop scanned a list that could not contain its visual, fell through to a 32-bit-visual config, and eglCreateWindowSurface failed with EGL_BAD_CONFIG. Keep the alpha-8 list as the first tier and add an alpha-relaxed second tier used only for the visual match; the sizeless fallbacks below still run on the alpha-8 list. Mesa is unaffected (its default-visual configs carry alpha), and a destination-alpha-free default framebuffer is exactly what native GLX hands out on these visuals anyway. Found by running Minecraft through the new GLXImpl on Espryt: NVIDIA EGL also needs EGL_PLATFORM=x11 under a Wayland session or eglGetDisplay itself returns no display, which is a launcher-environment concern, not a library one. |
||
|
|
08f98ad9ce |
[Feat] (MG_Impl): implement GLX 1.4 on the EGL layer so GLFW apps run on Linux
Desktop Linux GL apps (GLFW/LWJGL, glxgears, anything X11) create contexts through GLX, and MobileGL only spoke EGL - the two exported glX symbols were proc-address stubs that could resolve GL entry points but never produce a context. GLXImpl is the missing sibling of WGLImpl/CGLImpl: the same window-system-binding pattern, calling the internal MG_Impl::EGLImpl namespace directly. The surface covers exactly what GLFW 3.4 resolves via dlsym plus the legacy visual API: FBConfig enumeration mirrors the two EGLState configs (stencil-8 first so stencil-wanting choosers land on it), glXGetVisualFromFBConfig answers with the screen's default visual (falling back to any 24-bit TrueColor one), and glXCreateContextAttribsARB maps the ARB attribs onto EGL context attribs the way WGL's Ext_CreateContextAttribsARB does - profile mask only emitted for 3.2+ or an explicit profile request, since that bit is what keys MobileGL's relaxed-semantics compatibility mode. Legacy glXCreateContext/CreateNewContext hand out 3.3 compatibility contexts, matching wglCreateContext. Drawables follow the WGL HWND model: the GLXWindow is the X window itself, the EGL window surface is created lazily on first MakeCurrent and cached per XID, and the GLX layer owns size discovery per the platform-layer contract - it pushes changes through EGLImpl::ResizePlatformWindowSurface, polling XGetGeometry on MakeCurrent and on swaps throttled to 250ms so a fast-swapping app is not paying a server round trip per frame. libX11 is dlopen'd at runtime like everywhere else in the tree; Xlib.h is already in every TU via the vulkan include, so XVisualInfo gets an ABI mirror struct (Xutil.h needs the Bool and Status macros that Includes.h deliberately pops) and the caller's XFree pairs with our malloc. glXGetProcAddress now resolves glX names from the export table before falling through to the shared GL resolver, which previously returned nullptr for every glX extension entry point - GLFW requires glXCreateContextAttribsARB and glXSwapIntervalEXT to arrive that way. Verified with a smoke test replaying GLFW's exact call sequence (dlsym-only resolution, manual FBConfig filtering, 3.2 core forward-compatible context, glXCreateWindow, 60 swapped frames, clean glGetError) on both backends against the real NVIDIA driver, then with Minecraft 1.21.1, 1.21.4+Fabric+Sodium and 26.2-snapshot-6 reaching in-world rendering on both Espryt and Magma. |
||
|
|
d39a706d57 |
[Perf] (MG_Backend): stop paying for descriptor slots and mip barriers nobody asked for
Five independent bits of per-draw and per-operation waste in the DirectVulkan backend, all removing work whose answer was already known. The per-draw descriptor walk iterated all 256 slots of bindingKinds to find the one to eight bindings a real GL program declares, because that vector is sized to the binding cap rather than to the program. Reflection now records the bindings it actually assigned, and the draw path iterates that. It is built at the end of ReflectLayout, not where bindingKinds is sized - at that point the vector is only zero-initialised and the kinds are assigned further down, so a list built there would be empty. It has to stay ascending: Vulkan consumes pDynamicOffsets in binding order and the writer pushes them in iteration order, so an unordered list would silently mis-pair dynamic offsets with their uniform blocks. Descriptor pools were sized maxSets * the 256-binding cap, declaring 81,920 descriptors per pool and 245,760 across the frames in flight, for sets that hold what shader reflection found. Sized from eight now; an outlier program is absorbed by the VK_ERROR_OUT_OF_POOL_MEMORY path that already exists, which works because pool sizes are aggregate budgets rather than per-set limits. TrackLiveResource swept the whole live-buffer vector on every insert once it passed 256 entries, and when the buffers are all live the sweep removes nothing and the vector grows by one - so creating N live buffers cost about N^2/2 expired() checks. It sweeps on a doubling watermark now, with the same reclamation semantics. GenerateMipmap transitioned each destination level individually inside its loop, but every generated level starts in the same layout and the loop only moves a level out of TRANSFER_DST after writing it, so the whole range can be prepared in one barrier - 3(N-1)+1 barrier commands become 2(N-1)+2. Each level is still transitioned to TRANSFER_SRC before it is read, so the dependency between consecutive levels is unchanged. WaitForFrameSerial drained the entire graphics queue, as its own comment admitted. Every submission records the frame serial it was made under, so it now waits on the first fence at or past the requested serial. The narrow path deliberately does not call NotifyDeviceIdle(): that claims every submission has retired, which is only true after a real drain, so it stays on the fallback. Verified with an 8213-case A/B (textures, buffers, queries, mipmaps, uniforms and the whole direct_state_access suite): the Espryt failure list is identical, the Magma failure list differs by one case, and both crash sets are unchanged on Magma. That one case, buffer_storage.map_persistent_draw, does not reproduce in isolation - running the buffer_storage group alone gives byte-identical results on both builds (the same three failures, not including it), and it reports NotSupported when run on its own. It is the same ordering-dependent behaviour this suite shows elsewhere, and the three Espryt crash-set differences are the known copy_image cluster moving chunk position. Flagging rather than hiding it. direct_state_access stays at Espryt 370/371 and Magma 371/371; unit tests 421/421. |
||
|
|
f3d52faad4 |
[Perf] (MG_State, MG_Backend): stop glViewport from evicting a cached VkPipeline
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. |
||
|
|
ba81ee114e |
[Feat] (MG_Backend, MG_Impl, MG_Util): attach one layer of any layered texture on DirectVulkan
Whether a backend can attach a single layer of a texture to a framebuffer was one Bool, so it could only give the most conservative answer any target needed. DirectVulkan therefore declined every layer of every target and direct_state_access.framebuffers_texture_layer_attachment failed with 542 messages across four targets. The three ways a GL layer maps onto Vulkan are independent capabilities, so the flag becomes a per-TextureTarget mask. A 2D or 2D multisample array layer IS a VkImage array layer and needed nothing but the gate opened. A cube map array is one 2D image with arrayLayers = 6 * cubeCount and CUBE_COMPATIBLE, which is a shape VkTextureManager simply did not have - it is declined softly when the depth is not a whole number of cubes or the level is not square, because that function's Bool return exists for unrepresentable shapes and asserting there would abort on ordinary input, GL_PROXY_TEXTURE_CUBE_MAP_ARRAY above all. A 3D texture's layer is a z slice, which needs a 2D-array-compatible image and a per-slice clear, because vkCmdClearColorImage cannot address a subset of a 3D image's slices - a render pass whose only content is its LOAD_OP_CLEAR can, since its attachment is a 2D view over that one slice. VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT is asked for per format and withdrawn per format, mirroring the MUTABLE_FORMAT pattern already in this file: the capability is per format+usage, so a single global probe answers a different question than the one the frontend goes on to ask. Losing it costs per-slice attachment for that format; failing creation would lose the texture. Three things found on the way that are not the headline: glFramebufferTextureLayer, the non-DSA twin, had no gate at all and additionally refused cube map arrays that GL 4.5 requires it to accept. GL 4.6 core 9.2.8 makes the two entry points equivalent, so they now decline in the same places - leaving one ungated is what let an unrepresentable attachment reach the renderer. ComputeFullMipLevelCount takes max(x, y, z), and for every array shape z is the layer count rather than a mip-able axis, so a 4x4 array with 192 layers asked for six mip levels on an image whose legal maximum is three (VUID-VkImageCreateInfo-mipLevels-00958). Only the image's own extent can bound it. lavapipe had been letting that through. A layered GL clear queues layerCount = depth, which is illegal for a VK_IMAGE_TYPE_3D image (VUID-vkCmdClearColorImage-baseArrayLayer-01472 pins it to 0/1, read as the whole mip level) and the old code passed it straight through. Takes framebuffers_texture_layer_attachment green on DirectVulkan, so the whole direct_state_access suite is 371/371 there; Espryt stays 370/371, the remaining case being the fp64 one it declines by design. Known and deliberately not fixed here, with a FIXME at the site: KHR-GL44/45/46.geometry_shader.layered_framebuffer.clear_call_support now fails on DirectVulkan - a layered clear of a 3D texture reads back zeros. Those cases exist only in the GL44+ lists, above the 4.0 this backend reports. An A/B of a 6935-case subset (cube map array, texture storage, framebuffer, 3D, the full DSA suite and the GL33 texture group) is otherwise clean on both backends: 16 cases fixed and none broken on Espryt, 15 fixed and those 2 broken on Magma, and zero difference anywhere at GL 4.0 or below. The FIXME records which causes were already ruled out by bisection so the next reader does not repeat them. |
||
|
|
c8c7b19579 |
[Feat] (MG_Backend, MG_Util): give DirectVulkan GL's provoking vertex
Vulkan's built-in convention is "provoking vertex first"; GL's default is LAST_VERTEX_CONVENTION, and GL derives both flat shading and the transform feedback vertex order from it. DirectVulkan had no way to say so, which is why direct_state_access.queries_functional failed on a value with nothing in its log - the primitives came back counted against a strip recorded in the wrong vertex order. VK_EXT_provoking_vertex is now enabled when present, and the mode is a hashed field of the pipeline payload rather than dynamic state, because it is baked into VkPipelineRasterizationStateCreateInfo: two draws differing only in it must not collide on one cached VkPipeline, or whichever mode built first would stick for the rest of the frame. The pNext is chained only when the mode is not Vulkan's default, so a device without the extension produces a byte-identical VkGraphicsPipelineCreateInfo to before. Two carve-outs, both measured rather than reasoned: A geometry shader already emits its triangles in GL's vertex order, so asking for LAST rotates them a second time and transform_feedback.geometry reads back the wrong vertices. The mode is one pipeline bit and the input-assembler path wants the opposite, so the two cannot both be satisfied: a program that runs a geometry shader and captures transform feedback keeps Vulkan's own convention. That test is read off the program's own shader list, not programObj.rasterizationProducerStage - the latter is filled by the clip-fixup analysis, which does not run for every program and reads Unknown for exactly the programs this guard exists to catch. Both halves are link-time facts folded into programObj.hash, so no pipeline memo can hand back one built for the other mode; keying on IsTransformFeedbackActive() instead would be a live bug, since neither memo key moves on glBeginTransformFeedback. transformFeedbackPreservesProvokingVertex is deliberately not requested. It buys nothing here - the capture order queries_functional needs comes from provokingVertexLast alone - and leaving it off keeps VUID-VkGraphicsPipelineCreateInfo-topology-04884 disarmed, so a TRIANGLE_FAN pipeline may take LAST on any device. The blit pipeline routes through the same selector: it has no flat varying and no capture, but on a device without provokingVertexModePerPipeline a blit left on FIRST inside a render pass whose draws are LAST is an illegal mix. Per the POST rule the new extension gets rows for provokingVertexLast and for the two properties that change what MobileGL can promise. Fixes queries_functional on Magma (370/371). An A/B over a 976-case transform feedback / geometry shader / layered rendering subset of GL30-GL45 is otherwise identical on both backends and additionally takes 14 geometry_shader rendering and layered_rendering cases from failing to passing on Magma. |
||
|
|
0e7692251d |
[Feat] (MG_State, MG_Impl, MG_Util): store a compressed texture image and hand it back
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. |
||
|
|
34f09291da |
[Feat] (MG_State, MG_Backend, MG_Util): feed a 64-bit vertex attribute on DirectVulkan
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. |
||
|
|
3b65e646e1 |
[Fix] (MG_Backend): give every colour attachment its own backend slot on DirectGLES
ES only accepts glDrawBuffers bufs[s] == GL_COLOR_ATTACHMENTs, so a desktop glDrawBuffer(GL_COLOR_ATTACHMENT3) cannot be expressed directly and DirectGLES compacts: it physically relocates the draw buffer's image onto backend point 0 so ES's output-0-to-attachment-0 rule lands on the right image. The clears were therefore always correct. The read side was not. GetBackendAttachmentType derived the attachment-to-point map by searching the draw-buffer array and falling back to the identity point for anything it did not find. That derivation is not injective against the compaction: after clearing attachments 0..7 one at a time, every one of them has been relocated onto point 0 in turn, so a later glReadBuffer(GL_COLOR_ATTACHMENT0) - not a draw buffer any more - takes the identity fallback to point 0 and reads attachment 7's image. Hence the single mismatch, 0.875 where 0 was expected: 7/8 is attachment 7's clear colour. The map is now stored state rather than a re-derivation, and kept a permutation: a draw buffer takes the point ES forces on it, everything else keeps its identity point when that point survived, and an attachment evicted from its identity point is parked on the lowest free one so it stays addressable for glReadBuffer and blits. With identity draw buffers nothing moves and not one extra GL call is issued, which is what keeps ordinary rendering untouched. Two things the permutation depends on. The attachment loop now detaches a colour point whose frontend owner is empty - SyncAttachmentObject only ever attaches, so without this a point handed to an empty attachment would still hold the previous owner's image and hand it back. And QueryReadColorAttachmentInternalFormat asked GL_COLOR_ATTACHMENT0 for the format it sizes the multisample-resolve scratch renderbuffer from; it now asks the point the read buffer actually names, since that is only CA0 when the map happens to be identity. Fixes framebuffers_read_draw_buffer on Espryt. A 5677-case readback and framebuffer subset of GL30-33 stays at zero failures on both backends. |
||
|
|
25b9370815 |
[Fix] (MG_Backend): stop a renderbuffer blit reading a freed image layout
VkRenderPassManager kept m_renderbufferResources on FastSTL's open-addressing UnorderedMap while BlitFramebuffer caches a raw pointer into one of its elements - ResolveColorBlitBinding stores &rbResource->layout - and then calls MaterializePendingClearForRenderbuffer, which looks that same resource up again. FastSTL's operator[] runs its load-factor check before find_key and reallocates the whole bucket array when occupancy crosses it, so even a plain lookup relocates every element; erase only tombstones and never lowers the occupancy, so the doubling keeps firing. After a relocation the cached pointer names freed storage still holding the pre-clear VK_IMAGE_LAYOUT_UNDEFINED, BlitFramebuffer takes its "source image layout is undefined" early return, and the blit is silently dropped - glReadPixels then returns the zero-filled fresh allocation. That is why the failures looked arbitrary: which iteration breaks is pure arithmetic on the table's occupancy, and the observed set (GL_R8 at k=0,1,3,7, GL_R16 at k=6, GL_RG16 at k=4) is exactly the doubling ladder. Padding the map with unrelated live renderbuffers moves the failures to the positions the model predicts and every previously failing format then passes, so nothing else hides behind it. Reordering the materialize ahead of the resolves - the fix ReadPixels got, see the note at its call site - does not cover this, because BlitFramebuffer resolves two bindings and the second resolve still runs after the first pointer is taken. The depth blit, GetOrCreateRenderPass's depthRenderbufferResource and ReadDepthStencilPixels cache the same kind of pointer, so the invariant belongs in the container rather than in a per-call-site ordering rule. m_textureResources was already node-based for exactly this reason; this is the map that was left behind. Fixes renderbuffers_storage_multisample on DirectVulkan. |
||
|
|
4ce808b9f2 |
[Feat] (MG_State, MG_Impl, MG_Backend): let a bound program pipeline actually draw
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. |
||
|
|
5545d31c37 |
[Feat] (MG_Backend, MG_Util): give a cube map array real storage on DirectGLES
TextureCubeMapArray was missing from every storage and upload switch in the DirectGLES texture sync, so a cube map array reached the driver with no storage at all - and from the glFramebufferTextureLayer branch, so attaching one of its layers fell through to glFramebufferTexture2D and raised INVALID_ENUM. Every GL_TEXTURE_CUBE_MAP_ARRAY colour check in direct_state_access.framebuffers_texture_layer_attachment read nothing. ES 3.2 has GL_TEXTURE_CUBE_MAP_ARRAY natively and it stores exactly like a 2D array whose depth is six times the cube count, so each switch gains the case beside Texture2DArray and nothing else changes. 1D arrays join the layer branch for the same reason - their backend image is a 2D array. Per the POST rule the new GLES dependency gets a capability (SupportsTextureCubeMapArray, ES 3.2 core or EXT/OES_texture_cube_map_array) and a DriverPost row saying what a user loses without it. Takes framebuffers_texture_layer_attachment from failing to passing on Espryt. It still fails on DirectVulkan, which declines a layered attachment outright. |
||
|
|
588ddba722 |
[Fix] (MG_Backend): scale a depth blit, keep going after one declines, and mip a 1D texture
Three DirectVulkan gaps found together.
glBlitFramebuffer's depth/stencil path refused any blit whose source and
destination extents differ, because vkCmdCopyImage cannot resize. vkCmdBlitImage
can, and VK_FILTER_NEAREST is the only filter Vulkan allows for depth/stencil
anyway - which is what the GL front end already requires. A same-size pair keeps
the cheaper copy.
Worse, that refusal and four others were `return`, not `continue`, so a
depth/stencil aspect this backend could not handle abandoned the whole function -
including the colour blit that only starts after the aspect loop. The CTS's
scaling blits therefore lost their colour as well, which is why
direct_state_access.framebuffers_blit failed all three of its checks rather than
one.
VulkanRenderer::GenerateMipmap declined GL_TEXTURE_1D. It needed nothing else:
the blit loop derives every offset from the storage extent, and a 1D texture's is
{width, 1, 1}, which is exactly the y and z offsets a 1D image requires.
Also: IsTimerQueryResultReady now asks the query pool before the frame serial.
The pool polls with VK_QUERY_RESULT_WITH_AVAILABILITY_BIT and is the authority;
the frame serial only advances at Present and neither completion notifier will
mark the current serial done, so a timestamp written and fence-waited inside one
GL frame could never be read back within it.
Takes framebuffers_blit and textures_generate_mipmaps from failing to passing on
DirectVulkan. queries_functional still fails there on a value.
|
||
|
|
62301b1061 |
[Fix] (MG_State): let a double-typed varying be captured by transform feedback
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. |
||
|
|
f3a846d336 |
[Docs] (README): carry the 4.2 short-term target into the status note
The compatibility section already said 4.2; the status note at the top of the README still said 3.3, so the two disagreed depending on how far a reader got. |
||
|
|
9cdc82fbdd |
[Fix] (MG_Backend): actually bind the sampler object DirectGLES just synced
BindCurrentTextures' program-driven path synced a bound sampler object's parameters to its backend object and then never put it on the texture unit, so every sampler object was inert and the driver kept sampling with the texture's own parameters - direct_state_access.samplers_functional read black where the sampler's NEAREST filtering should have given red. The bind alone is a regression, and the CTS says so loudly: a sampler left on a unit by an earlier draw keeps being applied, and a multisample texture takes no sampler object at all, so the next draw against one is rejected and all 27 textures_storage_multisample_3d_* cases fail. The sibling path in the same function had an empty else branch where the unbind belonged; it now unbinds, making the two symmetric. Takes samplers_functional from failing to passing on Espryt, with no other case moving in either direction. |
||
|
|
4a9d20c49f |
[Fix] (MG_Backend): resolve a framebuffer attachment's layer in the Vulkan blit bindings
ResolveAttachmentBaseArrayLayer answered zero for everything but a cube map face, so every blit, copy and glReadPixels against a layered attachment read layer zero whatever was attached. It reads the attachment's layer now. A 3D texture needs the other half of the distinction: its image has arrayLayers == 1 and the GL layer is a z slice, which VkBufferImageCopy will not take as a base array layer. BlitImageBinding carries it separately as depthOffset, and the readback copy region uses it as the image offset's z. Takes textures_copy from failing to passing on DirectVulkan, which is what glCopyTextureSubImage3D needs to see the slice the CTS attached rather than slice zero. |
||
|
|
394d1ce748 |
[Feat] (MG_Impl, MG_Util): copy into 1D and 3D textures, and accept the BPTC and ETC2 enums
Two unrelated texture gaps. glCopyTextureSubImage1D and 3D validated their arguments and then did nothing: CopyTexSubImage1D_State and CopyTexSubImage3D_State were empty TODOs and no backend exposes anything but a 2D blit. But a texture's contents live in its CPU storage - the backends sync from it - so the copy does not need a blit at all. CopyReadFramebufferIntoMipmapRegion reads the region out of the read framebuffer through the existing ReadPixels path, in the destination's own canonical client layout so the bytes need no second conversion, and writes them straight into the level. GL 4.6 core 8.6 says the copy ignores pixel-store state and any bound pack buffer, which the borrowed readback does not, so both are neutralised for the duration and restored after. A cube map destination addresses its faces as separate upload targets, so its zoffset picks the target rather than a slice. ConvertGLEnumToTextureInternalFormat had arms for the six generic compressed formats and the four RGTC ones, all resolving to uncompressed storage, but none for BPTC or ETC2/EAC - so glTexImage2D with one of those fourteen enums answered INVALID_ENUM, which was never a legal reply for formats core GL has required since 4.2 and 4.3. They follow the same deviation for the same reason: nothing in this stack can compress them, and uncompressed storage is the trade the RGTC formats already take. Takes textures_compressed_subimage from failing to passing on both backends and textures_copy on Espryt. textures_copy still fails on Magma, where the readback of a layered attachment does not yet resolve the attached layer. |
||
|
|
300b458132 |
[Feat] (MG_State, MG_Impl): give program pipelines their object and their state
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. |
||
|
|
1f1a331a44 |
[Feat] (MG_Impl, MG_State): implement the framebuffer parameter getters and setters
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. |
||
|
|
817091641c |
[Fix] (MG_Impl): give a cube map the storage and the layered attachment it asks for
direct_state_access.framebuffers_texture_attachment threw on both backends, and three separate things were wrong on the way to a cube map framebuffer. glTexStorage1D/2D/3D validated their target by converting it to a single TextureUploadTarget. GL_TEXTURE_CUBE_MAP has no single upload target - it allocates all six faces - so the conversion produced Unknown and a legal glTexStorage2D(GL_TEXTURE_CUBE_MAP, ...) was rejected with INVALID_ENUM, which is where the case threw. The accepted set for these entry points is the dimension's storage targets, which IsTextureStorageTargetForDimension already spells out, so that is what they check now. TextureStorage2D then allocated only the primary upload target, leaving a cube map with one face out of six - cube-incomplete, so every framebuffer it was attached to answered GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT. It allocates every upload target the object has; for every other 2D target that is the same single target as before. ResolveRepresentableFramebufferTextureUploadTarget declined every layered target but 2D array, so glNamedFramebufferTexture on a cube map reported "not represented by the current framebuffer attachment model". Cube maps, cube map arrays, 1D arrays, 2D multisample arrays and 3D textures are all the same shape as the 2D array that already worked - glFramebufferTexture binds the whole texture and the attachment records a representative upload target - so they are all handled now. DirectGLES routes a layered attachment to glFramebufferTexture, which is exactly this. Takes framebuffers_texture_attachment from failing to passing on both backends. |
||
|
|
e64c7c7e65 |
[Fix] (MG_Backend): never back a multisample texture with a one-sample Vulkan image
Every one of the sixty direct_state_access.textures_storage_multisample_2d_* and _3d_* cases failed on DirectVulkan, for every internal format, with no GL error anywhere - a pure data mismatch. The CTS asks for glTextureStorage2DMultisample(tex, samples = 1, ...), which is legal GL, and MobileGL carried the 1 faithfully through to VkImageCreateInfo::samples = VK_SAMPLE_COUNT_1_BIT. It then binds that image to the auxiliary program's sampler2DMS, whose SPIR-V is OpTypeImage with MS = 1. VUID-RuntimeSpirv-samples-08726 forbids exactly that pairing: an MS access must come from an image created with more than one sample. The texelFetch therefore read undefined data - which is why it looked format-independent and raised nothing. GL only promises "at least the requested number of samples", so a multisample texture is now floored at two. GL_TEXTURE_SAMPLES still reports what the application asked for; that is read off the texture object, not off the image. The device-capability round below it is bounded at two for the same reason - letting it land back on one sample would recreate the violation silently for any format whose only supported count is one. Takes all 60 textures_storage_multisample_* cases from failing to passing on DirectVulkan, which goes from 296/371 to 356/371. DirectGLES is untouched. |
||
|
|
dd60ff39ce |
[Feat] (MG_State, MG_Impl, MG_Backend, MG_Util): make the border colour real sampler state
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.
|
||
|
|
96ad7ca0cc |
[Fix] (MG_Impl): asking a renderbuffer for more samples than it has is INVALID_OPERATION
ValidateRenderbufferStorageSamples_State answered INVALID_VALUE for a sample count above GL_MAX_SAMPLES. GL 4.6 core 9.2.4 reserves INVALID_VALUE for a negative count: a count that is well formed but larger than the format can deliver is INVALID_OPERATION, because the argument is fine and the format is what cannot honour it. Takes direct_state_access.renderbuffers_storage_multisample_errors from failing to passing on both backends. |
||
|
|
e80a23eae6 |
[Fix] (MG_Backend): read a multi-slice glGetTexImage off the GPU instead of the CPU shadow
DirectGLES served every multi-slice glGetTexImage from the CPU shadow copy, on the grounds that its scratch FBO can only expose one layer at a time. But the shadow only holds what was uploaded, so any slice that was rendered to rather than written by glTexSubImage came back stale - and a layered framebuffer produces exactly that. The scratch FBO can expose one layer at a time repeatedly. The read now attaches each layer in turn and takes the slice off the GPU, walking the destination over GL_PACK_SKIP_IMAGES / GL_PACK_IMAGE_HEIGHT itself so each per-slice call packs a plain 2D image with the same layout StoreWideRowsToClient computes for the whole stack. The shadow stays as the fallback for the formats a colour attachment cannot represent at all, and for any slice whose attachment comes back incomplete. Takes all 27 remaining direct_state_access.textures_storage_multisample_3d_* cases from failing to passing on Espryt - they render into a TEXTURE_2D_MULTISAMPLE_ARRAY one layer per colour attachment and then read the whole array back. DirectVulkan is untouched. |
||
|
|
088f263495 |
[Feat] (MG_Impl): answer the two query parameters the getters were missing
GetQueryObjectValue implemented GL_QUERY_RESULT_AVAILABLE and GL_QUERY_RESULT and rejected everything else, so direct_state_access.queries_functional threw on its very first probe - GL_QUERY_TARGET - and never reached any of the checks it was written for. GL_QUERY_TARGET is state the object has carried all along; it just had no case. GL_QUERY_RESULT_NO_WAIT is GL_QUERY_RESULT with the backend asked not to block, and it brings a wrinkle the shared getter could not express: when the result has not landed, GL_ARB_query_buffer_object leaves the destination untouched rather than writing a placeholder. GetQueryObjectValue now reports "succeeded but produced no value" through an optional out-parameter, and all five callers - the four buffer forms and the four client-memory forms - skip the write on it. The switch is deliberately widened by exactly these two names: its default INVALID_ENUM is what the GL33 and GL40 query error cases rely on. queries_functional passes on Espryt. On Magma it stops throwing and fails on a value instead, which is a separate problem in the query results themselves. |
||
|
|
3b3b6e5b8b |
[Fix] (MG_Backend): read back the stencil half, and clear an sRGB target to the value asked for
Two reasons a framebuffer's contents came back wrong, both on the read/clear side rather than the write side. Stencil, on both backends. The CTS reads stencil with glReadPixels(GL_STENCIL_INDEX, GL_INT), which is as legal as the unsigned widths, and neither backend accepted it: DirectGLES's ReadPixelsStencilViaNative rejected every signed type, after which the call fell through to a native ES read the driver refuses and nothing was written at all, so the caller kept its zeros; DirectVulkan's pack switch had no GL_INT case, and of the cases it did have only GL_UNSIGNED_INT sourced the stencil plane - GL_FLOAT and GL_UNSIGNED_SHORT emitted a depth value, which is meaningless for a stencil-only image. Both now take the signed and float widths, and DirectVulkan decides "this is a stencil read" once rather than per type. DirectGLES also gains the GL_FLOAT_32_UNSIGNED_INT_24_8_REV fallback a DEPTH32F_STENCIL8 attachment needs, which rejects the 24_8 packed type. sRGB, on DirectVulkan. Every other write path goes through the UNORM twin view while GL_FRAMEBUFFER_SRGB is off, storing the raw value GL asked for, but a deferred clear is materialised with vkCmdClearColorImage - which names the image, so the driver applied the sRGB transfer function and a clear to 0.25 landed at 0.537. PreCompensateSrgbClearColor hands it the linear colour whose encoding is the requested value instead. It is a no-op for non-sRGB destinations, for integer clear encodings, and when GL_FRAMEBUFFER_SRGB is on and GL really does want the encode. Takes renderbuffers_storage from failing to passing on both backends, plus renderbuffers_storage_multisample and framebuffers_blit on Espryt. |
||
|
|
9eda2147b1 |
[Fix] (MG_Impl, MG_Backend): let the backend that can honour a layered attachment have it
NamedFramebufferTextureLayer declined every attachment but layer zero, on both backends. That was right for DirectVulkan, which maps a GL layer onto a Vulkan array layer with no notion of a 3D depth slice, but wrong for DirectGLES: SyncAttachmentObject already routes a layered upload target to glFramebufferTextureLayer with the attachment's layer passed straight through, and array storage already carries the real layer count into glTexStorage3D. The one backend that could render to the layer was being told it could not. The decision now lives in a DynamicBackendParameters flag, so it is the backend that answers rather than the entry point guessing. DirectGLES sets it when the driver resolved glFramebufferTextureLayer; DirectVulkan leaves it false until VkRenderPassManager tells a depth slice from an array layer. framebuffers_texture_layer_attachment's colour checks now pass on Espryt for 3D, 2D array and 2D multisample array textures - the case still fails there on cube map arrays, which DirectGLES gives no storage at all, and on the depth and stencil halves. No case changes on DirectVulkan, which keeps the old behaviour. |
||
|
|
a63699cde6 |
[Fix] (MG_Impl, MG_Backend): reject incomplete cube maps in mipmap generation instead of crashing on them
Both direct_state_access.textures_generate_mipmap* cases crashed DirectVulkan. Two causes, neither of them a broken invariant: glGenerateMipmap and glGenerateTextureMipmap never checked cube completeness, so an incomplete cube map went straight to the backend, which asserts that the texture it is handed is complete. GL 4.6 core 8.14.4 makes that call INVALID_OPERATION - there is no consistent set of faces to filter down - and both entry points now say so through a shared check. VulkanRenderer::GenerateMipmap asserted that the target was one of the four it implements. 1D, 1D array and cube map array are legal GL and the front end passes them through, so meeting one is a gap in this backend's coverage; it now logs and declines, leaving the generated levels unwritten rather than aborting. textures_generate_mipmap_errors passes on both backends now. textures_generate_mipmaps stops crashing but still fails: DirectVulkan does not generate the 1D mip chain the case checks - the frontend's storage allocation gives the levels the right sizes, which is why the case passes when run on its own, but not the descending content the full-run state leaves it looking for. |
||
|
|
765aaec6dc |
[Fix] (MG_Impl, MG_Backend): stop the new layer attachment from reaching backends that cannot back it
Implementing NamedFramebufferTextureLayer made layered attachments reachable for the first time, and direct_state_access.framebuffers_texture_layer_attachment went from Fail to Crash on DirectVulkan. Two separate gaps sat behind it, both of them asserted on rather than reported: - The renderer resolves an attachment's GL layer straight onto a Vulkan array layer. A 3D texture's z-slice therefore lands outside its image, which has one array layer by construction, and the array texture objects are still the one-image stubs in TextureObjectStubs.h, so their image has a single layer whatever GL believes. MaterializePendingClearForTexture tripped over a clear whose layer span was outside the image it was given. - A cube map array has no image shape in VkTextureManager at all, so SyncTextureAndGetDescriptor returns null for it. NamedFramebufferTextureLayer now answers the full error set for every target and layer - which is what took the two error cases green - and then declines to attach anything but layer zero of a non-cube-array texture, through the same RecordUnsupportedFramebufferTextureAttachmentError the by-target entry point already uses. Layer zero of the other targets is the plain first-slice attachment glFramebufferTextureLayer already backs, so it still goes through. SyncTextureResource's assertion on an unsupported texture shape is also gone: it is a gap in this backend's coverage, not a broken invariant, and the code below it already handles the failure by declining the sync. It logs a warning instead. framebuffers_texture_layer_attachment goes back to Fail on DirectVulkan rather than Crash; no case changes in either direction beyond that. |
||
|
|
bcd669bd25 |
[Feat] (MG_Impl): complete the by-name framebuffer attachment and buffer-selection entry points
Four direct_state_access framebuffer cases failed on one shared cause and three local ones. The shared cause: every DSA framebuffer entry point resolved its name through GetNamedFramebufferObject_State, which rejects zero outright. But zero names the default framebuffer to these functions, so glGetNamedFramebufferAttachmentParameteriv, glNamedFramebufferDrawBuffer(s) and glNamedFramebufferReadBuffer answered INVALID_VALUE for every default-framebuffer query the CTS makes. They now resolve zero to the default framebuffer object and tell the two kinds apart explicitly, which is what the accepted-name rules key off anyway. Attachment queries: the accepted attachment names differ between the default framebuffer (FRONT/BACK variants, DEPTH, STENCIL) and a framebuffer object (COLOR_ATTACHMENTi, DEPTH/STENCIL/DEPTH_STENCIL_ATTACHMENT), and a name outside the relevant list is INVALID_ENUM. Both getters share ResolveAttachmentQueryName for that, so the by-target form no longer aliases GL_FRONT onto a framebuffer object's colour attachment 0. The TEXTURE_* parameters are also rejected with INVALID_ENUM when the attached object is a renderbuffer. Buffer selection: naming a buffer that belongs to the other kind of framebuffer is INVALID_OPERATION, not INVALID_ENUM - the enum is accepted, the framebuffer just has no such buffer. glDrawBuffers additionally rejects the multi-buffer names (FRONT, LEFT, RIGHT, FRONT_AND_BACK) with INVALID_ENUM on both kinds, takes BACK only when n is one, and glReadBuffer treats the multi-buffer names as accepted-but-unselectable. Both colour-attachment range checks now go through ValidateColorAttachmentInRange instead of comparing against MAX_DRAW_BUFFERS with an off-by-one. NamedFramebufferTextureLayer was a stub that reported "not represented by the current framebuffer attachment model" for every call, even though the attachment model stores a layer and the by-target glFramebufferTextureLayer already uses it. It is implemented against the same model, with the per-target layer limits and the INVALID_OPERATION-for-a-bad-name rule that separates it from NamedFramebufferTexture. NamedFramebufferTexture itself gained the two checks it lacked: colour attachment range, and a negative level. Takes framebuffers_get_attachment_parameters, framebuffers_get_attachment_parameter_errors, framebuffers_texture_attachment_errors and framebuffers_draw_read_buffers_errors from failing to passing on both backends. |
||
|
|
81604d5596 |
[Feat] (MG_Impl, MG_Test): validate the direct-state-access texture copies
CopyTextureSubImage1D and 3D were do-nothing stubs and the 2D form checked only its effective target, so all 28 conditions in direct_state_access.textures_copy_errors went unreported: level and region bounds, and every read-framebuffer precondition. The read-framebuffer half lands in FramebufferImpl as ValidateReadFramebufferForCopy - incomplete read framebuffer (INVALID_FRAMEBUFFER_OPERATION), a read buffer that names no attachment, and a multisampled read buffer (both INVALID_OPERATION). It decides multisampledness by attachment kind rather than by sample count alone, because a TEXTURE_2D_MULTISAMPLE attachment sets SAMPLE_BUFFERS even when its sample count is one - which is exactly what the CTS attaches, and what a renderbuffer-only check would have missed. The texture half is ValidateCopyTextureSubImage, shared by all three forms; 1D and 3D also get the effective-target rule their form specifies. NOTE: the copy itself is still not implemented for 1D and 3D - CopyTexSubImage1D_State and CopyTexSubImage3D_State remain TODOs and no backend exposes anything but a 2D blit - so direct_state_access.textures_copy stays red. Only the errors are complete, which is what un-stubbing these two entry points buys; both carry a comment saying so. CopyTextureSubImage2DUsesNamedObjectAndRestoresBinding had been passing a storage-less texture and no read framebuffer, which the new validation correctly rejects. It now sets up a legal copy, so it still measures the by-name plumbing it was written for. Takes direct_state_access.textures_copy_errors from failing to passing on both backends. |
||
|
|
31ea6aa5a3 |
[Feat] (MG_Impl): give the by-name texture image queries their error set
glGetTextureImage resolved a texture by name and went straight to the read, skipping every object-level rule glGetTexImage enforces through GetTexImage_State - and on DirectVulkan it skipped the level checks in CopyTextureImageToClientOrPBO_State as well, because that backend answers GetTextureImage itself. Fifteen of the sixteen conditions in direct_state_access.textures_image_query_errors went unreported. The object-level half of that error set now lives in ValidateTextureImageQuery and both entry points run it. Three rules are new rather than merely relocated: - Multisample and buffer textures are not in the accepted target list; neither has a single image to return. - The destination-size checks (bufSize, and the span written into a bound pixel pack buffer) move ahead of the read. They existed, but downstream of it, where any early bail-out - an unmapped level, a pack step that declines the format - swallowed them. Both measure the tightly packed span summed over the object's faces, which is the least a query can produce, so nothing that would have fit is rejected. - IsDepthLikeInternalFormat had no case for StencilIndex8, so a colour client format read back against a stencil-only texture looked like a matching pair. glGetCompressedTextureImage was a do-nothing stub. It validates the name and the level, then reports INVALID_OPERATION: no format MobileGL can hold is compressed, and answering GL_NO_ERROR without writing would hand the caller stale memory - the same reasoning GetCompressedTexImage_State already follows. Takes direct_state_access.textures_image_query_errors from failing to passing on both backends. |
||
|
|
7d6f6603c1 |
[Feat] (MG_Impl): enforce the unpack-buffer rules on texture sub-image uploads
TexSubImage1D/2D/3D_State each carried a TODO for the three INVALID_OPERATION conditions GL 4.6 core 8.5 attaches to sourcing an upload from a bound PIXEL_UNPACK_BUFFER: the store being mapped, an offset that is not a multiple of the size of one datum of `type`, and reads that would run past the end of the store. None of them was checked, so every such call was quietly accepted. ValidatePixelUnpackBufferSource now covers all three and returns true when no unpack buffer is bound, so the callers can run it unconditionally. Persistent mappings stay legal sources, matching what ReadPixels already does on the pack side. The overrun check measures the tightly packed span, which is the smallest the unpack can read - pixel store parameters only ever widen it - so it cannot reject an upload that would have fit. TextureSubImage2D needed the call of its own: unlike its 1D and 3D siblings it does not route through TexSubImage2D_State. Takes direct_state_access.textures_subimage_errors from failing to passing on both backends. |
||
|
|
39c17c0b1b |
[Fix] (MG_Impl): validate the float texture parameter setter and the compressed size query
Two independent gaps in the texture parameter paths, both reported by direct_state_access: TexParameterf_State never ran ValidateTextureParameterForTarget. The integer setter reaches it through TextureParameterObject_State and the scalar float setter through TextureParameterObjectf_State, but glTexParameterfv and glTextureParameterfv funnel every non-vector pname straight into TexParameterf_State - so in float form MobileGL accepted sampler state on a multisample texture, a mipmapping min filter or a REPEAT wrap on a rectangle texture, and a negative TEXTURE_BASE_LEVEL/TEXTURE_MAX_LEVEL, all of which the integer form rejected. It now validates first, passing the same anisotropy-exempt param the by-object float setter uses so the anisotropy range check is not run twice. GL_TEXTURE_COMPRESSED_IMAGE_SIZE answered 0 for every texture. GL 4.6 core 8.11 makes the query INVALID_OPERATION on an image whose internal format is uncompressed and on any proxy target. TextureInternalFormat has no compressed enumerator, so that is every texture MobileGL can hold today; the condition is still written against an IsCompressedTextureFormat predicate so both level getters answer consistently once compressed formats land, and GL_TEXTURE_COMPRESSED now reads from the same predicate instead of a hardcoded false. Takes textures_parameter_setup_errors and textures_level_parameter_errors from failing to passing on both backends. |
||
|
|
88138b48ec |
[Test] (MG_Test): follow the backends to an advertised GL 4.0
Both AdvertisesVoxyRequiredRenderingExtensions cases pinned TargetGLVersion at 3.3, which was the reported version until V_OpenGL40 joined the advertised extension lists. The version assertion is incidental to what these cases are for - Voxy needs the individual ARB extensions, not a version - so it just tracks the new report instead of holding the old one. |
||
|
|
c114ce750b |
[Feat] (MG_Backend): advertise OpenGL 4.0 on both backends
Both backends stopped their advertised version list at V_OpenGL33, so an application - or the CTS - asking what MobileGL supports was told 3.3 even though the 4.0 entry points and the KHR-GL40 suite already pass on both. Adding V_OpenGL40 lets that work be reached through the ordinary version query instead of only through the individual ARB extension strings. |
||
|
|
1ca2d3c0fe |
[Docs] (README): move the short-term target to OpenGL 4.2
The 3.3 line is done - GL30 through GL33 conform on both backends - and the work in flight (GL40, direct state access) is already past it, so the stated short-term target now reads 4.2 and MG_State/MG_Impl are focused there. Performance work joins the focus list alongside the two backends. |
||
|
|
58c17f85a5 |
[Fix] (MG_State, MG_Backend): start TEXTURE_COMPARE_FUNC at LEQUAL
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. |
||
|
|
4873da6844 |
[Fix] (MG_Impl): accept COLOR when invalidating the default framebuffer
The validation added with the invalidation entry points took the default framebuffer's buffers to be only FRONT_LEFT, FRONT_RIGHT, BACK_LEFT, BACK_RIGHT, DEPTH and STENCIL, so a call naming COLOR came back INVALID_ENUM. The by-name forms spell the colour buffer the way glClearNamedFramebuffer does - COLOR, DEPTH, STENCIL - while the target forms use the individual left/right tokens, and both spellings arrive at the same validation, so both sets belong there (GL 4.6 core 17.4.4). Caught by framebuffers_invalidate_data and framebuffers_invalidate_subdata, which had been passing while the entry points were stubs doing nothing at all. Those two plus invalidate_data_and_subdata_errors now pass together on both backends. |
||
|
|
efeb24ff9b |
[Fix] (MG_Impl, MG_State): answer the texture parameters the getters were missing
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. |
||
|
|
18c1a4d586 |
[Feat] (MG_Impl): implement the query getters that write into a buffer object
glGetQueryBufferObjectiv and its three siblings were stubs. They are the ordinary query getters with the destination changed from client memory to a buffer object, so everything about the query itself - the name, whether it is still active, the parameter - is already answered by the shared GetQueryObjectValue, including the errors it raises. What was left is the destination: a negative offset is INVALID_VALUE, a name that is not a buffer object is INVALID_OPERATION, and so is a write that would run past the end of the buffer. The four differ only in the width they store, so they share one template. direct_state_access.queries_errors passes on both backends, putting the group at 4 of 5. queries_functional now reaches further into the test and ends in an unrelated InternalError rather than a plain failure. |
||
|
|
66ac3486e1 |
[Feat] (MG_Impl): validate the framebuffer invalidation entry points
glInvalidateFramebuffer, glInvalidateSubFramebuffer and their two by-name forms were all stubs, so every call - including the malformed ones - returned quietly with no error. These four only grant permission to throw the named attachments' contents away, and keeping them satisfies "the contents become undefined", so the frontend validates the call and leaves the contents alone. Actually discarding is a bandwidth optimisation that would need a backend dependency; it can be added later without changing what any of these promise. The validation is where the real content is. Which tokens name an attachment depends on which framebuffer is affected: the default framebuffer has buffers (FRONT_LEFT and company) and a framebuffer object has attachment points, so a token from the wrong set is INVALID_ENUM. A COLOR_ATTACHMENTm past GL_MAX_COLOR_ATTACHMENTS is different in kind - a well-formed enum naming a point that does not exist - and is INVALID_OPERATION, which the existing colour-attachment range validator already expresses. Negative counts and negative sub-region extents are INVALID_VALUE. direct_state_access.invalidate_data_and_subdata_errors passes on both backends. |