mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-11 13:48:30 +09:00
7c5fc03b2636c97c5112424e4578164dd9e27a9b
429
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7c5fc03b26 | [Fix, Test] (MG_Backend/DirectVulkan): never bind or cache a null pipeline, and name the modules a failed vkCreateGraphicsPipelines rejected | ||
|
|
efa0345c36 | [Fix] (MG_Backend/DirectVulkan): offscreen surfaces never touch a window system - delete the hidden-Xlib fallback, make surface and device-enumeration failures loud, guard zero devices | ||
|
|
f5b8a505ed | [Fix] (MG_Backend/DirectVulkan): keep the transient arena's outgrown buffers alive until frame rewind - the transient drain freed them while frame-serial memos still bound them | ||
|
|
8371365db5 | [Fix] (MG_Backend/DirectVulkan): include the texture enum converter the WARN-level log line needs - VulkanRenderer.cpp did not compile below INFO log level | ||
|
|
b219992ee3 | [Feat, Perf, Test] (MG_Util, MG_Backend, MG_Test): test-lane SPIR-V validation latch; entry-local Private rewrite, in-chain rect lowering and uniform-Location strip | ||
|
|
94233ef928 | [Fix] (MG_Backend/DirectVulkan): drop draws and dispatches when a program has no optimized SPIR-V - a phase-B failure left raw glslang words in GetGeneratedSpirv | ||
|
|
4831387cf0 | [Fix] (MG_State, MG_Backend/DirectVulkan): a program whose SPIR-V job was cancelled faulted on the first glUniform - bounds-check the shadow lookup | ||
|
|
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.
|
||
|
|
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.
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
95a7b17d45 |
[Fix] (DirectVulkan): clear an integer colour buffer with an integer value
glClearBufferiv and glClearBufferuiv flattened their values into the payload's float vector, and every clear was later written into VkClearColorValue::float32. Vulkan reads that union according to the destination image's format rather than converting between its members, so an R8I attachment cleared to -16 received the bit pattern of -16.0f. On top of that, QueueRenderbufferClear copied only the float vector into the pending clear, so even the flattened value was dropped and the attachment kept reading zero - which is what the conformance tests actually observed. The payload now records which of the three entry points supplied the colour and keeps the value in that form, and one helper builds the union member the encoding calls for. GL's rule that a format with no alpha channel reads as one has to be applied in the value's own type, so the "does this format lack alpha" question is now asked separately from the substitution and the helper applies it to whichever member is live. glClear is left on the float path explicitly: ClearFramebufferPayload has no other form. Takes every integer renderbuffer format in direct_state_access.renderbuffers_storage from failing to passing on Magma - 115 reported mismatches down to 20, the rest being the stencil formats Espryt fails too and SRGB8_ALPHA8 - and makes framebuffers_clear pass on both backends. |
||
|
|
19932f9e49 |
[Feat] (MG_Impl, MG_Backend): implement the integer direct state access framebuffer clears
glClearNamedFramebufferiv and glClearNamedFramebufferuiv were stubs, so a clear through them was silently dropped and the attachment kept whatever it held. Their float siblings were already implemented, which is what made the gap look like a rendering bug rather than a missing entry point. Which buffers they accept is narrower than glClearNamedFramebufferfv and differs between the two: signed values clear COLOR or STENCIL, unsigned only COLOR (GL 4.6 core 17.4.3.1). Only the colour buffer is indexed, so a stencil clear naming any drawbuffer other than 0 is INVALID_VALUE rather than merely ignored, and anything else is INVALID_ENUM. Resolving the framebuffer by name goes through the same helper the float forms use, which is what reports INVALID_OPERATION for a name that is neither zero nor an existing framebuffer. Both backends express them the way they already express the float forms: DirectGLES binds the named framebuffer and forwards to glClearBuffer*, Magma queues the payload against the named framebuffer rather than the bound one. direct_state_access.framebuffers_clear_errors passes on both backends, and framebuffers_clear passes on Espryt. Magma still fails that one, for a separate reason on the materialization side rather than in these entry points. |
||
|
|
3d97f6fa8f |
[Fix] (DirectVulkan): decline a draw with no usable fallback instead of aborting
GetFallbackTexture asserted that the target was 2D or rectangle, so a sampler whose texture could not be resolved took the process down whenever it was any other kind. A multisample sampler reaches exactly that path: its texture is reported incomplete, the resolve falls back, and the assert fires. Sixty direct_state_access multisample cases died that way, and because the abort kills the whole process the harness lost the rest of its chunk with them -- one run needed 63 invocations to get through the suite instead of 3. The fallback is a single-sampled 2D image, so it genuinely cannot stand in for a multisample sampler: that descriptor demands a multisample view, and binding this one is invalid usage rather than a degraded picture. So report that no fallback exists and let the caller decline the draw. An unbound or incomplete sampler is an application-level mistake with a defined GL meaning; it is never a reason to abort. The cases still fail -- multisample textures are not yet complete enough to sample -- but they fail as one reported case each. |
||
|
|
bb582203d9 |
[Feat] (MG_Impl, MG_State, MG_Util): attach a buffer texture to a range of its buffer
glTexBufferRange, glTextureBuffer and glTextureBufferRange were all stubs, so a buffer texture could only ever be attached through glTexBuffer -- by binding, and always to the whole buffer. Give the buffer texture the window it is supposed to address. The non-range forms record it as offset 0 with a whole-buffer sentinel rather than the size the buffer happens to have, so a later respecify keeps being followed instead of freezing the texture at yesterday's size. All four entry points now share one attach path, differing only in how they name the texture: by binding for the target forms, by name for the DSA ones. Both backends honour the window: DirectVulkan offsets and clamps the buffer view, DirectGLES uses glTexBufferRange when the texture names a sub-range and keeps plain glTexBuffer for the whole-buffer case, which also works on a driver without the range entry point. GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT reported 0 with a comment explaining that the range entry points were stubbed. It now reports what the device actually requires -- minTexelBufferOffsetAlignment on Vulkan, the driver's own value on GLES -- and the range entry points enforce it. Zero was never a legal answer; the minimum is 1, and an application that trusted it would have built unaligned offsets. |
||
|
|
cb2ba71feb |
[Feat] (DirectVulkan): run the tessellation stages
The backend already turned a tessellation control/evaluation shader into the right VkShaderStage, but nothing downstream knew what to do with it: GL_PATCHES had no topology, so it fell through to the triangle-list default, and the pipeline carried no tessellation state at all. A GL_PATCHES draw therefore ran the vertex and fragment stages over raw triangles. Map GL_PATCHES to VK_PRIMITIVE_TOPOLOGY_PATCH_LIST, carry GL_PATCH_VERTICES into the pipeline as patchControlPoints (part of the key, since two patch sizes are two pipelines), attach VkPipelineTessellationStateCreateInfo for a patch topology only, and enable the tessellationShader device feature. POST reports the feature, because without it a program with a tessellation stage cannot build a pipeline at all and GL_PATCHES draws render nothing. |
||
|
|
6ea7ccdf64 |
[Feat] (DirectVulkan): support an arbitrary primitive restart index
Vulkan restarts only on the fixed all-ones value of the index type, so GL_PRIMITIVE_RESTART with a glPrimitiveRestartIndex of anything else used to hard-fail the draw. GL_PRIMITIVE_RESTART_FIXED_INDEX already matches Vulkan and is untouched. Rewrite the indices into a transient copy instead, substituting the fixed value for the application's. An index that already equals the fixed value would then be indistinguishable from a restart, so it is nudged down by one: it can only be a real index, since the application's restart index is a different number, and the vertex it names is outside any well-defined draw -- whereas leaving it alone would tear the primitive in two. The element array buffer is rewritten whole rather than only the drawn range, because an indirect draw's firstIndex lives in GPU memory and cannot be adjusted from here; every element therefore keeps its position. |
||
|
|
14605723f0 |
[Fix] (DirectVulkan): flag a transform feedback capture as a GPU write
A capture is a GPU write like any shader's, so a later CPU read of the buffer has to wait for it. Only shader storage buffers were flagged, so mapping or reading back a capture buffer could observe whatever the queue had retired so far. Nothing needs copying -- the capture writes land in coherent host-visible storage already -- but coherence only says the writes are visible once they have happened, which is exactly what MarkGpuWritten arranges through the readback op. |
||
|
|
a680611c9f |
[Fix] (DirectVulkan): never stream a buffer whose storage the application holds
AcquirePersistentMap promises the storage it creates is never recreated, because the frontend adopts it in place of the shadow and hands out pointers into it. AcquireStreamedSlice broke that promise: its downgrade path releases the resident storage unconditionally to avoid keeping a second stale copy, so binding such a buffer as a vertex or index source freed the memory the application was still pointing at. It also fed that draw the wrong bytes. The streaming copy is uploaded from the shadow, and a persistently mapped buffer can hold bytes the shadow never saw -- a transform feedback capture writes straight into the resident storage. The next capture into the same buffer then landed in freshly recreated storage while the application kept reading the original, which is how the ping-pong in transform_feedback.draw_xfb_feedbackk_test stalled after its first doubling. Route a persistently mapped resource to the resident path instead, where its single piece of storage is bound directly. |
||
|
|
93224ca406 |
[Fix] (DirectVulkan): make transform feedback writes visible to what reads them
GL makes transform feedback results visible to every later command on their own, with no glMemoryBarrier in between -- unlike shader storage writes. An application replaying a capture with glDrawTransformFeedback is therefore entitled to the captured bytes without asking for them, so the barrier the Vulkan memory model requires has to come from here. It cannot be recorded where the write happens: the capturing draw runs inside a render pass that declares no self-dependency. Flag it there instead and emit the barrier at the next point that could read the buffer -- the following draw's setup, or a readback -- ending the render pass first, the same shape glMemoryBarrier already uses. The destination covers every way a captured buffer comes back: replayed as vertex attributes or indices, read through a uniform or storage binding, sourced as an indirect command, copied out, or mapped. |
||
|
|
fbed4485b7 |
[Fix] (DirectVulkan): key the program cache on the transform feedback capture layout
The program cache is content-hash-shared across GL program names, so its key has to cover everything that changes the modules it stores. The capture layout did not: XfbCaptureDecoratePass bakes XfbBuffer/XfbStride/Offset into the SPIR-V from the frontend's layout, none of which is in the SPIR-V being hashed. Two programs with identical shaders and different glTransformFeedbackVaryings therefore shared one entry, and the first one linked decided how both captured. That is precisely what changing the buffer mode does -- the same varyings recorded with GL_SEPARATE_ATTRIBS instead of GL_INTERLEAVED_ATTRIBS -- so the separate-attribs pass of transform_feedback.draw_xfb_test replayed a capture that was still interleaved into buffer 0. Hash the captured varyings' names, buffer indices and offsets plus the per-buffer strides, and only for a capturing compile, so no other program changes key. |
||
|
|
cff959b2e8 |
[Feat] (DirectVulkan, MG_Util): honour a glVertexAttribDivisor other than 1
Vulkan's VK_VERTEX_INPUT_RATE_INSTANCE advances an attribute once per instance and has no way to say anything else, so every non-zero divisor collapsed to 1: an attribute the application asked to change every three instances changed every one, and KHR-GL40.draw_indirect.basic-drawArrays-instancing and its elements sibling drew the wrong colours from instance one onward. VK_EXT_vertex_attribute_divisor is exactly this state, so it is enabled when the device has it and the per-binding divisors ride into the pipeline through VkPipelineVertexInputDivisorStateCreateInfoEXT. Only divisors other than 1 are listed - 1 is what the plain input rate already means - and they join the layout hash, so two layouts that differ only in a divisor no longer share a pipeline. POST reports the feature either way, because without it the failure is silent and looks like a shader bug: the attribute is fetched, just from the wrong instance. The GLES side gains the two checks this session's other work made load-bearing for the same reason - glPatchParameteri (without it GL_PATCH_VERTICES stays at the driver's 3 and a patch draw of any other size renders nothing) and the transform feedback object entry points (without them a second object cannot open a capture while the first is paused). KHR-GL40.draw_indirect on Magma: 70/70 but for the arbitrary primitive-restart index, which Vulkan cannot express at all. |
||
|
|
a50b2c422b |
[Fix] (DirectVulkan): submit a generated mip chain before a later upload can overtake it
Texture uploads go out on a command buffer of their own the moment they happen, while glGenerateMipmap records its blit chain into the frame's command buffer, which is not submitted until the frame ends. So a glTexSubImage2D into a level that was just generated reached the GPU FIRST and the blits then wrote over it. KHR-GL40.texture_gather.base-level does exactly that - generates the chain, then writes the texels it is going to sample into level 1 and points TEXTURE_BASE_LEVEL at it - and read back the generated content instead of what it had written. The image view, the mip range and the upload itself were all correct; only their order on the GPU was not. This is the same hazard the mip-chain-growth recreate above already flushes for, from the other side: there the recorded work had to reach the GPU before an out-of-band copy read the image, here before an out-of-band copy writes it. Submitting at the end of the generation orders every upload that can follow. |
||
|
|
28d0af6f04 |
[Feat] (MG_Util, DirectGLES, DirectVulkan): normalize rectangle coordinates in the module
Neither target API has GL_TEXTURE_RECTANGLE: ESSL has no rectangle sampler, and Vulkan's SPIR-V environment does not allow Dim::Rect. Both emulate it on a plain 2D texture, and the two differ in exactly one way - a rectangle lookup addresses texels where a 2D one addresses [0,1]. That one difference now lives in one SPIR-V pass, so neither backend has to know about it: every lookup taking normalized coordinates gets its coordinate divided by the size the texture reports, and the image type is then rewritten to 2D. Magma had no rectangle handling at all - it fed Dim::Rect straight to Vulkan, which read the texel coordinates as normalized and sampled the edge, so all fifteen KHR-GL40.texture_gather.*-2drect cases came back holding the clear colour. This replaces the ESSL text rewrite that did the same divide for DirectGLES only. Doing it in the module instead is both shorter and stricter: the pass resolves an operation's image type through the sampled-image and pointer wrappers rather than matching a sampler name in generated source, so it cannot be fooled by an expression where it expected an identifier, and it needs no help from the frontend reflection to know which samplers were rectangles. Still declined, as before: the Dref *sample* forms, whose coordinate carries the compare value in its last component, and the projective ones, where the divide would have to happen after the perspective divide. texelFetch is deliberately untouched - integer texel coordinates mean the same thing on both targets. KHR-GL40.texture_gather: Magma 66 failures -> 2, Espryt stays at 75/75. |
||
|
|
44ee6b66b3 |
[Fix] (MG_State, DirectVulkan): apply the incomplete-texture rule on Magma too
The completeness rule itself is GL's, not a backend's, so it now reads as one question both backends ask - SamplesAsIncompleteTexture(texture, effective sampler) - and each answers in whatever way it already expresses "nothing is bound at this sampler". DirectGLES leaves the native target unbound; Magma has a fallback texture for exactly that case and now routes an incomplete texture to it. The fallback's texel had never been written, so it read whatever its freshly allocated storage held. GL is specific here: an incomplete texture - and a sampler with nothing bound - reads (0, 0, 0, 1). It says so now, which is what makes KHR-GL40.texture_gather.incomplete-texture-last-comp (it gathers the alpha) meaningful rather than accidentally right. |