ValidateRenderbufferStorageSamples_State answered INVALID_VALUE for a sample
count above GL_MAX_SAMPLES. GL 4.6 core 9.2.4 reserves INVALID_VALUE for a
negative count: a count that is well formed but larger than the format can
deliver is INVALID_OPERATION, because the argument is fine and the format is
what cannot honour it.
Takes direct_state_access.renderbuffers_storage_multisample_errors from failing
to passing on both backends.
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.
GetQueryObjectValue implemented GL_QUERY_RESULT_AVAILABLE and GL_QUERY_RESULT and
rejected everything else, so direct_state_access.queries_functional threw on its
very first probe - GL_QUERY_TARGET - and never reached any of the checks it was
written for.
GL_QUERY_TARGET is state the object has carried all along; it just had no case.
GL_QUERY_RESULT_NO_WAIT is GL_QUERY_RESULT with the backend asked not to block,
and it brings a wrinkle the shared getter could not express: when the result has
not landed, GL_ARB_query_buffer_object leaves the destination untouched rather
than writing a placeholder. GetQueryObjectValue now reports "succeeded but
produced no value" through an optional out-parameter, and all five callers - the
four buffer forms and the four client-memory forms - skip the write on it.
The switch is deliberately widened by exactly these two names: its default
INVALID_ENUM is what the GL33 and GL40 query error cases rely on.
queries_functional passes on Espryt. On Magma it stops throwing and fails on a
value instead, which is a separate problem in the query results themselves.
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.
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.
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.
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.
Four direct_state_access framebuffer cases failed on one shared cause and three
local ones.
The shared cause: every DSA framebuffer entry point resolved its name through
GetNamedFramebufferObject_State, which rejects zero outright. But zero names the
default framebuffer to these functions, so glGetNamedFramebufferAttachmentParameteriv,
glNamedFramebufferDrawBuffer(s) and glNamedFramebufferReadBuffer answered
INVALID_VALUE for every default-framebuffer query the CTS makes. They now resolve
zero to the default framebuffer object and tell the two kinds apart explicitly,
which is what the accepted-name rules key off anyway.
Attachment queries: the accepted attachment names differ between the default
framebuffer (FRONT/BACK variants, DEPTH, STENCIL) and a framebuffer object
(COLOR_ATTACHMENTi, DEPTH/STENCIL/DEPTH_STENCIL_ATTACHMENT), and a name outside
the relevant list is INVALID_ENUM. Both getters share ResolveAttachmentQueryName
for that, so the by-target form no longer aliases GL_FRONT onto a framebuffer
object's colour attachment 0. The TEXTURE_* parameters are also rejected with
INVALID_ENUM when the attached object is a renderbuffer.
Buffer selection: naming a buffer that belongs to the other kind of framebuffer
is INVALID_OPERATION, not INVALID_ENUM - the enum is accepted, the framebuffer
just has no such buffer. glDrawBuffers additionally rejects the multi-buffer
names (FRONT, LEFT, RIGHT, FRONT_AND_BACK) with INVALID_ENUM on both kinds,
takes BACK only when n is one, and glReadBuffer treats the multi-buffer names as
accepted-but-unselectable. Both colour-attachment range checks now go through
ValidateColorAttachmentInRange instead of comparing against MAX_DRAW_BUFFERS with
an off-by-one.
NamedFramebufferTextureLayer was a stub that reported "not represented by the
current framebuffer attachment model" for every call, even though the attachment
model stores a layer and the by-target glFramebufferTextureLayer already uses it.
It is implemented against the same model, with the per-target layer limits and
the INVALID_OPERATION-for-a-bad-name rule that separates it from
NamedFramebufferTexture. NamedFramebufferTexture itself gained the two checks it
lacked: colour attachment range, and a negative level.
Takes framebuffers_get_attachment_parameters, framebuffers_get_attachment_parameter_errors,
framebuffers_texture_attachment_errors and framebuffers_draw_read_buffers_errors
from failing to passing on both backends.
The fetch script tries git.hit.moe, then the repo.miawa.cn mirror, and only
then Git LFS, but it bailed out of the mirror loop on the first file no
mirror could serve and then pulled the whole case from GitHub. A case whose
mirrors served every file but one paid GitHub's LFS bandwidth for all of
them.
Collect the files that survived every mirror and every retry instead, and
scope the LFS fallback to just those, matching what the local macOS retrace
helper already does.
CopyTextureSubImage1D and 3D were do-nothing stubs and the 2D form checked only
its effective target, so all 28 conditions in
direct_state_access.textures_copy_errors went unreported: level and region
bounds, and every read-framebuffer precondition.
The read-framebuffer half lands in FramebufferImpl as ValidateReadFramebufferForCopy -
incomplete read framebuffer (INVALID_FRAMEBUFFER_OPERATION), a read buffer that
names no attachment, and a multisampled read buffer (both INVALID_OPERATION). It
decides multisampledness by attachment kind rather than by sample count alone,
because a TEXTURE_2D_MULTISAMPLE attachment sets SAMPLE_BUFFERS even when its
sample count is one - which is exactly what the CTS attaches, and what a
renderbuffer-only check would have missed.
The texture half is ValidateCopyTextureSubImage, shared by all three forms; 1D
and 3D also get the effective-target rule their form specifies.
NOTE: the copy itself is still not implemented for 1D and 3D - CopyTexSubImage1D_State
and CopyTexSubImage3D_State remain TODOs and no backend exposes anything but a
2D blit - so direct_state_access.textures_copy stays red. Only the errors are
complete, which is what un-stubbing these two entry points buys; both carry a
comment saying so.
CopyTextureSubImage2DUsesNamedObjectAndRestoresBinding had been passing a
storage-less texture and no read framebuffer, which the new validation correctly
rejects. It now sets up a legal copy, so it still measures the by-name plumbing
it was written for.
Takes direct_state_access.textures_copy_errors from failing to passing on both
backends.
glGetTextureImage resolved a texture by name and went straight to the read,
skipping every object-level rule glGetTexImage enforces through
GetTexImage_State - and on DirectVulkan it skipped the level checks in
CopyTextureImageToClientOrPBO_State as well, because that backend answers
GetTextureImage itself. Fifteen of the sixteen conditions in
direct_state_access.textures_image_query_errors went unreported.
The object-level half of that error set now lives in ValidateTextureImageQuery
and both entry points run it. Three rules are new rather than merely relocated:
- Multisample and buffer textures are not in the accepted target list; neither
has a single image to return.
- The destination-size checks (bufSize, and the span written into a bound pixel
pack buffer) move ahead of the read. They existed, but downstream of it, where
any early bail-out - an unmapped level, a pack step that declines the format -
swallowed them. Both measure the tightly packed span summed over the object's
faces, which is the least a query can produce, so nothing that would have fit
is rejected.
- IsDepthLikeInternalFormat had no case for StencilIndex8, so a colour client
format read back against a stencil-only texture looked like a matching pair.
glGetCompressedTextureImage was a do-nothing stub. It validates the name and the
level, then reports INVALID_OPERATION: no format MobileGL can hold is
compressed, and answering GL_NO_ERROR without writing would hand the caller
stale memory - the same reasoning GetCompressedTexImage_State already follows.
Takes direct_state_access.textures_image_query_errors from failing to passing on
both backends.
TexSubImage1D/2D/3D_State each carried a TODO for the three INVALID_OPERATION
conditions GL 4.6 core 8.5 attaches to sourcing an upload from a bound
PIXEL_UNPACK_BUFFER: the store being mapped, an offset that is not a multiple of
the size of one datum of `type`, and reads that would run past the end of the
store. None of them was checked, so every such call was quietly accepted.
ValidatePixelUnpackBufferSource now covers all three and returns true when no
unpack buffer is bound, so the callers can run it unconditionally. Persistent
mappings stay legal sources, matching what ReadPixels already does on the pack
side. The overrun check measures the tightly packed span, which is the smallest
the unpack can read - pixel store parameters only ever widen it - so it cannot
reject an upload that would have fit.
TextureSubImage2D needed the call of its own: unlike its 1D and 3D siblings it
does not route through TexSubImage2D_State.
Takes direct_state_access.textures_subimage_errors from failing to passing on
both backends.
Two independent gaps in the texture parameter paths, both reported by
direct_state_access:
TexParameterf_State never ran ValidateTextureParameterForTarget. The integer
setter reaches it through TextureParameterObject_State and the scalar float
setter through TextureParameterObjectf_State, but glTexParameterfv and
glTextureParameterfv funnel every non-vector pname straight into
TexParameterf_State - so in float form MobileGL accepted sampler state on a
multisample texture, a mipmapping min filter or a REPEAT wrap on a rectangle
texture, and a negative TEXTURE_BASE_LEVEL/TEXTURE_MAX_LEVEL, all of which the
integer form rejected. It now validates first, passing the same
anisotropy-exempt param the by-object float setter uses so the anisotropy range
check is not run twice.
GL_TEXTURE_COMPRESSED_IMAGE_SIZE answered 0 for every texture. GL 4.6 core 8.11
makes the query INVALID_OPERATION on an image whose internal format is
uncompressed and on any proxy target. TextureInternalFormat has no compressed
enumerator, so that is every texture MobileGL can hold today; the condition is
still written against an IsCompressedTextureFormat predicate so both level
getters answer consistently once compressed formats land, and
GL_TEXTURE_COMPRESSED now reads from the same predicate instead of a hardcoded
false.
Takes textures_parameter_setup_errors and textures_level_parameter_errors from
failing to passing on both backends.
Both AdvertisesVoxyRequiredRenderingExtensions cases pinned TargetGLVersion at
3.3, which was the reported version until V_OpenGL40 joined the advertised
extension lists. The version assertion is incidental to what these cases are
for - Voxy needs the individual ARB extensions, not a version - so it just
tracks the new report instead of holding the old one.
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.
The 3.3 line is done - GL30 through GL33 conform on both backends - and the
work in flight (GL40, direct state access) is already past it, so the stated
short-term target now reads 4.2 and MG_State/MG_Impl are focused there.
Performance work joins the focus list alongside the two backends.
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.
The validation added with the invalidation entry points took the default framebuffer's
buffers to be only FRONT_LEFT, FRONT_RIGHT, BACK_LEFT, BACK_RIGHT, DEPTH and STENCIL, so a
call naming COLOR came back INVALID_ENUM. The by-name forms spell the colour buffer the way
glClearNamedFramebuffer does - COLOR, DEPTH, STENCIL - while the target forms use the
individual left/right tokens, and both spellings arrive at the same validation, so both sets
belong there (GL 4.6 core 17.4.4).
Caught by framebuffers_invalidate_data and framebuffers_invalidate_subdata, which had been
passing while the entry points were stubs doing nothing at all. Those two plus
invalidate_data_and_subdata_errors now pass together on both backends.
glGetTexParameter and its by-name form rejected several parameters GL 4.6 core table 8.20
lists, with INVALID_ENUM as if the application had made them up. GL_DEPTH_STENCIL_TEXTURE_MODE
was the worst of them: the float setter accepted it, validated it and then threw the value
away, the integer setter did not accept it at all, and neither getter could report it - so
the mode could be set and never read back, and setting it through glTextureParameteri was an
error.
It is real state now, defaulting to DEPTH_COMPONENT, set by both setters and readable from
both getters. GL_TEXTURE_LOD_BIAS was in the same position: settable, not gettable.
The by-name getters reach the target-based ones through a temporary binding rather than the
per-object path, so both had to learn these; the per-object path gained the swizzle
components, the target, the image format compatibility type and the texture-view parameters
at the same time, since they were missing there for the same reason.
direct_state_access.textures_get_set_parameter passes on both backends, and textures_defaults
stops raising an internal error and reports an ordinary failure it can be diagnosed from.
glGetQueryBufferObjectiv and its three siblings were stubs. They are the ordinary query
getters with the destination changed from client memory to a buffer object, so everything
about the query itself - the name, whether it is still active, the parameter - is already
answered by the shared GetQueryObjectValue, including the errors it raises.
What was left is the destination: a negative offset is INVALID_VALUE, a name that is not a
buffer object is INVALID_OPERATION, and so is a write that would run past the end of the
buffer. The four differ only in the width they store, so they share one template.
direct_state_access.queries_errors passes on both backends, putting the group at 4 of 5.
queries_functional now reaches further into the test and ends in an unrelated InternalError
rather than a plain failure.
glInvalidateFramebuffer, glInvalidateSubFramebuffer and their two by-name forms were all
stubs, so every call - including the malformed ones - returned quietly with no error.
These four only grant permission to throw the named attachments' contents away, and keeping
them satisfies "the contents become undefined", so the frontend validates the call and
leaves the contents alone. Actually discarding is a bandwidth optimisation that would need a
backend dependency; it can be added later without changing what any of these promise.
The validation is where the real content is. Which tokens name an attachment depends on
which framebuffer is affected: the default framebuffer has buffers (FRONT_LEFT and company)
and a framebuffer object has attachment points, so a token from the wrong set is
INVALID_ENUM. A COLOR_ATTACHMENTm past GL_MAX_COLOR_ATTACHMENTS is different in kind - a
well-formed enum naming a point that does not exist - and is INVALID_OPERATION, which the
existing colour-attachment range validator already expresses. Negative counts and negative
sub-region extents are INVALID_VALUE.
direct_state_access.invalidate_data_and_subdata_errors passes on both backends.
glMapBufferRange and glMapNamedBufferRange rejected an access of zero with INVALID_ENUM.
Zero is a perfectly well-formed bitfield value - it contains no invalid flags - and what it
violates is the separate rule that a mapping has to ask for read or write access, which GL
reports as INVALID_OPERATION. Both callers already checked that rule immediately after, so
the validator was reporting the wrong error for a case its callers were about to handle
correctly.
direct_state_access.buffers_errors passes, which puts the whole buffers group at 4 of 4 on
both backends.
glClearBufferData and friends accepted exactly two argument triples - R8UI with
UNSIGNED_BYTE and R32UI with UNSIGNED_INT, both through RED_INTEGER - and raised
INVALID_ENUM for everything else. That is most of the entry point missing rather than a
narrow gap: GL takes any of the sized formats in the buffer-texture table, which is what an
application clearing an RGBA8 or R32F buffer uses.
The wrong error also hid the checks behind it. A test clearing a mapped buffer, or one
passing a misaligned offset, never reached those rules because the format tuple was rejected
first, so INVALID_ENUM came back where INVALID_OPERATION or INVALID_VALUE was due - the
validation was there and correct all along, just unreachable.
internalformat now goes through the same table the buffer textures use (shared rather than
written out twice, since it is the same list for the same reason), and format and type
through the ordinary pixel format converters. The element size comes from the internal
format, which is what offset and size have to be multiples of. Note that a bad format or
type here is INVALID_VALUE, not INVALID_ENUM (GL 4.6 core 6.3) - the odd one out among the
enum arguments, and what the conformance tests check for.
The pattern is still replicated verbatim, which is correct while the client layout matches
the internal format - every real caller, and every conformance case. When they differ it now
says so instead of quietly writing a differently-sized pattern.
direct_state_access.buffers_clear and buffers_functional pass on both backends;
buffers_errors is down to one unrelated complaint about glMapNamedBufferRange.
The by-name read was a stub, so it left the caller's buffer untouched and a test comparing
it against a reference saw whatever that memory already held. Its by-target sibling
glGetBufferSubData was already implemented, so this is that function with the buffer
resolved by name instead of through a binding: the same non-negative offset and size check,
the same bound-by-the-buffer's-size check, the same refusal to read a buffer mapped without
GL_MAP_PERSISTENT_BIT, and the same SyncGpuWrites before the download so a GPU-side write
that has not landed yet is not missed.
Resolving by name reports INVALID_OPERATION for a name that is not a buffer, which the
by-target form expresses as "target is bound to no buffer object" instead.
direct_state_access.buffers_get_named_buffer_subdata passes on both backends.
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.
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.
Two unit tests asserted behaviour the conformance tests had since contradicted, so they
were testing MobileGL's old answer rather than GL's.
QueryTest expected glIsQuery to report a name straight out of glGenQueries as a query
object. It is not one: GenQueries reserves names, and they "acquire query state only when
they are first used by calling BeginQuery" (GL 4.6 core 4.2.1). The test now checks that a
reserved name reads FALSE, that BeginQuery is what turns it into an object, and that a
sibling name left untouched stays FALSE. A companion case covers the direct state access
half, where glCreateQueries does create the object outright - which is the whole reason the
two entry points both exist.
The DirectGLES binding test built its texture with glGenTextures and glBindTexture and
nothing else, then expected BindCurrentTextures to bind it natively. A texture with no
image is incomplete and samples as (0, 0, 0, 1), which DirectGLES expresses by leaving the
native target unbound, so the setup no longer produced the binding the test then went on to
clear. It now gives the texture a format and a 1x1 level 0 - one level is the entire mip
chain at that size, so it is complete under any filter - and asserts that directly, so a
future completeness change fails on the setup line instead of on the assertion three calls
later.
glTexStorage2DMultisample and glTexStorage3DMultisample forwarded straight to the
glTexImage*Multisample allocation and stopped there. The allocation is indeed the same;
what the storage forms add is that it is final - TEXTURE_IMMUTABLE_FORMAT becomes TRUE and
any later call on that texture is INVALID_OPERATION (GL 4.6 core 8.19). MobileGL left the
texture mutable forever, so it reported TEXTURE_IMMUTABLE_FORMAT as FALSE and accepted
being respecified any number of times, silently discarding storage a test or an
application had already rendered into.
The by-name forms had no validation of their own either. The target forms get their target
checked when the binding is resolved; reached by name there is no binding, so
glTextureStorage2DMultisample took any texture, any extent and any sample count. It now
rejects a target that belongs to the other entry point (INVALID_OPERATION), extents
outside 1..GL_MAX_TEXTURE_SIZE and a depth past GL_MAX_ARRAY_TEXTURE_LAYERS
(INVALID_VALUE), and a sample count above GL_MAX_SAMPLES (INVALID_OPERATION) - measured
against the limit the getter reports rather than the backend parameter it is derived from,
since the frontend raises that number.
glTextureStorage1D/2D/3D gained the same treatment: a target belonging to a different one
of the three is INVALID_OPERATION, a zero extent is INVALID_VALUE (immutable storage
describes a real image, unlike glTexImage*D where an empty level is legal), and a level
count longer than the level-zero size admits is INVALID_OPERATION. Which dimensions take
part in that mip chain is per target: a 1D array keeps its layer count in height, so its
height does not halve.
Takes direct_state_access.textures_storage_multisample_2d_* from 0 to 30 of 30 on Espryt,
and the whole group from 74.93% to 82.48%. Magma still fails them for a separate reason.
glGetTextureParameter* resolve the texture by name and then hand the work to the
target-based getter, which validates the target it was given. For a buffer texture that
is GL_TEXTURE_BUFFER, and the target form correctly calls that an unaccepted token -
INVALID_ENUM.
By name there is no token to blame. The application named an object that carries none of
the sampler or level state the query reports, which is INVALID_OPERATION (GL 4.6 core
8.11). The four by-name getters check the resolved object before delegating, so the error
describes what the caller actually got wrong.
Fixes direct_state_access.textures_parameter_errors on both backends, taking the group to
74.93% on Espryt and 73.32% on Magma.
The Android and Windows paths each have a skill; the desktop Linux one had only
a runner script and a README section, so it was the least discoverable of the
three despite being the one to reach for while iterating - it needs no device
and no GPU, and a single test group takes seconds rather than hours.
Records what the other two skills cannot: that the toolchain has to be GCC 13+
or Clang 20+ (Clang 18 reports __cpp_concepts as 201907L, which switches
libstdc++'s <expected> off and breaks the shader transpiler), that
EGL_PLATFORM=surfaceless is mandatory for DirectGLES and why the symptom points
at the wrong call, and which of this environment's results are MobileGL's own
versus artefacts of software rendering.
Also states the rule the other skills only imply: report Espryt and Magma
separately. They fail different cases, and one combined number hides which
backend a change moved.
glTextureBuffer and glTextureBufferRange took any internal format the texture enum
converter recognised. A buffer texture accepts a much shorter list than a sampled or a
renderable texture does (GL 4.6 core table 8.16), and it cannot be inferred from either,
so a format like GL_RGB8 was accepted and produced a texture nothing could read.
Two error codes were wrong as well. A texture whose effective target is not
GL_TEXTURE_BUFFER is the wrong object rather than the wrong token, so it is
INVALID_OPERATION. And the range form never checked its range against the buffer it was
attaching, so a size past the end of the buffer was accepted and left the texture
addressing memory the buffer does not own.
Fixes direct_state_access.textures_buffer_errors and textures_buffer_range_errors on both
backends.
The DirectGLES capability probe queried GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT with a bare
glGetIntegerv while every other query in the same function goes through glesFuncs. A bare
call resolves to MobileGL's own exported entry point, which answers that pname out of the
capability table this code is in the middle of filling in, so the value read back was the
default it started from and the driver's real alignment never arrived.
The backend therefore advertised an alignment of 1. An application that trusts that -
which is the only thing it can do - passes glTextureBufferRange an offset the ES driver
cannot honour, and the driver produces a texture that reads as zeros with no error
anywhere. The alignment llvmpipe actually wants is 16.
Takes direct_state_access.textures_buffer_* from 3 to 30 of 30 on DirectGLES, and the
whole DSA group from 66.85% to 74.12%. DirectVulkan was unaffected: its alignment comes
from a Vulkan device limit and was already right.
run_cts_local.py and the mobilegl-desktop VK-GL-CTS target were both in the tree
with nothing describing how to reach them, so the only documented ways to run the
suite needed either an Android device or a Windows box with a GPU. The desktop
Linux path needs neither: lavapipe gives DirectVulkan a headless surface and
Mesa's surfaceless EGL gives DirectGLES a context, so a single test group can be
measured in seconds while working on it.
Records the two things that cost time to find. EGL_PLATFORM=surfaceless is
mandatory for DirectGLES - without a /dev/dri node Mesa fails eglInitialize on
the default display, and MobileGL surfaces that as EGL_BAD_ALLOC from
eglCreatePbufferSurface, which points at the wrong call entirely. And
DirectVulkan's default-framebuffer readback returns zeros here exactly as it does
on Adreno, so that defect is MobileGL's and reproducible without a phone.
The direct_state_access reference table is the measured baseline for the fixes in
this branch, so a later change has something to be compared against.
GL_COLOR_ATTACHMENTn is a token for every n up to 31, but only the first
GL_MAX_COLOR_ATTACHMENTS of them name an attachment point of a framebuffer object. The
enum conversion accepted the whole token range, so attaching a renderbuffer or a texture
to a colour attachment past the limit silently succeeded instead of reporting
INVALID_OPERATION, and the attachment landed in a slot nothing else would ever look at.
glBindVertexBuffers and glVertexArrayVertexBuffers take a range of binding points rather
than one index. A range running past the last binding point is INVALID_OPERATION, which
the per-binding validation could not report: it saw one index at a time and reported the
INVALID_VALUE that a single out-of-range index earns. The range is checked up front now,
before any binding point is touched, so a rejected call also leaves none of them changed.
Takes direct_state_access.vertex_arrays_* to 18 of 19 and fixes
direct_state_access.framebuffers_renderbuffer_attachment_errors on both backends.
glCreateTransformFeedbacks, glTransformFeedbackBufferBase, glTransformFeedbackBufferRange
and the three glGetTransformFeedback* queries were all stubs, so a transform feedback
object could only be configured and inspected by binding it first - the exact thing
direct state access exists to avoid. The queries were the worse half: they returned
nothing and raised no error, so an application could not tell that it had learned
nothing.
glCreateTransformFeedbacks creates the objects outright. glGenTransformFeedbacks only
reserves names, and a reserved name becomes an object when it is first bound
(GL 4.6 core 13.2.1); the DSA form has no bind step to create them from.
The queries and the buffer bindings read and write a named object's state. That state
lives in two places: the context keeps one live copy of the capture bindings and the
active/paused flags for whichever object is bound, and every other object's copy sits in
its saved state until a bind swaps it in. The by-name accessors added to the context
resolve that, so a query for the bound object reads the live copy rather than a stale
save.
GL_TRANSFORM_FEEDBACK_BUFFER_START and _SIZE are answered as zero unless the binding was
made by the range form, matching what the buffer object binding points already do.
Takes direct_state_access.xfb_* from 0 to 4 of 5 on both backends; xfb_functional still
fails on the capture itself, which is a separate defect.
The binding-point half of ARB_vertex_attrib_binding was implemented, but nothing
outside it could see the result. glGetIntegerv answered GL_MAX_VERTEX_ATTRIB_BINDINGS,
GL_MAX_VERTEX_ATTRIB_RELATIVE_OFFSET and GL_MAX_VERTEX_ATTRIB_STRIDE with a hardcoded
0 and a comment saying the entry points were stubs, which they no longer are. An
application that sizes its loops off those limits therefore saw none, and every
"bindingindex must be less than MAX_VERTEX_ATTRIB_BINDINGS" check silently accepted
everything because the limit it validated against was not the one it reported.
The indexed getters answer GL_VERTEX_BINDING_{BUFFER,DIVISOR,OFFSET,STRIDE} from the
bound vertex array now, and the non-indexed getter reports them as indexed-only rather
than returning a fabricated 0.
glVertexAttribPointer is defined in terms of the binding model: it also points the
attribute at its own binding point and gives that point the buffer, the pointer as the
offset and the effective (never zero) stride. MobileGL resolved the pointer form
straight into the flat attribute view and left the binding point untouched, so
GL_VERTEX_BINDING_OFFSET read back 0 for every attribute set up the classic way. The
flat view keeps the raw stride, because GL_VERTEX_ATTRIB_ARRAY_STRIDE reports that
argument verbatim, so the binding point is recorded alongside it rather than resolved
from it. glVertexAttribDivisor likewise now moves the binding point's divisor.
The by-name entry points reject vertex array 0. MobileGL keeps a real object at index 0
for the compatibility paths, so the name validation used to let the default vertex array
through a direct-state-access call that has no such thing.
glVertexAttribFormat and friends validated with the pointer-only subset, which reports
GL_BGRA as an out-of-range size instead of applying the BGRA rules, and never saw
relativeoffset at all. They share the full format validation now, which also grew the
GL_UNSIGNED_INT_10F_11F_11F_REV rules - that type has no DataType of its own, so it has
to be recognised before the conversion turns it into Unknown and reports the wrong error.
glVertexAttribLFormat and glVertexArrayAttribLFormat were stubs. They validate their
arguments now and then report that 64-bit vertex attributes are unsupported, which is
honest; silently accepting a format that can never be used is not.
Takes direct_state_access.vertex_arrays_* from 12 to 17 of 19 on both backends.
glGetVertexArrayiv, glGetVertexArrayIndexediv and glGetVertexArrayIndexed64iv
were stubs, so nothing could read a vertex array's state without binding it
first -- the exact thing direct state access exists to avoid.
They read the state the vertex array already holds. Two accessors were needed for
that: the relative offset and the binding points, which are the binding-point
view the flat per-attribute state was resolved from and cannot be reconstructed
from the resolved form.
Note the index means different things by entry point: for the 32-bit indexed
query it is an attribute, but GL_VERTEX_BINDING_OFFSET names a vertex buffer
binding point directly (GL 4.6 core 10.3.1). GL_VERTEX_ATTRIB_ARRAY_LONG is
answered GL_FALSE throughout, which is honest while 64-bit vertex attributes are
unsupported.
Takes direct_state_access.vertex_arrays_* from 8 to 12 of 19 on Espryt.
GL_VERTEX_BINDING_OFFSET still reads back 0: the query is right but the offset is
not reaching the binding point, which is a separate defect further up.
glGenQueries only reserves names; a name becomes a query object when it is first
used with BeginQuery or QueryCounter (GL 4.6 core 4.2.1). MobileGL created the
live object eagerly at glGenQueries time and glIsQuery reported every reserved
name as an object, with a comment noting the shortcut.
The registry already distinguished the two states -- a target of 0 means the name
has never been used -- so glIsQuery now consults it, and a name that came from
glCreateQueries carries a flag saying it is an object regardless.
glCreateQueries itself was a stub. It creates the objects outright with their
target already fixed, which is the whole point of the DSA form: there is no
binding step to infer the target from later.
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.
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.
It was exported as a stub: it logged a warning and returned, leaving the caller's
buffer untouched. Anything reading back through it saw whatever the destination
already held, which for a freshly allocated vector is zeros -- so every
direct_state_access texture test comparing a readback against reference data
failed without a GL error to explain it.
glReadnPixels is glReadPixels with a bound on how much it may write (GL 4.6 core
18.2.8, originally GL_ARB_robustness) and is identical in every other respect, so
it validates and reads through exactly the same path once the destination is
known to be big enough.
Sizing the read honours the GL_PACK_* state: rows are padded to GL_PACK_ALIGNMENT
and laid out GL_PACK_ROW_LENGTH wide, with the skip parameters offsetting the
first texel. The last row is deliberately not padded -- nothing follows it to
align -- which is what makes a tightly-sized destination legal.
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.
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.
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.
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.
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.
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.
The GL_UNIFORM interface queries and glGetActiveUniform(s)iv describe the same
set of resources in two spellings, but they were reading it from two different
places: the latter from the frontend reflection, the former forwarded straight
to the backend program.
The backend program is not a source of truth for this. It does not exist at all
for a program whose types its shading language cannot express -- a
double-precision uniform has no ESSL form, so the program never links there --
and the interface queries then described a program with no uniforms, which is
how gpu_shader_fp64.fp64.state_query failed.
Route GL_ACTIVE_RESOURCES / GL_MAX_NAME_LENGTH, the resource index, the resource
name and the resource properties for GL_UNIFORM through the same reflection that
already answers glGetActiveUniformsiv, so the two spellings can no longer
disagree and neither depends on the backend having linked. The props that
reflection does not model (GL_ATOMIC_COUNTER_BUFFER_INDEX and the
GL_REFERENCED_BY_* stage bits) still come from the backend, looked up by the
uniform's name so the two index spaces do not have to agree.
GL_MAX_NAME_LENGTH counts the terminator; the stored maximum does not, as every
other caller of GetUniformMaxLength() already accounted for.