Commit Graph
770 Commits
Author SHA1 Message Date
BZLZHH 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.
2026-08-06 06:13:34 -04:00
BZLZHH 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.
2026-08-05 23:12:12 -04:00
BZLZHH 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.
2026-08-05 15:19:37 -04:00
BZLZHH 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.
2026-08-05 12:49:54 -04:00
BZLZHH 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.
2026-08-05 12:07:04 -04:00
BZLZHH 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.
2026-08-05 10:04:30 -04:00
BZLZHH 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.
2026-08-05 08:49:23 -04:00
BZLZHH 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.
2026-08-05 08:24:01 -04:00
BZLZHH 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.
2026-08-05 08:24:01 -04:00
BZLZHH 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.
2026-08-05 07:20:42 -04:00
BZLZHH 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.
2026-08-05 06:58:00 -04:00
BZLZHH 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.
2026-08-05 06:50:13 -04:00
BZLZHH 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.
2026-08-05 06:08:59 -04:00
BZLZHH 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.
2026-08-05 05:56:09 -04:00
BZLZHH 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.
2026-08-05 04:46:15 -04:00
BZLZHH 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.
2026-08-05 04:45:55 -04:00
BZLZHH 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.
2026-08-05 03:55:30 -04:00
BZLZHH 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.
2026-08-05 03:41:23 -04:00
BZLZHH 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.
2026-08-05 03:40:59 -04:00
BZLZHH 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.
2026-08-05 02:55:54 -04:00
BZLZHH 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.
2026-08-05 02:47:27 -04:00
BZLZHH 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.
2026-08-05 13:19:41 +08:00
BZLZHH 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.
2026-08-05 01:13:04 -04:00
BZLZHH 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.
2026-08-04 23:55:43 -04:00
BZLZHH 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.
2026-08-04 23:41:28 -04:00
BZLZHH 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.
2026-08-04 21:00:13 -04:00
BZLZHH 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.
2026-08-04 20:53:53 -04:00
BZLZHH 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.
2026-08-04 20:03:24 -04:00
BZLZHH 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.
2026-08-04 20:00:17 -04:00
BZLZHH 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.
2026-08-04 19:56:28 -04:00
BZLZHH 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.
2026-08-04 19:56:28 -04:00
BZLZHH 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.
2026-08-04 19:47:39 -04:00
BZLZHH 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.
2026-08-04 19:47:39 -04:00
BZLZHH 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.
2026-08-04 19:25:32 -04:00
BZLZHH 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.
2026-08-04 19:18:03 -04:00
BZLZHH 9dcda82d71 [Fix] (DirectVulkan): advertise GL_ARB_get_program_binary on Magma too
The extension and its three entry points are frontend state - no binary format is
exposed on either backend - but only DirectGLES listed it, so on Magma dEQP's loader
still left glProgramParameteri null and KHR-GL40.api.coverage called straight through
the null pointer. The entry point is not core before GL 4.1; this is what exposes it.
2026-08-04 19:10:40 -04:00
BZLZHH 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.
2026-08-04 13:25:40 -04:00
BZLZHH 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.
2026-08-04 13:14:36 -04:00
BZLZHH 28c3cfc1d6 [Fix] (DirectVulkan, MG_State): make a shader-written storage buffer readable on Magma
Reading a buffer a compute shader wrote gave zeros: the frontend shadow that MapBuffer
resolves against is only maintained by uploads, and Magma had no path back. Every
KHR-GL40.texture_gather case ends by dispatching a compute shader into an SSBO and
comparing the mapped result, so 66 of 75 failed on it.

Magma needs no readback: EnsureGpuResidentStorage - the same host-visible coherent
adoption the transform feedback capture already uses - makes the shadow BE the memory
the shader writes, so binding a buffer as a shader storage buffer now adopts it. What
coherence does not give is ordering: the writes are visible once they have happened,
and the CPU was reading before the dispatch had retired. The readback op therefore
submits the recorded work and waits.

That exposed a mistake in the frontend flag this rides on: MarkGpuWritten skipped
GPU-resident buffers, reasoning there was no shadow to refresh. True, but the wait is
still needed - "reconcile with the GPU write" is not always "copy it back", and which
of the two it is belongs to the backend. The flag now only says a write is outstanding;
DirectGLES's readback still skips its persistent-mapped buffers when copying.

KHR-GL40.texture_gather on Magma: 66 failures -> 19 (the rest are rectangle textures,
mipmap completeness and tessellation, all still to do). Espryt stays at 75/75.
2026-08-04 11:55:17 -04:00
BZLZHH 38e04eefae [Fix] (DirectVulkan): size the indirect draw command by GL's struct, not the renderer's
The indirect draw paths bounded their read out of GL_DRAW_INDIRECT_BUFFER - and took
their default stride - from `sizeof(DrawCmdParam)`, this renderer's own draw-parameter
struct. That is not the command GL defines: DrawCmdParam carries two extra members for
bounding vertex-stream conversion and is 24 bytes, where GL's DrawArraysIndirectCommand
is four uint32.

So every glDrawArraysIndirect against a tightly-sized indirect buffer - which is what an
application writes, and what the CTS writes - failed the range check and drew nothing.
It went unnoticed on the elements side only by coincidence: DrawIndexedCmdParam happens
to be exactly the 20 bytes of DrawElementsIndirectCommand.

Both sizes are now named constants of GL's own layout.

KHR-GL40.draw_indirect on Magma: 21 failures -> 3.
2026-08-04 11:47:35 -04:00
BZLZHH fd29cb914e [Fix] (DirectVulkan, MG_State): give each transform feedback object its own capture counters
The frontend half of ARB_transform_feedback2 landed for both backends, but Magma's
capture was still written for the one implicit span GL 3.3 has:

- A paused span kept capturing. VK_EXT_transform_feedback's counter buffers already
  make consecutive draws append, so pausing is simply "do not wrap this draw" - the
  counters keep their values and the next resumed draw carries on where the last
  captured one stopped.
- Those counter buffers were context-wide. Transform feedback objects can each hold an
  open, paused span at the same time - KHR-GL40.transform_feedback.draw_xfb_test keeps
  three - and they were all appending through one set of four slots. Each object now
  gets its own group, handed out on first use; past sixteen objects they share group 0,
  which only matters for concurrently-paused spans.
- The generation that identifies a span is what a backend keys its append state on, so
  it is now part of the per-object state the frontend saves and restores. Without that,
  resuming an object that was paused before another one began looked like a new span
  and restarted its counters at zero.

GL_PRIMITIVES_GENERATED needed one more thing. It counts what the last vertex
processing stage emitted whether or not anything is being captured, but
VK_QUERY_TYPE_TRANSFORM_FEEDBACK_STREAM_EXT only counts what the capture saw - so a
draw made while the span was paused is invisible to it. The frontend now tallies those
draws, and the Vulkan query adds the delta at result time. The correction lives in the
backend that needs it: an ES driver's GL_PRIMITIVES_GENERATED counts them by itself, and
adding it there too would double them.

transform_feedback* on Magma: 4 failures -> 3. Espryt stays at 38/38.
2026-08-04 11:42:44 -04:00
BZLZHH b95fcb7bca [Feat] (MG_State, MG_Impl, DirectGLES): implement glPatchParameteri
GL_PATCH_VERTICES decides how many vertices one tessellation patch consumes, and
glPatchParameteri was a stub - so the value stayed at the driver's default of 3 no
matter what the application asked for. KHR-GL40.texture_gather.gather-tesselation-shader
sets it to 1 and then draws a single patch: with the request dropped the draw had too
few vertices for one patch, produced nothing at all, and the case read back the clear
colour.

The value is context state on both sides and ES 3.2 spells the entry point exactly the
same way, so it is stored in the render state (where glGetIntegerv(GL_PATCH_VERTICES)
now finds it) and forwarded. Validation needs the real bound, so GL_MAX_PATCH_VERTICES
and GL_MAX_TESS_GEN_LEVEL are probed off the host driver alongside the other limits and
answered from there too; the defaults are the GL 4.0 core minimums.

KHR-GL40.texture_gather is now 75/75.
2026-08-04 10:58:14 -04:00
BZLZHH 5fce287de5 [Fix] (DirectGLES): generate a three-channel float mip chain on the CPU
glGenerateMipmap requires the level-0 format to be colour-renderable, and ES has no
colour-renderable three-channel float format at all - so an ES driver rejects
GL_RGB16F and GL_RGB32F where every desktop driver accepts them, and the error was
forwarded to the application. KHR-GL40.texture_gather.plain-gather-float-2d-rgb and
its offset- sibling build their texture that way and fail on the leftover error alone.

The blit-based emulation already used for GL_R11F_G11F_B10F is no help: it renders
level n from level n-1, so it needs exactly the renderability that is missing. But a
format the driver cannot render into is a format nothing can have rendered into
either, which makes the frontend's own copy of the texels authoritative for precisely
these formats. So the chain is box-filtered there and the levels are marked dirty; the
backend sync that follows uploads them like any other texture data.

Deliberately narrow: only the two formats whose texels are a plain float array, and
only when they are what the texture actually holds. Every other format keeps the
driver's behaviour, error included.
2026-08-04 10:54:17 -04:00
BZLZHH ae6949e459 [Fix] (MG_State, DirectGLES): sample a mipmap-incomplete texture as black
A minification filter that reads the mip chain requires every level from the base down
to hold exactly half the previous one's size; a texture that does not is incomplete and
every lookup on it returns (0, 0, 0, 1) (GL 4.6 core 8.17). Nothing checked it.

The ES driver cannot catch this on MobileGL's behalf, which is why it has to be a
frontend rule here: the backend texture is immutable storage allocated from the level
set as it stood, so a level the application later redefined at a different size never
reaches the driver at all, and the ES texture stays complete. That is exactly what
KHR-GL40.texture_gather.incomplete-texture does - it redefines level 1 of a complete
chain as 1x1 - and it read the original contents back.

The check runs where the sampling bindings are established, and an incomplete texture
simply leaves its native target unbound: an unbound ES target samples as (0, 0, 0, 1),
which is the answer GL asks for, with no scratch texture to keep around.

An array texture's layer count is not one of the dimensions that halves, so the
comparison only shrinks the components that belong to the image itself - getting that
wrong turned eight *-2darray cases black.
2026-08-04 10:49:51 -04:00
BZLZHH 5437947240 [Feat] (DirectGLES, MG_Util): normalize the coordinates of a rectangle lookup
A rectangle texture is emulated on an ES 2D texture, and LowerRectImagesForEssl
rewrites the image type in the SPIR-V to match. That is exact only where the lookup
addresses texels directly, which is why the pass declined any module containing a
lookup that takes normalized coordinates - the whole KHR-GL40.texture_gather 2drect
set among them.

The missing half is one divide: a rectangle lookup's coordinate is in texels and the
2D lookup it becomes wants [0,1], so the coordinate has to be divided by the texture's
size. It goes in on the ESSL the transpiler produces, next to the LOD-bias emulation
that already rewrites lookup arguments there, and reads the size back with
textureSize() rather than plumbing a uniform down - the emulated texture is a real ES
2D texture, so the shader can ask it directly.

Only the forms whose argument 1 is the bare coordinate are rewritten - texture,
textureOffset and the three textureGather flavours, which covers the Dref gathers too
because those carry the compare value in a separate argument. texelFetch is
deliberately left alone: its coordinates are integer texels on both targets. The
SPIR-V pass keeps declining everything else, so a projective lookup or a Dref sample
(where the compare value rides in coord.z) still refuses the module instead of
producing something subtly wrong.

Which samplers were declared rectangle is no longer visible in the transpiled source -
they are plain sampler2D by then - so the names come from the frontend program's
reflection.
2026-08-04 10:38:01 -04:00
BZLZHH 2dcc15bb0e [Feat] (MG_Impl, DirectGLES): advertise GL_ARB_get_program_binary with no binary format
glProgramParameteri is not core before GL 4.1, so in the 4.0 context the CTS runs it
only exists through GL_ARB_get_program_binary or GL_ARB_separate_shader_objects.
MobileGL advertised neither, so dEQP's loader left the entry point null - and
KHR-GL40.api.coverage, which registers glProgramParameteri from GL 3.2 upwards, called
straight through the null pointer and took the process down.

GL_NUM_PROGRAM_BINARY_FORMATS was already 0, and the extension explicitly allows an
implementation to support no binary format at all; that is the honest state of things
here, since a MobileGL program is a glslang link plus a per-backend translation with no
serialised form. So the extension is advertised for what it really provides:
glProgramParameteri stores GL_PROGRAM_BINARY_RETRIEVABLE_HINT (reported back by
glGetProgramiv alongside a GL_PROGRAM_BINARY_LENGTH of zero), glGetProgramBinary is the
INVALID_OPERATION the spec requires when that length is zero, and glProgramBinary
rejects every format with INVALID_ENUM and leaves the program's LINK_STATUS false.

Applications that ask for a binary get the documented "no formats" answer and fall
back, which is what they already had to do - only now they can ask.
2026-08-04 10:27:23 -04:00
BZLZHH 76f37a18e6 [Feat] (MG_State, MG_Impl, DirectGLES): transform feedback objects, pause/resume and the special capture names
GL 4.0 folds ARB_transform_feedback2 and _3 into core, and neither existed:
glGenTransformFeedbacks, glBindTransformFeedback, glDeleteTransformFeedbacks,
glIsTransformFeedback, glPause/ResumeTransformFeedback, the whole
glDrawTransformFeedback family and glBegin/EndQueryIndexed were all stubs, and
gl_NextBuffer / gl_SkipComponents1..4 failed the link as "not an output of the vertex
stage". Seven KHR-GL40.transform_feedback* cases failed on it, three of them by
leaving a capture open at deinit and taking the process down.

Objects. The capture state and the indexed GL_TRANSFORM_FEEDBACK_BUFFER bindings are
object state, but the context keeps one live copy of both, which is what every
existing reader - each backend's per-draw sync, the drawing and getter paths - is
written against. Rather than teach all of them about objects, a bind saves the live
copy into the outgoing object and restores the incoming one's. Object 0 is the
default object and needs no seeding; operator[] materialises the rest on first touch.

Pause. A paused span captures nothing, and three rules key off that: a draw is exempt
from the capture primitive-mode match, it feeds PRIMITIVES_GENERATED but not
TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN, and glUseProgram is allowed again (that last
one was already refused for an active capture, correctly for GL 3.3, which has no
pause).

glDrawTransformFeedback replays the vertices the object captured in its last completed
span, recorded at End. "Has a completed span" is tracked separately from that count,
because a completed empty span draws nothing while an object that never ended one is
INVALID_OPERATION. Drawing from the object whose capture is currently open is
deliberately allowed - feeding a result straight into the next span is the point of
KHR-GL40.transform_feedback.draw_xfb_feedbackk_test.

DirectGLES gets a real driver object per frontend object. That is the only reason the
default one would not do: several objects can be paused at once, and a paused span
lives inside the driver's object. The deferred driver-side Begin (still needed - ES
wants the program current and the buffers bound) now also has to be held back while
the span is paused, or a pause taken before the first draw would open the span on that
draw and subject it to the primitive-mode rule it is exempt from.

Special names. gl_NextBuffer and gl_SkipComponents<n> are consumed during varying
resolution and never become varyings of their own, so they only move where the
following ones land - and stay out of the name list the backend declares on its own
driver. ES cannot express the resulting layout at all: it packs every captured varying
into one gap-free record. So when the layout has holes or spans several buffers,
DirectGLES captures into a scratch buffer bound in place of the application's, and
End distributes the records to the offsets GL asked for. Only the bytes a varying
occupies are written, which is exactly what makes the holes keep the contents the
application left there - the property KHR-GL40.transform_feedback3.skip_components
checks.

glBegin/EndQueryIndexed and glGetQueryIndexediv differ from the plain forms only in the
vertex stream they address, so they validate the index and forward. GL_MAX_VERTEX_STREAMS
stays at 1: multi-stream capture needs ARB_gpu_shader5 stream qualifiers that no ES
driver implements, and the CTS cases that need more than one stream check the limit and
skip.

KHR-GL40.transform_feedback, transform_feedback2 and transform_feedback3: 38/38.
2026-08-04 10:18:53 -04:00
BZLZHH 9bf23d7ffd [Fix] (MG_State, DirectGLES): read a shader-written storage buffer back before mapping it
Buffer contents live in a CPU shadow that every read - MapBuffer, MapBufferRange,
GetBufferSubData, CopyBufferSubData - resolves against, and backend transfer ops only
ever push the shadow outwards. Two paths already knew the GPU can write a buffer on
its own and mirrored the result back by hand (ReadPixels into a pixel-pack buffer,
the transform feedback capture at EndTransformFeedback); a shader storage buffer
written by a draw or a dispatch had no such path at all, so the map handed the
application the bytes from before the dispatch.

Nothing exercised it until now because GL 3.3 has no compute stage. Every
KHR-GL40.texture_gather case ends by dispatching a compute shader that writes its
sampled texel into an SSBO and comparing the mapped result, and all 71 read back the
zero-filled shadow.

Adds the missing direction as a backend op: BufferObject::MarkGpuWritten flags a
buffer the GPU may have moved ahead of the shadow, SyncGpuWrites pulls it back at
every read point, and DirectGLES implements the readback with a plain read map of the
ES buffer. The flag is raised where the storage-buffer points are bound for the
upcoming draw or dispatch, which is the last moment the set of exposed buffers is
known, and cleared by the readback - so a buffer nothing writes costs one bool test
per map. Backends that cannot read their storage back leave the op null and keep
today's behaviour; a GPU-resident (coherent persistent) buffer needs nothing, since
its reads already resolve against the memory the shader wrote.

Drops the texture_gather failures from 71/75 to 25/75 with no crashes left.
2026-08-04 09:40:53 -04:00
BZLZHH 3dc6a1b6db [Fix] (MG_Impl, DirectGLES): answer the texture-gather offset limit queries
glGetIntegerv(GL_MIN_PROGRAM_TEXTURE_GATHER_OFFSET) and its GL_MAX_ counterpart fell
through to the default arm of the getter and raised GL_INVALID_ENUM, leaving the
caller's variable untouched - KHR-GL40.texture_gather.api-enums read back the
uninitialised 32764 that happened to be on its stack and failed on the error alone.

Both are core state from GL 4.0 (table 23.53) and from ES 3.1 (table 20.40), so the
value is simply the host driver's, probed alongside the other limits in
FillInGLESCapabilities and carried to the getter through DynamicBackendParameters.
The probe result is widened to the -8/+7 core minimums rather than trusted blindly:
a driver that leaves the out-parameter alone (no ES 3.1, or an enum it ignores) would
otherwise hand us a range narrower than GL 4.0 requires MobileGL to advertise, and
the shaders the CTS builds assume the guaranteed range regardless.
2026-08-04 09:40:15 -04:00
BZLZHH 598c5497b0 [Fix] (DirectVulkan): submit pending work before growing a texture's mip chain
Sizing backings by their defined mip level count gave every level-0-only texture a
single-level image, and left growing it to the recreate-and-preserve path: the new
image is created and the old contents are carried over by a vkCmdCopyImage that
PreserveTextureContentsOnRecreate submits on its own command buffer and waits on
straight away.

Whatever the frame has already recorded into the old image has not been submitted
yet at that point, so that copy reads the texture as it stood before this frame's
writes. GenerateMipmap then descends the whole chain from a stale level 0, and the
composite pass that samples it renders a washed-out frame -
minecraft-1.21.4-fabric-iris-iterationt-in-world (Iris's mipmapped colour target,
the one texture in the trace that grows 1 -> 10 levels) came back at ssim 0.5699
against a 0.99 threshold.

This is the hazard the storage-usage upgrade already flushes for before its own
preserve-copy; growing the mip chain is simply the second trigger of that same
recreate, and it was added without the same ordering guarantee. Flush there too,
gated on a texture whose live image really does carry a short chain, so the submit
happens once per texture and only when a recreate is actually coming.

Keeps the single-level backing and its memory saving; ssim goes back to 0.9992.
2026-08-02 13:09:48 -04:00