Compare commits

..
26 Commits
Author SHA1 Message Date
Claude 7463236b34 [Fix] (MG_Impl): report a buffer texture as the wrong object, not the wrong token
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.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnW6wQYv2Pvrfisqak8UJu
2026-08-05 02:36:31 +00:00
Claude 658e08c918 [Docs] (tools/cts): add the desktop Linux CTS skill
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.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnW6wQYv2Pvrfisqak8UJu
2026-08-05 02:31:46 +00:00
Claude e543d9f7da [Docs] (tools/cts): refresh the DSA reference table for the fixes in this branch
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnW6wQYv2Pvrfisqak8UJu
2026-08-05 02:29:42 +00:00
Claude 31d7d97e86 [Fix] (MG_Impl): apply the buffer texture's own format and range rules
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.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnW6wQYv2Pvrfisqak8UJu
2026-08-05 02:29:25 +00:00
Claude cf89f394c5 [Fix] (MG_Util): ask the ES driver for the texture buffer offset alignment
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.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnW6wQYv2Pvrfisqak8UJu
2026-08-05 02:26:50 +00:00
Claude 2412807afd [Docs] (tools/cts): document the desktop Linux CTS path and the DSA baseline
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.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnW6wQYv2Pvrfisqak8UJu
2026-08-05 02:22:09 +00:00
Claude 9ed287dbc7 [Fix] (MG_Impl): bound a colour attachment and a vertex binding range by the limit
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.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnW6wQYv2Pvrfisqak8UJu
2026-08-05 02:21:13 +00:00
Claude 836306feee [Feat] (MG_Impl, MG_State): implement the direct state access transform feedback API
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.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnW6wQYv2Pvrfisqak8UJu
2026-08-05 02:16:48 +00:00
Claude 126f617428 [Fix] (MG_Impl, MG_State): give the vertex buffer binding points a real state view
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.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnW6wQYv2Pvrfisqak8UJu
2026-08-05 02:16:32 +00:00
BZLZHH 6359b0002b [Feat] (MG_Impl, MG_State): implement the DSA vertex array queries
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.
2026-08-04 21:16:59 -04:00
BZLZHH c186f5f255 [Feat] (MG_Impl): implement glCreateQueries and stop treating a reserved name as a query
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.
2026-08-04 21:07:18 -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 f39e6eb82d [Feat] (MG_Impl): implement glReadnPixels
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.
2026-08-04 20:33:14 -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 90ae0f048c [Fix] (MG_Impl): answer the GL_UNIFORM program interface from the frontend reflection
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.
2026-08-04 19:36:49 -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
58 changed files with 1924 additions and 264 deletions
+1
View File
@@ -191,6 +191,7 @@ set(SOURCE_FILES
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecoratePositionInvariantPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecoratePositionInvariantPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateNoPerspectivePass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateNoPerspectivePass.cpp
+2
View File
@@ -315,6 +315,8 @@ namespace MobileGL {
Int MaxComputeWorkGroupInvocations = 128; Int MaxComputeWorkGroupInvocations = 128;
Int MaxShaderStorageBufferBindings = 8; Int MaxShaderStorageBufferBindings = 8;
Int MaxTextureBufferSize = 65536; Int MaxTextureBufferSize = 65536;
// GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT; 1 means the offset is unconstrained.
Int TextureBufferOffsetAlignment = 1;
Int MaxUniformBufferBindings = 24; Int MaxUniformBufferBindings = 24;
Int MaxUniformBlockSize = 16384; Int MaxUniformBlockSize = 16384;
Int MaxImageUnits = 8; Int MaxImageUnits = 8;
@@ -1112,6 +1112,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
m_dynamicParameters.MaxComputeWorkGroupInvocations = m_GLESCapabilities.MaxComputeWorkGroupInvocations; m_dynamicParameters.MaxComputeWorkGroupInvocations = m_GLESCapabilities.MaxComputeWorkGroupInvocations;
m_dynamicParameters.MaxShaderStorageBufferBindings = m_GLESCapabilities.MaxShaderStorageBufferBindings; m_dynamicParameters.MaxShaderStorageBufferBindings = m_GLESCapabilities.MaxShaderStorageBufferBindings;
m_dynamicParameters.MaxTextureBufferSize = m_GLESCapabilities.MaxTextureBufferSize; m_dynamicParameters.MaxTextureBufferSize = m_GLESCapabilities.MaxTextureBufferSize;
m_dynamicParameters.TextureBufferOffsetAlignment = m_GLESCapabilities.TextureBufferOffsetAlignment;
m_dynamicParameters.MaxUniformBufferBindings = m_GLESCapabilities.MaxUniformBufferBindings; m_dynamicParameters.MaxUniformBufferBindings = m_GLESCapabilities.MaxUniformBufferBindings;
m_dynamicParameters.MaxUniformBlockSize = m_GLESCapabilities.MaxUniformBlockSize; m_dynamicParameters.MaxUniformBlockSize = m_GLESCapabilities.MaxUniformBlockSize;
const Int maxSupportedTextureUnits = const Int maxSupportedTextureUnits =
@@ -1621,9 +1621,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
const auto& effectiveSampler = textureUnit.GetSamplerObject() const auto& effectiveSampler = textureUnit.GetSamplerObject()
? textureUnit.GetSamplerObject() ? textureUnit.GetSamplerObject()
: textureObject->GetSamplerObject(); : textureObject->GetSamplerObject();
const Bool mipmappedFilter = if (MG_State::GLState::SamplesAsIncompleteTexture(textureObject.get(),
effectiveSampler && effectiveSampler->GetMipmapMode() != SamplerMipmapMode::None; effectiveSampler.get())) {
if (!MG_State::GLState::IsMipmapCompleteForFilter(textureObject.get(), mipmappedFilter)) {
continue; continue;
} }
+22 -30
View File
@@ -2232,7 +2232,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
"ID: %u, buffer ID: %u, buffer size: %zu, format: %s", "ID: %u, buffer ID: %u, buffer size: %zu, format: %s",
m_backendTextureId, backendId, buffer->GetSize(), m_backendTextureId, backendId, buffer->GetSize(),
MG_Util::ConvertGLEnumToString(glInternalFormat).c_str()); MG_Util::ConvertGLEnumToString(glInternalFormat).c_str());
g_GLESFuncs.glTexBuffer(GL_TEXTURE_BUFFER, glInternalFormat, backendId); // A texture that names a window of the buffer needs the range form; the
// whole-buffer forms report offset 0 and the buffer's current size, which
// glTexBuffer expresses more directly (and works where the range entry point
// is absent).
const SizeT rangeOffset = textureBufferObject->GetBufferRangeOffset();
const SizeT rangeSize = textureBufferObject->GetBufferRangeSizeInBytes();
if (rangeOffset == 0 && rangeSize == buffer->GetSize()) {
g_GLESFuncs.glTexBuffer(GL_TEXTURE_BUFFER, glInternalFormat, backendId);
} else if (g_GLESFuncs.glTexBufferRange != nullptr) {
g_GLESFuncs.glTexBufferRange(GL_TEXTURE_BUFFER, glInternalFormat, backendId,
static_cast<GLintptr>(rangeOffset),
static_cast<GLsizeiptr>(rangeSize));
} else {
MGLOG_E("Texture buffer %u names a sub-range but the driver has no "
"glTexBufferRange; binding the whole buffer instead",
stateTextureObject->GetExternalIndex());
g_GLESFuncs.glTexBuffer(GL_TEXTURE_BUFFER, glInternalFormat, backendId);
}
DebugImpl::ErrorLopper::Loop( DebugImpl::ErrorLopper::Loop(
[file = __FILE__, line = __LINE__, func = __func__, glInternalFormat, backendId](GLenum err) { [file = __FILE__, line = __LINE__, func = __func__, glInternalFormat, backendId](GLenum err) {
MGLOG_D("%s(%s:%d) glTexBuffer(format=%s, buffer=%u) ES error: %s", MGLOG_D("%s(%s:%d) glTexBuffer(format=%s, buffer=%u) ES error: %s",
@@ -3384,18 +3401,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
} }
// ES has no rectangle sampler, and SPIRV-Cross refuses the whole module rather // ES has no rectangle sampler, and SPIRV-Cross refuses the whole module rather
// than approximating one. Rewriting the type to 2D is exact for a lookup that // than approximating one. The shared pass turns the type into the 2D one and
// takes integer texel coordinates and needs the coordinate divided by the // divides the coordinate of every normalized-coordinate lookup by the texture
// texture size for one that does not - see NormalizeRectSamplerCoordinates // size, which is the whole of the difference between the two.
// below, which the ESSL the transpiler produces goes through. The pass declines
// anything neither step can convert.
Vector<unsigned int> rectLoweredSpirv; Vector<unsigned int> rectLoweredSpirv;
Bool loweredRectImages = false; if (MG_Util::ShaderTranspiler::ShaderCompiler::LowerRectImages(*effectiveSpirv, rectLoweredSpirv) &&
if (MG_Util::ShaderTranspiler::ShaderCompiler::LowerRectImagesForEssl(*effectiveSpirv,
rectLoweredSpirv) &&
!rectLoweredSpirv.empty()) { !rectLoweredSpirv.empty()) {
effectiveSpirv = &rectLoweredSpirv; effectiveSpirv = &rectLoweredSpirv;
loweredRectImages = true;
} }
MG_Util::ShaderTranspiler::SpvcSession spvcSession(*effectiveSpirv, MG_Util::ShaderTranspiler::SpvcSession spvcSession(*effectiveSpirv,
@@ -3432,26 +3444,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
source = ForceFlatIntegerVaryings(source, glShaderType); source = ForceFlatIntegerVaryings(source, glShaderType);
source = BroadcastLegacyFragColor(std::move(source), glShaderType, m_fragColorBroadcastCount); source = BroadcastLegacyFragColor(std::move(source), glShaderType, m_fragColorBroadcastCount);
source = EmulateTextureLodBias(source); source = EmulateTextureLodBias(source);
if (loweredRectImages) {
// The image type is 2D now, so the transpiled lookups address [0,1]; the
// application wrote them in texels. Only the frontend still knows which
// samplers were declared rectangle.
Vector<String> rectSamplerNames;
const Uint uniformCount = stateProgramObject->GetUniformCount();
for (Uint i = 0; i < uniformCount; ++i) {
switch (stateProgramObject->GetActiveUniformType(i)) {
case GL_SAMPLER_2D_RECT:
case GL_SAMPLER_2D_RECT_SHADOW:
case GL_INT_SAMPLER_2D_RECT:
case GL_UNSIGNED_INT_SAMPLER_2D_RECT:
rectSamplerNames.push_back(stateProgramObject->GetActiveUniformName(i));
break;
default:
break;
}
}
source = NormalizeRectSamplerCoordinates(source, rectSamplerNames);
}
source = EmulateBaseInstanceInVertexShader(std::move(source), glShaderType); source = EmulateBaseInstanceInVertexShader(std::move(source), glShaderType);
source = PromoteDrawParameterGlobalsToUniforms(std::move(source), glShaderType); source = PromoteDrawParameterGlobalsToUniforms(std::move(source), glShaderType);
source = ForceSupporterOutput(source); source = ForceSupporterOutput(source);
+1 -1
View File
@@ -279,7 +279,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// arrays as 2D arrays (height 1, layers in depth), and rectangle textures as plain 2D - // arrays as 2D arrays (height 1, layers in depth), and rectangle textures as plain 2D -
// they are single-level and already clamp, so only the non-normalized coordinates differ. // they are single-level and already clamp, so only the non-normalized coordinates differ.
// Must match the shader-side emulation: SPIRV-Cross handles 1D/1D-array itself, and // Must match the shader-side emulation: SPIRV-Cross handles 1D/1D-array itself, and
// ShaderCompiler::LowerRectImagesForEssl rewrites rectangle images (declining any module // ShaderCompiler::LowerRectImages rewrites rectangle images (declining any module
// whose lookups are not integer-coordinate, which SPIRV-Cross then still rejects). // whose lookups are not integer-coordinate, which SPIRV-Cross then still rejects).
inline TextureTarget MapToBackendTextureTarget(TextureTarget target) { inline TextureTarget MapToBackendTextureTarget(TextureTarget target) {
switch (target) { switch (target) {
-60
View File
@@ -597,66 +597,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
return result; return result;
} }
String NormalizeRectSamplerCoordinates(const String& glslCode,
const Vector<String>& rectSamplerNames) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (rectSamplerNames.empty() || glslCode.find("texture") == String::npos) {
return glslCode;
}
// Lookups whose argument 1 is a plain (non-projective) texel-space coordinate on a
// rectangle sampler. texelFetch* is absent on purpose: its coordinates are integer
// texels on the 2D target too, so it already lands in the right place.
static const char* const kRectCoordinateLookups[] = {
"textureGatherOffsets", "textureGatherOffset", "textureGather",
"textureOffset", "texture",
};
String result = glslCode;
// Right to left, so the offsets of the not-yet-rewritten calls stay valid.
for (SizeT scan = result.size(); scan-- > 0;) {
if (result[scan] != 't') continue;
if (scan > 0 && IsIdentifierChar(result[scan - 1])) continue;
SizeT openParen = 0;
Bool matched = false;
for (const char* name : kRectCoordinateLookups) {
const SizeT nameLength = std::strlen(name);
if (result.compare(scan, nameLength, name) != 0) continue;
const SizeT after = result.find_first_not_of(" \t", scan + nameLength);
if (after == String::npos || result[after] != '(') continue;
openParen = after;
matched = true;
break;
}
if (!matched) continue;
const Vector<SizeT> marks = SplitCallArguments(result, openParen);
if (marks.size() < 2) continue; // needs a sampler and a coordinate
const SizeT firstArgStart = result.find_first_not_of(" \t", openParen + 1);
SizeT firstArgEnd = marks.front();
while (firstArgEnd > firstArgStart &&
(result[firstArgEnd - 1] == ' ' || result[firstArgEnd - 1] == '\t')) {
--firstArgEnd;
}
if (firstArgStart == String::npos || firstArgEnd <= firstArgStart) continue;
const String samplerName = result.substr(firstArgStart, firstArgEnd - firstArgStart);
if (std::find(rectSamplerNames.begin(), rectSamplerNames.end(), samplerName) ==
rectSamplerNames.end()) {
continue;
}
// Wrap argument 1: (coord) / vec2(textureSize(sampler, 0)).
const SizeT coordStart = marks[0] + 1;
const SizeT coordEnd = marks[1];
result.insert(coordEnd, String(") / vec2(textureSize(") + samplerName + ", 0)))");
result.insert(coordStart, "((");
}
return result;
}
} // namespace PrgramImpl } // namespace PrgramImpl
namespace Utils { namespace Utils {
-9
View File
@@ -129,15 +129,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
// all have a zero bias is therefore unaffected. Returns the source unchanged when // all have a zero bias is therefore unaffected. Returns the source unchanged when
// there is nothing to rewrite. // there is nothing to rewrite.
String EmulateTextureLodBias(const String& glslCode); String EmulateTextureLodBias(const String& glslCode);
// GL_TEXTURE_RECTANGLE is emulated on an ES 2D texture and LowerRectImagesForEssl
// rewrites the image type to match, but a rectangle lookup addresses texels
// directly while a 2D one addresses [0,1] - so every lookup that takes normalized
// coordinates has to divide by the texture's size. `rectSamplerNames` is the set of
// samplers the program declared as rectangle; texelFetch is left alone (its
// coordinates are unnormalized on both targets) and so is anything projective,
// which LowerRectImagesForEssl still declines outright.
String NormalizeRectSamplerCoordinates(const String& glslCode,
const Vector<String>& rectSamplerNames);
} // namespace PrgramImpl } // namespace PrgramImpl
namespace Utils { namespace Utils {
@@ -530,7 +530,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
E_GL_ARB_shader_draw_parameters, E_GL_ARB_gpu_shader_int64, E_GL_KHR_debug, E_GL_ARB_shader_draw_parameters, E_GL_ARB_gpu_shader_int64, E_GL_KHR_debug,
E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind, E_GL_ARB_shading_language_420pack, E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind, E_GL_ARB_shading_language_420pack,
E_GL_ARB_vertex_attrib_binding, E_GL_ARB_shader_image_size, E_GL_ARB_vertex_attrib_binding, E_GL_ARB_shader_image_size,
E_GL_ARB_explicit_attrib_location}; E_GL_ARB_explicit_attrib_location,
// Advertised with GL_NUM_PROGRAM_BINARY_FORMATS = 0, which the
// extension explicitly permits. It is also the only thing that
// exposes glProgramParameteri before GL 4.1.
E_GL_ARB_get_program_binary};
if (shaderSubgroupSupported && !MG_Config::Features.DisableSubgroup) { if (shaderSubgroupSupported && !MG_Config::Features.DisableSubgroup) {
extensions.push_back(E_GL_KHR_shader_subgroup); extensions.push_back(E_GL_KHR_shader_subgroup);
} }
@@ -776,6 +780,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_dynamicParameters.MaxComputeWorkGroupInvocations = m_vulkanCaps.MaxComputeWorkGroupInvocations; m_dynamicParameters.MaxComputeWorkGroupInvocations = m_vulkanCaps.MaxComputeWorkGroupInvocations;
m_dynamicParameters.MaxShaderStorageBufferBindings = m_vulkanCaps.MaxShaderStorageBufferBindings; m_dynamicParameters.MaxShaderStorageBufferBindings = m_vulkanCaps.MaxShaderStorageBufferBindings;
m_dynamicParameters.MaxTextureBufferSize = m_vulkanCaps.MaxTextureBufferSize; m_dynamicParameters.MaxTextureBufferSize = m_vulkanCaps.MaxTextureBufferSize;
m_dynamicParameters.TextureBufferOffsetAlignment = m_vulkanCaps.TextureBufferOffsetAlignment;
m_dynamicParameters.MaxUniformBufferBindings = m_vulkanCaps.MaxUniformBufferBindings; m_dynamicParameters.MaxUniformBufferBindings = m_vulkanCaps.MaxUniformBufferBindings;
m_dynamicParameters.MaxUniformBlockSize = m_vulkanCaps.MaxUniformBlockSize; m_dynamicParameters.MaxUniformBlockSize = m_vulkanCaps.MaxUniformBlockSize;
m_dynamicParameters.MaxImageUnits = m_dynamicParameters.MaxImageUnits =
@@ -205,6 +205,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.topology, sizeof(payload.topology))); XXHASH_VERIFY(XXH64_update(m_hashState, &payload.topology, sizeof(payload.topology)));
XXHASH_VERIFY( XXHASH_VERIFY(
XXH64_update(m_hashState, &payload.primitiveRestartEnable, sizeof(payload.primitiveRestartEnable))); XXH64_update(m_hashState, &payload.primitiveRestartEnable, sizeof(payload.primitiveRestartEnable)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.patchControlPoints, sizeof(payload.patchControlPoints)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.polygonMode, sizeof(payload.polygonMode))); XXHASH_VERIFY(XXH64_update(m_hashState, &payload.polygonMode, sizeof(payload.polygonMode)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.cullMode, sizeof(payload.cullMode))); XXHASH_VERIFY(XXH64_update(m_hashState, &payload.cullMode, sizeof(payload.cullMode)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.frontFace, sizeof(payload.frontFace))); XXHASH_VERIFY(XXH64_update(m_hashState, &payload.frontFace, sizeof(payload.frontFace)));
@@ -380,6 +381,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
ia.topology = payload.topology; ia.topology = payload.topology;
ia.primitiveRestartEnable = payload.primitiveRestartEnable ? VK_TRUE : VK_FALSE; ia.primitiveRestartEnable = payload.primitiveRestartEnable ? VK_TRUE : VK_FALSE;
// Only a patch topology has a tessellation stage to configure; leaving the pointer null
// otherwise is what the spec expects.
VkPipelineTessellationStateCreateInfo tessellation{VK_STRUCTURE_TYPE_PIPELINE_TESSELLATION_STATE_CREATE_INFO};
tessellation.patchControlPoints = payload.patchControlPoints;
VkPipelineViewportStateCreateInfo vpci{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO}; VkPipelineViewportStateCreateInfo vpci{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO};
vpci.viewportCount = 1; vpci.viewportCount = 1;
vpci.scissorCount = 1; vpci.scissorCount = 1;
@@ -444,6 +450,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
gpi.pStages = payload.stages->data(); gpi.pStages = payload.stages->data();
gpi.pVertexInputState = payload.vertexInputState; gpi.pVertexInputState = payload.vertexInputState;
gpi.pInputAssemblyState = &ia; gpi.pInputAssemblyState = &ia;
gpi.pTessellationState =
payload.topology == VK_PRIMITIVE_TOPOLOGY_PATCH_LIST ? &tessellation : nullptr;
gpi.pViewportState = &vpci; gpi.pViewportState = &vpci;
gpi.pRasterizationState = &raster; gpi.pRasterizationState = &raster;
gpi.pMultisampleState = &ms; gpi.pMultisampleState = &ms;
@@ -30,6 +30,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 subpass = 0; Uint32 subpass = 0;
VkPrimitiveTopology topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; VkPrimitiveTopology topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
Bool primitiveRestartEnable = false; Bool primitiveRestartEnable = false;
// GL_PATCH_VERTICES; only read for a PATCH_LIST topology.
Uint32 patchControlPoints = 3;
VkPolygonMode polygonMode = VK_POLYGON_MODE_FILL; VkPolygonMode polygonMode = VK_POLYGON_MODE_FILL;
VkCullModeFlags cullMode = VK_CULL_MODE_BACK_BIT; VkCullModeFlags cullMode = VK_CULL_MODE_BACK_BIT;
VkFrontFace frontFace = VK_FRONT_FACE_CLOCKWISE; VkFrontFace frontFace = VK_FRONT_FACE_CLOCKWISE;
@@ -1761,6 +1761,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
XXHASH_VERIFY(XXH64_update(m_hashState, &binding, sizeof(binding))); XXHASH_VERIFY(XXH64_update(m_hashState, &binding, sizeof(binding)));
} }
// The transform feedback capture layout is baked into the modules by
// XfbCaptureDecoratePass rather than coming from the SPIR-V, so it has to be part of
// the key: two programs can share every shader and still capture differently, which
// is exactly what changing the buffer mode does (glTransformFeedbackVaryings with the
// same varyings but GL_SEPARATE_ATTRIBS instead of GL_INTERLEAVED_ATTRIBS). Only
// hashed for a capturing compile, so nothing else changes key.
if (flags & CompileOptionBit::XfbCapture) {
for (const auto& varying : program.GetTransformFeedbackVaryings()) {
XXHASH_VERIFY(XXH64_update(m_hashState, varying.name.data(), varying.name.size()));
XXHASH_VERIFY(XXH64_update(m_hashState, &varying.bufferIndex, sizeof(varying.bufferIndex)));
XXHASH_VERIFY(XXH64_update(m_hashState, &varying.offsetBytes, sizeof(varying.offsetBytes)));
}
const SizeT bufferCount = program.GetTransformFeedbackBufferCount();
for (SizeT i = 0; i < bufferCount; ++i) {
const Uint32 stride = program.GetTransformFeedbackStride(static_cast<Uint32>(i));
XXHASH_VERIFY(XXH64_update(m_hashState, &stride, sizeof(stride)));
}
}
HashType hash = XXH64_digest(m_hashState); HashType hash = XXH64_digest(m_hashState);
return hash; return hash;
} }
@@ -2388,6 +2407,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
} }
// Vulkan's SPIR-V environment has no rectangle image dimension, so a
// GL_TEXTURE_RECTANGLE lookup has to become the 2D one the texture is really
// stored as - which addresses [0,1] where the application addressed texels.
{
Vector<Uint> rectLoweredSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::LowerRectImages(moduleSpirvs[i], rectLoweredSpirv) &&
!rectLoweredSpirv.empty()) {
moduleSpirvs[i] = Move(rectLoweredSpirv);
}
}
// GL apps depend on cross-program position invariance for multi-pass equality // GL apps depend on cross-program position invariance for multi-pass equality
// depth tests (MC 26.3's OIT re-draws the cloud geometry with GEQUAL against the // depth tests (MC 26.3's OIT re-draws the cloud geometry with GEQUAL against the
// depth its own first pass wrote); decorate Position outputs Invariant so // depth its own first pass wrote); decorate Position outputs Invariant so
@@ -266,12 +266,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const auto& samplerOverride = textureUnit.GetSamplerObject(); const auto& samplerOverride = textureUnit.GetSamplerObject();
const auto preferredTarget = programObj.samplerTextureTargetByBinding[binding]; const auto preferredTarget = programObj.samplerTextureTargetByBinding[binding];
SharedPtr<MG_State::GLState::ITextureObject> fallbackHolder; SharedPtr<MG_State::GLState::ITextureObject> fallbackHolder;
// A texture that fails the completeness rules for the filter in effect reads
// (0, 0, 0, 1), which is exactly what the fallback texture holds - so it takes the
// same route as a sampler with nothing bound.
if (texture != nullptr &&
MG_State::GLState::SamplesAsIncompleteTexture(
texture, samplerOverride ? samplerOverride.get() : texture->GetSamplerObject().get())) {
texture = nullptr;
}
if (texture == nullptr) { if (texture == nullptr) {
fallbackHolder = GetFallbackTexture(preferredTarget); fallbackHolder = GetFallbackTexture(preferredTarget);
texture = fallbackHolder.get(); texture = fallbackHolder.get();
MOBILEGL_ASSERT(texture != nullptr, if (texture == nullptr) {
"ResolveSamplerDescriptor: no fallback texture available for binding=%u location=%d unit=%d target=%d", MGLOG_E("ResolveSamplerDescriptor: no fallback texture available for binding=%u ('%s') "
binding, location, unit, static_cast<Int>(preferredTarget)); "location=%d unit=%d target=%d",
binding, programObj.samplerNameByBinding[binding].c_str(), location, unit,
static_cast<Int>(preferredTarget));
return false;
}
MGLOG_W( MGLOG_W(
"ResolveSamplerDescriptor: using fallback texture for unbound sampler binding=%u ('%s') location=%d unit=%d target=%d", "ResolveSamplerDescriptor: using fallback texture for unbound sampler binding=%u ('%s') location=%d unit=%d target=%d",
binding, programObj.samplerNameByBinding[binding].c_str(), location, unit, binding, programObj.samplerNameByBinding[binding].c_str(), location, unit,
@@ -592,7 +604,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const VkDeviceSize texelSize = const VkDeviceSize texelSize =
static_cast<VkDeviceSize>(MG_Util::GetSizedInternalFormatSizeInBytes(internalFormat)); static_cast<VkDeviceSize>(MG_Util::GetSizedInternalFormatSizeInBytes(internalFormat));
VkDeviceSize viewRange = slice.size; // glTextureBufferRange addresses a window of the buffer, not all of it; the whole-buffer
// forms report the buffer's current size here, so both go through the same clamp.
const VkDeviceSize rangeOffset = static_cast<VkDeviceSize>(textureBuffer->GetBufferRangeOffset());
const VkDeviceSize rangeSize = static_cast<VkDeviceSize>(textureBuffer->GetBufferRangeSizeInBytes());
VkDeviceSize viewRange = std::min(rangeSize, slice.size > rangeOffset ? slice.size - rangeOffset : 0);
if (texelSize > 0) { if (texelSize > 0) {
viewRange = (viewRange / texelSize) * texelSize; viewRange = (viewRange / texelSize) * texelSize;
} }
@@ -605,7 +621,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
viewInfo.sType = VK_STRUCTURE_TYPE_BUFFER_VIEW_CREATE_INFO; viewInfo.sType = VK_STRUCTURE_TYPE_BUFFER_VIEW_CREATE_INFO;
viewInfo.buffer = slice.buffer; viewInfo.buffer = slice.buffer;
viewInfo.format = vkFormat; viewInfo.format = vkFormat;
viewInfo.offset = slice.offset; viewInfo.offset = slice.offset + rangeOffset;
viewInfo.range = viewRange; viewInfo.range = viewRange;
VkBufferView bufferView = VK_NULL_HANDLE; VkBufferView bufferView = VK_NULL_HANDLE;
@@ -760,15 +776,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
SharedPtr<MG_State::GLState::ITextureObject> UniformManager::GetFallbackTexture(TextureTarget target) const { SharedPtr<MG_State::GLState::ITextureObject> UniformManager::GetFallbackTexture(TextureTarget target) const {
MOBILEGL_ASSERT(target == TextureTarget::Texture2D || target == TextureTarget::TextureRectangle, // The fallback is a single-sampled 2D image, so it can only stand in for a sampler that
"UniformManager::GetFallbackTexture: unsupported fallback target=%d", // would accept one. A multisample sampler in particular cannot: its descriptor demands a
static_cast<Int>(target)); // multisample view, and handing it this one is invalid Vulkan, not a degraded picture.
// Report that there is no fallback and let the caller decline the draw - aborting the
// process over an unbound sampler is never the right answer.
if (target != TextureTarget::Texture2D && target != TextureTarget::TextureRectangle) {
MGLOG_E("UniformManager::GetFallbackTexture: no fallback exists for target=%d",
static_cast<Int>(target));
return nullptr;
}
if (m_fallbackTexture2D == nullptr) { if (m_fallbackTexture2D == nullptr) {
auto fallbackTexture = MakeShared<MG_State::GLState::TextureObject2D>(kFallbackTexture2DExternalIndex); auto fallbackTexture = MakeShared<MG_State::GLState::TextureObject2D>(kFallbackTexture2DExternalIndex);
fallbackTexture->SetInternalFormat(TextureInternalFormat::RGBA8); fallbackTexture->SetInternalFormat(TextureInternalFormat::RGBA8);
fallbackTexture->AllocateStorage(TextureUploadTarget::Texture2D, 0, fallbackTexture->AllocateStorage(TextureUploadTarget::Texture2D, 0,
{.texelSize = {1, 1, 1}, .byteSize = 4}); {.texelSize = {1, 1, 1}, .byteSize = 4});
// (0, 0, 0, 1): what GL reads from a texture that is not complete, and the only
// sensible answer for a sampler with nothing bound.
static Uint8 kOpaqueBlackTexel[4] = {0, 0, 0, 255};
fallbackTexture->UpdateMipmapSubData(TextureUploadTarget::Texture2D, 0,
{kOpaqueBlackTexel, sizeof(kOpaqueBlackTexel)});
fallbackTexture->MarkStorageDirty(TextureUploadTarget::Texture2D, 0, true); fallbackTexture->MarkStorageDirty(TextureUploadTarget::Texture2D, 0, true);
m_fallbackTexture2D = fallbackTexture; m_fallbackTexture2D = fallbackTexture;
} }
@@ -87,6 +87,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Vector<Uint32> bindingAttributeLocations; Vector<Uint32> bindingAttributeLocations;
Vector<Bool> bindingUsesClientMemory; Vector<Bool> bindingUsesClientMemory;
Vector<VertexStreamConversion> bindingConversions; Vector<VertexStreamConversion> bindingConversions;
Vector<VkVertexInputBindingDivisorDescriptionEXT> bindingDivisors;
Uint32 unsupportedAttribMask = 0; Uint32 unsupportedAttribMask = 0;
for (Uint32 location = 0; location < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS; ++location) { for (Uint32 location = 0; location < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS; ++location) {
@@ -180,6 +181,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
bindingConversions.push_back(conversion); bindingConversions.push_back(conversion);
builder.AddBinding(binding, stride, inputRate); builder.AddBinding(binding, stride, inputRate);
builder.AddAttribute(location, binding, vkFormat, 0); builder.AddAttribute(location, binding, vkFormat, 0);
// Divisor 1 is what VK_VERTEX_INPUT_RATE_INSTANCE already means; only anything
// else needs the extension to say it.
if (inputRate == VK_VERTEX_INPUT_RATE_INSTANCE && attr.Divisor != 1) {
bindingDivisors.push_back({binding, static_cast<Uint32>(attr.Divisor)});
}
} }
const auto& state = builder.Build(); const auto& state = builder.Build();
@@ -191,6 +197,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
BackendVertexInputState& entry = *slot; BackendVertexInputState& entry = *slot;
entry.hash = hash; entry.hash = hash;
entry.lastUsedFrameBoundary = m_frameBoundaryCounter; entry.lastUsedFrameBoundary = m_frameBoundaryCounter;
entry.bindingDivisors = Move(bindingDivisors);
entry.bindings = builder.GetBindings(); entry.bindings = builder.GetBindings();
entry.attributes = builder.GetAttributes(); entry.attributes = builder.GetAttributes();
// See the layoutHash declaration: hash only the resolved layout, never // See the layoutHash declaration: hash only the resolved layout, never
@@ -207,6 +214,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.format, sizeof(attribute.format))); XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.format, sizeof(attribute.format)));
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.offset, sizeof(attribute.offset))); XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.offset, sizeof(attribute.offset)));
} }
for (const auto& divisor : entry.bindingDivisors) {
XXHASH_VERIFY(XXH64_update(m_hashState, &divisor.binding, sizeof(divisor.binding)));
XXHASH_VERIFY(XXH64_update(m_hashState, &divisor.divisor, sizeof(divisor.divisor)));
}
XXHASH_VERIFY(XXH64_update(m_hashState, &unsupportedAttribMask, sizeof(unsupportedAttribMask))); XXHASH_VERIFY(XXH64_update(m_hashState, &unsupportedAttribMask, sizeof(unsupportedAttribMask)));
entry.layoutHash = XXH64_digest(m_hashState); entry.layoutHash = XXH64_digest(m_hashState);
entry.attributeLocationMask = 0; entry.attributeLocationMask = 0;
@@ -224,6 +235,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
entry.state = state; entry.state = state;
entry.state.pVertexBindingDescriptions = entry.bindings.empty() ? nullptr : entry.bindings.data(); entry.state.pVertexBindingDescriptions = entry.bindings.empty() ? nullptr : entry.bindings.data();
entry.state.pVertexAttributeDescriptions = entry.attributes.empty() ? nullptr : entry.attributes.data(); entry.state.pVertexAttributeDescriptions = entry.attributes.empty() ? nullptr : entry.attributes.data();
if (!entry.bindingDivisors.empty()) {
entry.divisorState.vertexBindingDivisorCount = static_cast<Uint32>(entry.bindingDivisors.size());
entry.divisorState.pVertexBindingDivisors = entry.bindingDivisors.data();
entry.state.pNext = &entry.divisorState;
} else {
entry.state.pNext = nullptr;
}
return entry; return entry;
} }
@@ -53,6 +53,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Bitmask of `attributes[i].location` - the draw path needs it up to // Bitmask of `attributes[i].location` - the draw path needs it up to
// three times per draw, so it is baked once at build time. // three times per draw, so it is baked once at build time.
Uint32 attributeLocationMask = 0; Uint32 attributeLocationMask = 0;
// Per-binding glVertexAttribDivisor values other than 1. Vulkan's instance input
// rate advances once per instance and nothing else, so anything else has to be
// stated through VK_EXT_vertex_attribute_divisor. Empty when every instanced
// binding uses divisor 1, which is what the plain input rate already means.
Vector<VkVertexInputBindingDivisorDescriptionEXT> bindingDivisors;
VkPipelineVertexInputDivisorStateCreateInfoEXT divisorState{
VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_DIVISOR_STATE_CREATE_INFO_EXT
};
VkPipelineVertexInputStateCreateInfo state{ VkPipelineVertexInputStateCreateInfo state{
VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO
}; };
@@ -561,6 +561,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
auto resource = GetOrCreateResource(bufferObject); auto resource = GetOrCreateResource(bufferObject);
bufferObject->SyncPersistentMappedRange(); bufferObject->SyncPersistentMappedRange();
// A persistently mapped resource's storage IS the application's copy of the bytes -
// the frontend adopted it in place of the shadow and hands out pointers into it, and
// a shader can have written bytes the shadow never saw (a transform feedback
// capture). Streaming a second copy would feed this draw the stale shadow, and the
// downgrade below would release the storage the application still points at,
// breaking the "never recreated" promise AcquirePersistentMap makes.
if (resource->persistentMapped) {
return AcquireResidentSlice(kind, bufferObject, outSlice);
}
const VkDeviceSize size = static_cast<VkDeviceSize>(bufferObject->GetSize()); const VkDeviceSize size = static_cast<VkDeviceSize>(bufferObject->GetSize());
if (size == 0) { if (size == 0) {
MGLOG_E("VkBufferManager::AcquireStreamedSlice failed: buffer size is zero"); MGLOG_E("VkBufferManager::AcquireStreamedSlice failed: buffer size is zero");
@@ -3292,6 +3292,47 @@ void main() {
return true; return true;
} }
namespace {
// Copies index data, replacing every occurrence of the application's arbitrary restart
// index with the fixed all-ones value of the index type - the only one Vulkan restarts
// on. An index that already equals the fixed value would then be indistinguishable from
// a restart, so it is nudged to the next-lowest value: it can only be a real index (the
// application's restart index is a different number), and the vertex it selects is
// outside any well-defined draw anyway, whereas leaving it alone would tear the
// primitive in two.
void RewriteRestartIndices(const void* source, SizeT sizeBytes, VkIndexType indexType,
Uint32 applicationRestartIndex, Vector<Uint8>& output) {
output.resize(sizeBytes);
if (sizeBytes == 0 || source == nullptr) {
return;
}
Memcpy(output.data(), source, sizeBytes);
const auto rewrite = [&](auto* indices, auto fixedMax) {
const SizeT count = sizeBytes / sizeof(*indices);
for (SizeT i = 0; i < count; ++i) {
if (indices[i] == static_cast<decltype(fixedMax)>(applicationRestartIndex)) {
indices[i] = fixedMax;
} else if (indices[i] == fixedMax) {
indices[i] = fixedMax - 1;
}
}
};
switch (indexType) {
case VK_INDEX_TYPE_UINT8:
rewrite(reinterpret_cast<Uint8*>(output.data()), static_cast<Uint8>(0xFFu));
break;
case VK_INDEX_TYPE_UINT16:
rewrite(reinterpret_cast<Uint16*>(output.data()), static_cast<Uint16>(0xFFFFu));
break;
case VK_INDEX_TYPE_UINT32:
rewrite(reinterpret_cast<Uint32*>(output.data()), static_cast<Uint32>(0xFFFFFFFFu));
break;
default:
break;
}
}
} // namespace
Bool VulkanRenderer::UploadAndBindIndexBuffer(FrameContext::FrameData& frame, Bool VulkanRenderer::UploadAndBindIndexBuffer(FrameContext::FrameData& frame,
const MG_State::GLState::VertexArrayObject& vao, const MG_State::GLState::VertexArrayObject& vao,
const IndexBufferView* pIndexBufferView) { const IndexBufferView* pIndexBufferView) {
@@ -3315,8 +3356,10 @@ void main() {
// GL_PRIMITIVE_RESTART uses an arbitrary restart index (glPrimitiveRestartIndex), but Vulkan // GL_PRIMITIVE_RESTART uses an arbitrary restart index (glPrimitiveRestartIndex), but Vulkan
// only restarts on the fixed all-ones value of the index type. GL_PRIMITIVE_RESTART_FIXED_INDEX // only restarts on the fixed all-ones value of the index type. GL_PRIMITIVE_RESTART_FIXED_INDEX
// already matches that, so only the arbitrary form needs checking; hard-fail at this draw with // already matches that, so only the arbitrary form needs handling: rewrite the indices into a
// the reason if the index is not the fixed value (a fallback would silently drop restarts). // transient copy where the application's restart index becomes the fixed one.
Uint32 substituteRestartIndex = 0;
Bool substituteRestart = false;
if (MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) && if (MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) &&
!MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex)) { !MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex)) {
const Uint32 restartIndex = MG_State::pGLContext->GetPrimitiveRestartIndex(); const Uint32 restartIndex = MG_State::pGLContext->GetPrimitiveRestartIndex();
@@ -3327,15 +3370,8 @@ void main() {
case VK_INDEX_TYPE_UINT32: fixedMax = 0xFFFFFFFFu; break; case VK_INDEX_TYPE_UINT32: fixedMax = 0xFFFFFFFFu; break;
default: break; default: break;
} }
if (restartIndex != fixedMax) { substituteRestart = restartIndex != fixedMax;
THROW_EXCEPTION("GL_PRIMITIVE_RESTART with an arbitrary restart index (" + substituteRestartIndex = restartIndex;
std::to_string(restartIndex) +
") is not supported by the Vulkan backend, which only restarts on the fixed index "
"value (" +
std::to_string(fixedMax) +
") for this index type; use GL_PRIMITIVE_RESTART_FIXED_INDEX, or set "
"glPrimitiveRestartIndex to that value.");
}
} }
const auto* indexBuffer = const auto* indexBuffer =
@@ -3349,9 +3385,16 @@ void main() {
MGLOG_E("DrawElements skipped: no element array buffer bound and no client index data"); MGLOG_E("DrawElements skipped: no element array buffer bound and no client index data");
return false; return false;
} }
Vector<Uint8> rewrittenIndices;
const void* uploadSource = clientIndices;
if (substituteRestart) {
RewriteRestartIndices(clientIndices, pIndexBufferView->indexByteSize, vkIndexType,
substituteRestartIndex, rewrittenIndices);
uploadSource = rewrittenIndices.data();
}
BufferSlice slice{}; BufferSlice slice{};
if (!m_bufferManager.UploadTransient(BufferKind::Index, m_frameContext.GetCurrentFrameIndex(), if (!m_bufferManager.UploadTransient(BufferKind::Index, m_frameContext.GetCurrentFrameIndex(),
clientIndices, pIndexBufferView->indexByteSize, 4, slice)) { uploadSource, pIndexBufferView->indexByteSize, 4, slice)) {
MGLOG_E("DrawElements skipped: failed to upload client index data"); MGLOG_E("DrawElements skipped: failed to upload client index data");
return false; return false;
} }
@@ -3373,7 +3416,20 @@ void main() {
BufferSlice slice{}; BufferSlice slice{};
auto indexBufferShared = MG_State::pGLContext->GetBufferObject(indexBuffer->GetExternalIndex()); auto indexBufferShared = MG_State::pGLContext->GetBufferObject(indexBuffer->GetExternalIndex());
MOBILEGL_ASSERT(indexBufferShared != nullptr, "UploadAndBindIndexBuffer failed to resolve shared EBO"); MOBILEGL_ASSERT(indexBufferShared != nullptr, "UploadAndBindIndexBuffer failed to resolve shared EBO");
if (ShouldUseTransientVertexIndexBuffer(*indexBufferShared)) { if (substituteRestart) {
// The whole buffer is rewritten, not just this draw's range, so that every element
// index keeps its position: an indirect draw's firstIndex lives in GPU memory and
// cannot be adjusted from here.
indexBufferShared->SyncGpuWrites();
Vector<Uint8> rewrittenIndices;
RewriteRestartIndices(indexBufferShared->MappedData(), indexBufferShared->GetSize(), vkIndexType,
substituteRestartIndex, rewrittenIndices);
if (!m_bufferManager.UploadTransient(BufferKind::Index, m_frameContext.GetCurrentFrameIndex(),
rewrittenIndices.data(), rewrittenIndices.size(), 4, slice)) {
MGLOG_E("DrawElements skipped: failed to upload restart-substituted index data");
return false;
}
} else if (ShouldUseTransientVertexIndexBuffer(*indexBufferShared)) {
MOBILEGL_ASSERT(indexBufferShared->GetSize() != 0, "DrawElements requires non-empty EBO data"); MOBILEGL_ASSERT(indexBufferShared->GetSize() != 0, "DrawElements requires non-empty EBO data");
if (!m_bufferManager.AcquireStreamedSlice(BufferKind::Index, indexBufferShared, slice)) { if (!m_bufferManager.AcquireStreamedSlice(BufferKind::Index, indexBufferShared, slice)) {
MOBILEGL_ASSERT(false, "DrawElements skipped: failed to prepare transient index buffer"); MOBILEGL_ASSERT(false, "DrawElements skipped: failed to prepare transient index buffer");
@@ -4153,6 +4209,7 @@ void main() {
.subpass = 0, .subpass = 0,
.topology = vkTopology, .topology = vkTopology,
.primitiveRestartEnable = primitiveRestartEnabled, .primitiveRestartEnable = primitiveRestartEnabled,
.patchControlPoints = static_cast<Uint32>(MG_State::pGLContext->GetPatchVertices()),
.polygonMode = effectivePolygonMode, .polygonMode = effectivePolygonMode,
.cullMode = cullFaceEnabled .cullMode = cullFaceEnabled
? MG_Util::ConvertCullFaceModeToVkEnum(MG_State::pGLContext->GetCullFaceMode(), invertClockwise) ? MG_Util::ConvertCullFaceModeToVkEnum(MG_State::pGLContext->GetCullFaceMode(), invertClockwise)
@@ -4692,6 +4749,7 @@ void main() {
// Sync each sampled texture at most once across this whole draw: the layout // Sync each sampled texture at most once across this whole draw: the layout
// probe loop, the post-transition loop, and ResolveSamplerDescriptor would // probe loop, the post-transition loop, and ResolveSamplerDescriptor would
// otherwise each re-run the full SyncTexture path on the same textures. // otherwise each re-run the full SyncTexture path on the same textures.
MakeXfbWritesVisible();
VkTextureManager::DrawSyncScope drawSyncScope(*m_textureManager); VkTextureManager::DrawSyncScope drawSyncScope(*m_textureManager);
m_textureManager->CollectGarbage(); m_textureManager->CollectGarbage();
if (TrySetupDrawFastPath(frame, mode, aspects, drawParams, pIndexBufferView)) { if (TrySetupDrawFastPath(frame, mode, aspects, drawParams, pIndexBufferView)) {
@@ -6920,6 +6978,7 @@ void main() {
} }
Bool VulkanRenderer::FinishPendingGpuWork() { Bool VulkanRenderer::FinishPendingGpuWork() {
MakeXfbWritesVisible();
auto& frame = m_frameContext.GetCurrent(); auto& frame = m_frameContext.GetCurrent();
if (!frame.isCommandRecording) { if (!frame.isCommandRecording) {
return true; return true;
@@ -8013,6 +8072,18 @@ void main() {
} }
resource->layout = finalLayout; resource->layout = finalLayout;
// The chain above is GPU work recorded into this frame's command buffer, which is not
// submitted until the frame ends - but a texture upload goes out on a command buffer of
// its own the moment it happens. A glTexSubImage2D into a level this just generated
// would therefore reach the GPU FIRST and be overwritten by these blits, which is how
// KHR-GL40.texture_gather.base-level lost the texels it wrote into level 1 right after
// generating the chain. Submitting here is what orders the two.
if (HasPendingRecordedWork() && FlushPendingCommands()) {
// Fresh command buffer: the sampled-descriptor-set memo describes bindings that
// only existed in the retired one.
m_lastSampledSetValid = false;
}
} }
Uint32 VulkanRenderer::CurrentXfbCounterSlot() { Uint32 VulkanRenderer::CurrentXfbCounterSlot() {
@@ -8072,8 +8143,11 @@ void main() {
return false; return false;
} }
// Host-visible coherent GPU residency: the capture writes land where // Host-visible coherent GPU residency: the capture writes land where
// MapBuffer/GetBufferSubData read. // MapBuffer/GetBufferSubData read. Coherence makes them visible once they
// have happened, so the buffer is also flagged for the wait that a later CPU
// read has to perform - the capture is a GPU write like any shader's.
bufferObject->EnsureGpuResidentStorage(); bufferObject->EnsureGpuResidentStorage();
bufferObject->MarkGpuWritten();
BufferSlice slice{}; BufferSlice slice{};
if (!m_bufferManager.AcquireResidentSlice(BufferKind::Vertex, bufferObject, slice)) { if (!m_bufferManager.AcquireResidentSlice(BufferKind::Vertex, bufferObject, slice)) {
MGLOG_E("BeginXfbCaptureForDraw: failed to acquire capture buffer %zu", i); MGLOG_E("BeginXfbCaptureForDraw: failed to acquire capture buffer %zu", i);
@@ -8128,6 +8202,41 @@ void main() {
s_vkCmdEndTransformFeedbackEXT(frame.commandBuffer, 0, static_cast<Uint32>(bufferCount), counterBuffers, s_vkCmdEndTransformFeedbackEXT(frame.commandBuffer, 0, static_cast<Uint32>(bufferCount), counterBuffers,
counterOffsets); counterOffsets);
m_xfbCountersValid[counterSlot] = true; m_xfbCountersValid[counterSlot] = true;
m_xfbWritesPendingVisibility = true;
}
// GL makes transform feedback results visible to every later command on their own, with no
// glMemoryBarrier in between - unlike shader storage writes, which is why the barrier the
// Vulkan memory model requires has to be supplied here rather than by the application. It
// cannot be recorded where the write happens (inside the capturing draw's render pass, which
// declares no self-dependency), so it is emitted at the next point that could read the
// captured buffer: the following draw, or a readback.
void VulkanRenderer::MakeXfbWritesVisible() {
if (!m_xfbWritesPendingVisibility) {
return;
}
m_xfbWritesPendingVisibility = false;
auto& frame = m_frameContext.GetCurrent();
if (!frame.isCommandRecording) {
m_frameContext.BeginCommandRecording();
}
if (VkRenderPassManager::GetActiveRenderPass() != nullptr) {
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
}
VkMemoryBarrier memoryBarrier{};
memoryBarrier.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER;
memoryBarrier.srcAccessMask =
VK_ACCESS_TRANSFORM_FEEDBACK_WRITE_BIT_EXT | VK_ACCESS_TRANSFORM_FEEDBACK_COUNTER_WRITE_BIT_EXT;
// Every way a captured buffer can be read back: replayed as vertex attributes or indices
// by glDrawTransformFeedback, sampled through a uniform or storage binding, sourced as an
// indirect command, copied out, or mapped.
memoryBarrier.dstAccessMask =
VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT | VK_ACCESS_INDEX_READ_BIT | VK_ACCESS_UNIFORM_READ_BIT |
VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_INDIRECT_COMMAND_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT |
VK_ACCESS_HOST_READ_BIT | VK_ACCESS_MEMORY_READ_BIT |
VK_ACCESS_TRANSFORM_FEEDBACK_COUNTER_READ_BIT_EXT;
vkCmdPipelineBarrier(frame.commandBuffer, VK_PIPELINE_STAGE_TRANSFORM_FEEDBACK_BIT_EXT,
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 1, &memoryBarrier, 0, nullptr, 0, nullptr);
} }
void VulkanRenderer::DrawArrays(const DrawCmd& payload) { void VulkanRenderer::DrawArrays(const DrawCmd& payload) {
@@ -9686,6 +9795,7 @@ void main() {
? VK_FALSE ? VK_FALSE
: supportedDeviceFeatures.robustBufferAccess; : supportedDeviceFeatures.robustBufferAccess;
deviceFeatures.geometryShader = supportedDeviceFeatures.geometryShader; deviceFeatures.geometryShader = supportedDeviceFeatures.geometryShader;
deviceFeatures.tessellationShader = supportedDeviceFeatures.tessellationShader;
deviceFeatures.independentBlend = supportedDeviceFeatures.independentBlend; deviceFeatures.independentBlend = supportedDeviceFeatures.independentBlend;
m_independentBlendFeatureEnabled = deviceFeatures.independentBlend == VK_TRUE; m_independentBlendFeatureEnabled = deviceFeatures.independentBlend == VK_TRUE;
deviceFeatures.fillModeNonSolid = supportedDeviceFeatures.fillModeNonSolid; deviceFeatures.fillModeNonSolid = supportedDeviceFeatures.fillModeNonSolid;
@@ -9882,6 +9992,36 @@ void main() {
MGLOG_W("VK_EXT_transform_feedback is unavailable; transform feedback capture will not work"); MGLOG_W("VK_EXT_transform_feedback is unavailable; transform feedback capture will not work");
} }
// VK_EXT_vertex_attribute_divisor. Vulkan's instance input rate advances an attribute
// once per instance and nothing else, so without this every glVertexAttribDivisor value
// collapses to 1 and an attribute meant to change every N instances changes every one.
m_vertexAttributeDivisorEnabled = false;
VkPhysicalDeviceVertexAttributeDivisorFeaturesEXT vertexAttributeDivisorFeatures{};
vertexAttributeDivisorFeatures.sType =
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VERTEX_ATTRIBUTE_DIVISOR_FEATURES_EXT;
if (IsExtensionSupported(availableExtensions, VK_EXT_VERTEX_ATTRIBUTE_DIVISOR_EXTENSION_NAME) &&
getPhysicalDeviceFeatures2 != nullptr) {
VkPhysicalDeviceFeatures2 featureQuery{};
featureQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2;
featureQuery.pNext = &vertexAttributeDivisorFeatures;
getPhysicalDeviceFeatures2(m_physicalDevice.handle, &featureQuery);
if (vertexAttributeDivisorFeatures.vertexAttributeInstanceRateDivisor == VK_TRUE) {
if (!IsExtensionAlreadyEnabled(enabledDeviceExtensions,
VK_EXT_VERTEX_ATTRIBUTE_DIVISOR_EXTENSION_NAME)) {
enabledDeviceExtensions.push_back(VK_EXT_VERTEX_ATTRIBUTE_DIVISOR_EXTENSION_NAME);
}
vertexAttributeDivisorFeatures.vertexAttributeInstanceRateZeroDivisor = VK_FALSE;
vertexAttributeDivisorFeatures.pNext = const_cast<void*>(deviceCreateInfo.pNext);
deviceCreateInfo.pNext = &vertexAttributeDivisorFeatures;
m_vertexAttributeDivisorEnabled = true;
MGLOG_I("Enabled optional device extension: %s", VK_EXT_VERTEX_ATTRIBUTE_DIVISOR_EXTENSION_NAME);
}
}
if (!m_vertexAttributeDivisorEnabled) {
MGLOG_W("VK_EXT_vertex_attribute_divisor is unavailable; a glVertexAttribDivisor other "
"than 1 will advance its attribute once per instance");
}
// Host query reset lets the occlusion-query ring recycle slots without a // Host query reset lets the occlusion-query ring recycle slots without a
// command-buffer round trip. // command-buffer round trip.
m_hostQueryResetEnabled = false; m_hostQueryResetEnabled = false;
@@ -494,6 +494,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// VK_EXT_transform_feedback (GL transform feedback capture) // VK_EXT_transform_feedback (GL transform feedback capture)
Bool m_transformFeedbackFeatureEnabled = false; Bool m_transformFeedbackFeatureEnabled = false;
// VK_EXT_vertex_attribute_divisor: without it every non-zero glVertexAttribDivisor
// behaves as 1, because that is all Vulkan's instance input rate can express.
Bool m_vertexAttributeDivisorEnabled = false;
static inline PFN_vkCmdBindTransformFeedbackBuffersEXT s_vkCmdBindTransformFeedbackBuffersEXT = nullptr; static inline PFN_vkCmdBindTransformFeedbackBuffersEXT s_vkCmdBindTransformFeedbackBuffersEXT = nullptr;
static inline PFN_vkCmdBeginTransformFeedbackEXT s_vkCmdBeginTransformFeedbackEXT = nullptr; static inline PFN_vkCmdBeginTransformFeedbackEXT s_vkCmdBeginTransformFeedbackEXT = nullptr;
static inline PFN_vkCmdEndTransformFeedbackEXT s_vkCmdEndTransformFeedbackEXT = nullptr; static inline PFN_vkCmdEndTransformFeedbackEXT s_vkCmdEndTransformFeedbackEXT = nullptr;
@@ -515,6 +518,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// when GL transform feedback is active; binds capture buffers on demand. // when GL transform feedback is active; binds capture buffers on demand.
Bool BeginXfbCaptureForDraw(FrameContext::FrameData& frame); Bool BeginXfbCaptureForDraw(FrameContext::FrameData& frame);
void EndXfbCaptureForDraw(FrameContext::FrameData& frame, Bool began); void EndXfbCaptureForDraw(FrameContext::FrameData& frame, Bool began);
// Makes the captured bytes visible to whatever reads them next. Deferred rather than
// recorded next to the capture, because the capturing draw runs inside a render pass
// that declares no self-dependency.
void MakeXfbWritesVisible();
Bool m_xfbWritesPendingVisibility = false;
// Wrap one app draw in an occlusion-query slot while a GL_SAMPLES_PASSED // Wrap one app draw in an occlusion-query slot while a GL_SAMPLES_PASSED
// query is active. Returns whether a slot was begun (End must mirror it). // query is active. Returns whether a slot was begun (End must mirror it).
Bool BeginOcclusionForDraw(VkCommandBuffer commandBuffer); Bool BeginOcclusionForDraw(VkCommandBuffer commandBuffer);
@@ -877,6 +877,172 @@ namespace MobileGL::MG_Impl::GLImpl {
Memcpy(ids, names.data(), static_cast<SizeT>(n) * sizeof(GLuint)); Memcpy(ids, names.data(), static_cast<SizeT>(n) * sizeof(GLuint));
} }
void CreateTransformFeedbacks(GLsizei n, GLuint* ids) {
if (n < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "n must be non-negative."));
return;
}
if (n == 0 || ids == nullptr) return;
Vector<Uint> names;
MG_State::pGLContext->GenTransformFeedbackNames(static_cast<Uint>(n), names);
// Unlike glGenTransformFeedbacks, the names are objects immediately: there is no bind step
// to create them from (GL 4.6 core 13.2.1).
for (const Uint name : names) {
MG_State::pGLContext->CreateTransformFeedbackObject(name);
}
Memcpy(ids, names.data(), static_cast<SizeT>(n) * sizeof(GLuint));
}
namespace {
// Shared front half of the by-name transform feedback entry points: the object has to exist
// (INVALID_OPERATION otherwise) before anything else about the call is looked at.
Bool ValidateNamedTransformFeedback(GLuint xfb, const char* functionName) {
if (!MG_State::pGLContext->IsTransformFeedbackObject(xfb)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
std::to_string(xfb) + " is not a transform feedback object."));
return false;
}
return true;
}
Bool ValidateTransformFeedbackBufferIndex(GLuint index, const char* functionName) {
if (index >= MG_State::GLState::GLContext::MAX_TRANSFORM_FEEDBACK_BUFFERS) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
"index exceeds GL_MAX_TRANSFORM_FEEDBACK_BUFFERS."));
return false;
}
return true;
}
// A capture binding may not be changed while the object is capturing (GL 4.6 core 13.2.2).
Bool ValidateNamedTransformFeedbackNotActive(GLuint xfb, const char* functionName) {
if (MG_State::pGLContext->IsNamedTransformFeedbackActive(xfb)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
"The transform feedback object is capturing."));
return false;
}
return true;
}
SharedPtr<MG_State::GLState::BufferObject> ResolveTransformFeedbackBuffer(GLuint buffer,
const char* functionName) {
if (buffer == 0) return nullptr;
if (!MG_State::pGLContext->ValidateBufferName(buffer)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
std::to_string(buffer) + " is not a buffer object."));
return nullptr;
}
return MG_State::pGLContext->GetBufferObject(buffer);
}
} // namespace
void TransformFeedbackBufferBase(GLuint xfb, GLuint index, GLuint buffer) {
if (!ValidateNamedTransformFeedback(xfb, __func__)) return;
if (!ValidateTransformFeedbackBufferIndex(index, __func__)) return;
if (!ValidateNamedTransformFeedbackNotActive(xfb, __func__)) return;
if (buffer != 0 && !MG_State::pGLContext->ValidateBufferName(buffer)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
std::to_string(buffer) + " is not a buffer object."));
return;
}
MG_State::pGLContext->SetNamedTransformFeedbackBinding(xfb, index,
ResolveTransformFeedbackBuffer(buffer, __func__), {},
false);
}
void TransformFeedbackBufferRange(GLuint xfb, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size) {
if (!ValidateNamedTransformFeedback(xfb, __func__)) return;
if (!ValidateTransformFeedbackBufferIndex(index, __func__)) return;
if (!ValidateNamedTransformFeedbackNotActive(xfb, __func__)) return;
if (offset < 0 || size <= 0 || (offset % 4) != 0 || (size % 4) != 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"offset and size must be non-negative multiples of 4."));
return;
}
if (buffer != 0 && !MG_State::pGLContext->ValidateBufferName(buffer)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
std::to_string(buffer) + " is not a buffer object."));
return;
}
auto bufferObject = ResolveTransformFeedbackBuffer(buffer, __func__);
const Range1D range{static_cast<SizeT>(offset), static_cast<SizeT>(offset) + static_cast<SizeT>(size)};
MG_State::pGLContext->SetNamedTransformFeedbackBinding(xfb, index, bufferObject, range,
bufferObject != nullptr);
}
void GetTransformFeedbackiv(GLuint xfb, GLenum pname, GLint* param) {
if (!ValidateNamedTransformFeedback(xfb, __func__)) return;
if (!param) return;
switch (pname) {
case GL_TRANSFORM_FEEDBACK_ACTIVE:
*param = MG_State::pGLContext->IsNamedTransformFeedbackActive(xfb) ? GL_TRUE : GL_FALSE;
return;
case GL_TRANSFORM_FEEDBACK_PAUSED:
*param = MG_State::pGLContext->IsNamedTransformFeedbackPaused(xfb) ? GL_TRUE : GL_FALSE;
return;
default:
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"pname must be GL_TRANSFORM_FEEDBACK_ACTIVE or _PAUSED."));
return;
}
}
void GetTransformFeedbacki_v(GLuint xfb, GLenum pname, GLuint index, GLint* param) {
if (!ValidateNamedTransformFeedback(xfb, __func__)) return;
if (pname != GL_TRANSFORM_FEEDBACK_BUFFER_BINDING) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"pname must be GL_TRANSFORM_FEEDBACK_BUFFER_BINDING."));
return;
}
if (!ValidateTransformFeedbackBufferIndex(index, __func__)) return;
if (!param) return;
const auto binding = MG_State::pGLContext->GetNamedTransformFeedbackBinding(xfb, index);
*param = binding.Buffer ? static_cast<GLint>(binding.Buffer->GetExternalIndex()) : 0;
}
void GetTransformFeedbacki64_v(GLuint xfb, GLenum pname, GLuint index, GLint64* param) {
if (!ValidateNamedTransformFeedback(xfb, __func__)) return;
if (pname != GL_TRANSFORM_FEEDBACK_BUFFER_START && pname != GL_TRANSFORM_FEEDBACK_BUFFER_SIZE) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"pname must be GL_TRANSFORM_FEEDBACK_BUFFER_START or _SIZE."));
return;
}
if (!ValidateTransformFeedbackBufferIndex(index, __func__)) return;
if (!param) return;
const auto binding = MG_State::pGLContext->GetNamedTransformFeedbackBinding(xfb, index);
// glTransformFeedbackBufferBase leaves both at zero; only the range form sets them
// (GL 4.6 core table 23.48).
if (!binding.Buffer || !binding.HasExplicitRange) {
*param = 0;
return;
}
*param = (pname == GL_TRANSFORM_FEEDBACK_BUFFER_START)
? static_cast<GLint64>(binding.Range.start)
: static_cast<GLint64>(binding.Range.end - binding.Range.start);
}
void DeleteTransformFeedbacks(GLsizei n, const GLuint* ids) { void DeleteTransformFeedbacks(GLsizei n, const GLuint* ids) {
if (n < 0) { if (n < 0) {
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
@@ -16,7 +16,13 @@ namespace MobileGL::MG_Impl::GLImpl {
void PauseTransformFeedback(void); void PauseTransformFeedback(void);
void ResumeTransformFeedback(void); void ResumeTransformFeedback(void);
void GenTransformFeedbacks(GLsizei n, GLuint* ids); void GenTransformFeedbacks(GLsizei n, GLuint* ids);
void CreateTransformFeedbacks(GLsizei n, GLuint* ids);
void DeleteTransformFeedbacks(GLsizei n, const GLuint* ids); void DeleteTransformFeedbacks(GLsizei n, const GLuint* ids);
void TransformFeedbackBufferBase(GLuint xfb, GLuint index, GLuint buffer);
void TransformFeedbackBufferRange(GLuint xfb, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size);
void GetTransformFeedbackiv(GLuint xfb, GLenum pname, GLint* param);
void GetTransformFeedbacki_v(GLuint xfb, GLenum pname, GLuint index, GLint* param);
void GetTransformFeedbacki64_v(GLuint xfb, GLenum pname, GLuint index, GLint64* param);
void BindTransformFeedback(GLenum target, GLuint id); void BindTransformFeedback(GLenum target, GLuint id);
GLboolean IsTransformFeedback(GLuint id); GLboolean IsTransformFeedback(GLuint id);
void DrawTransformFeedback(GLenum mode, GLuint id); void DrawTransformFeedback(GLenum mode, GLuint id);
@@ -419,7 +419,7 @@ DECLARE_GL_FUNCTION_HEAD(void, DrawElementsInstancedBaseVertex, GLenum mode, GLs
DECLARE_GL_FUNCTION_HEAD(void, FramebufferTexture, GLenum target, GLenum attachment, GLuint texture, GLint level) DECLARE_GL_FUNCTION_END_NO_RETURN(void, FramebufferTexture, target, attachment, texture, level) DECLARE_GL_FUNCTION_HEAD(void, FramebufferTexture, GLenum target, GLenum attachment, GLuint texture, GLint level) DECLARE_GL_FUNCTION_END_NO_RETURN(void, FramebufferTexture, target, attachment, texture, level)
DECLARE_GL_FUNCTION_STUB_HEAD(void, PrimitiveBoundingBox, GLfloat minX, GLfloat minY, GLfloat minZ, GLfloat minW, GLfloat maxX, GLfloat maxY, GLfloat maxZ, GLfloat maxW) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PrimitiveBoundingBox, minX, minY, minZ, minW, maxX, maxY, maxZ, maxW) DECLARE_GL_FUNCTION_STUB_HEAD(void, PrimitiveBoundingBox, GLfloat minX, GLfloat minY, GLfloat minZ, GLfloat minW, GLfloat maxX, GLfloat maxY, GLfloat maxZ, GLfloat maxW) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PrimitiveBoundingBox, minX, minY, minZ, minW, maxX, maxY, maxZ, maxW)
DECLARE_GL_FUNCTION_HEAD(GLenum, GetGraphicsResetStatus) DECLARE_GL_FUNCTION_END(GLenum, GetGraphicsResetStatus) DECLARE_GL_FUNCTION_HEAD(GLenum, GetGraphicsResetStatus) DECLARE_GL_FUNCTION_END(GLenum, GetGraphicsResetStatus)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ReadnPixels, GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLsizei bufSize, void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ReadnPixels, x, y, width, height, format, type, bufSize, data) DECLARE_GL_FUNCTION_HEAD(void, ReadnPixels, GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLsizei bufSize, void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ReadnPixels, x, y, width, height, format, type, bufSize, data)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformfv, GLuint program, GLint location, GLsizei bufSize, GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformfv, program, location, bufSize, params) DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformfv, GLuint program, GLint location, GLsizei bufSize, GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformfv, program, location, bufSize, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformiv, GLuint program, GLint location, GLsizei bufSize, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformiv, program, location, bufSize, params) DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformiv, GLuint program, GLint location, GLsizei bufSize, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformiv, program, location, bufSize, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformuiv, GLuint program, GLint location, GLsizei bufSize, GLuint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformuiv, program, location, bufSize, params) DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformuiv, GLuint program, GLint location, GLsizei bufSize, GLuint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformuiv, program, location, bufSize, params)
@@ -434,7 +434,7 @@ DECLARE_GL_FUNCTION_HEAD(void, SamplerParameterIuiv, GLuint sampler, GLenum pnam
DECLARE_GL_FUNCTION_HEAD(void, GetSamplerParameterIiv, GLuint sampler, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetSamplerParameterIiv, sampler, pname, params) DECLARE_GL_FUNCTION_HEAD(void, GetSamplerParameterIiv, GLuint sampler, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetSamplerParameterIiv, sampler, pname, params)
DECLARE_GL_FUNCTION_HEAD(void, GetSamplerParameterIuiv, GLuint sampler, GLenum pname, GLuint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetSamplerParameterIuiv, sampler, pname, params) DECLARE_GL_FUNCTION_HEAD(void, GetSamplerParameterIuiv, GLuint sampler, GLenum pname, GLuint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetSamplerParameterIuiv, sampler, pname, params)
DECLARE_GL_FUNCTION_HEAD(void, TexBuffer, GLenum target, GLenum internalformat, GLuint buffer) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexBuffer, target, internalformat, buffer) DECLARE_GL_FUNCTION_HEAD(void, TexBuffer, GLenum target, GLenum internalformat, GLuint buffer) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexBuffer, target, internalformat, buffer)
DECLARE_GL_FUNCTION_STUB_HEAD(void, TexBufferRange, GLenum target, GLenum internalformat, GLuint buffer, GLintptr offset, GLsizeiptr size) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TexBufferRange, target, internalformat, buffer, offset, size) DECLARE_GL_FUNCTION_HEAD(void, TexBufferRange, GLenum target, GLenum internalformat, GLuint buffer, GLintptr offset, GLsizeiptr size) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexBufferRange, target, internalformat, buffer, offset, size)
DECLARE_GL_FUNCTION_HEAD(void, TexStorage3DMultisample, GLenum target, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth, GLboolean fixedsamplelocations) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexStorage3DMultisample, target, samples, internalformat, width, height, depth, fixedsamplelocations) DECLARE_GL_FUNCTION_HEAD(void, TexStorage3DMultisample, GLenum target, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth, GLboolean fixedsamplelocations) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexStorage3DMultisample, target, samples, internalformat, width, height, depth, fixedsamplelocations)
DECLARE_GL_FUNCTION_HEAD(void*, MapBufferRange, GLenum target, GLintptr offset, GLsizeiptr length, GLbitfield access) DECLARE_GL_FUNCTION_END(void*, MapBufferRange, target, offset, length, access) DECLARE_GL_FUNCTION_HEAD(void*, MapBufferRange, GLenum target, GLintptr offset, GLsizeiptr length, GLbitfield access) DECLARE_GL_FUNCTION_END(void*, MapBufferRange, target, offset, length, access)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ClearIndex, GLfloat c) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClearIndex, c) DECLARE_GL_FUNCTION_STUB_HEAD(void, ClearIndex, GLfloat c) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClearIndex, c)
@@ -996,7 +996,7 @@ DECLARE_GL_FUNCTION_HEAD(void, MultiDrawElementsIndirect, GLenum mode, GLenum ty
DECLARE_GL_FUNCTION_HEAD(GLint, GetProgramResourceLocationIndex, GLuint program, GLenum programInterface, const GLchar* name) DECLARE_GL_FUNCTION_END(GLint, GetProgramResourceLocationIndex, program, programInterface, name) DECLARE_GL_FUNCTION_HEAD(GLint, GetProgramResourceLocationIndex, GLuint program, GLenum programInterface, const GLchar* name) DECLARE_GL_FUNCTION_END(GLint, GetProgramResourceLocationIndex, program, programInterface, name)
DECLARE_GL_FUNCTION_HEAD(void, ShaderStorageBlockBinding, GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ShaderStorageBlockBinding, program, storageBlockIndex, storageBlockBinding) DECLARE_GL_FUNCTION_HEAD(void, ShaderStorageBlockBinding, GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ShaderStorageBlockBinding, program, storageBlockIndex, storageBlockBinding)
DECLARE_GL_FUNCTION_STUB_HEAD(void, TextureView, GLuint texture, GLenum target, GLuint origtexture, GLenum internalformat, GLuint minlevel, GLuint numlevels, GLuint minlayer, GLuint numlayers) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TextureView, texture, target, origtexture, internalformat, minlevel, numlevels, minlayer, numlayers) DECLARE_GL_FUNCTION_STUB_HEAD(void, TextureView, GLuint texture, GLenum target, GLuint origtexture, GLenum internalformat, GLuint minlevel, GLuint numlevels, GLuint minlayer, GLuint numlayers) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TextureView, texture, target, origtexture, internalformat, minlevel, numlevels, minlayer, numlayers)
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribLFormat, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribLFormat, attribindex, size, type, relativeoffset) DECLARE_GL_FUNCTION_HEAD(void, VertexAttribLFormat, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribLFormat, attribindex, size, type, relativeoffset)
DECLARE_GL_FUNCTION_HEAD(void, BufferStorage, GLenum target, GLsizeiptr size, const void* data, GLbitfield flags) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BufferStorage, target, size, data, flags) DECLARE_GL_FUNCTION_HEAD(void, BufferStorage, GLenum target, GLsizeiptr size, const void* data, GLbitfield flags) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BufferStorage, target, size, data, flags)
DECLARE_GL_FUNCTION_HEAD(void, ClearTexImage, GLuint texture, GLint level, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearTexImage, texture, level, format, type, data) DECLARE_GL_FUNCTION_HEAD(void, ClearTexImage, GLuint texture, GLint level, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearTexImage, texture, level, format, type, data)
DECLARE_GL_FUNCTION_HEAD(void, ClearTexSubImage, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearTexSubImage, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, type, data) DECLARE_GL_FUNCTION_HEAD(void, ClearTexSubImage, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearTexSubImage, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, type, data)
@@ -1007,12 +1007,12 @@ DECLARE_GL_FUNCTION_HEAD(void, BindSamplers, GLuint first, GLsizei count, const
DECLARE_GL_FUNCTION_STUB_HEAD(void, BindImageTextures, GLuint first, GLsizei count, const GLuint* textures) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BindImageTextures, first, count, textures) DECLARE_GL_FUNCTION_STUB_HEAD(void, BindImageTextures, GLuint first, GLsizei count, const GLuint* textures) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BindImageTextures, first, count, textures)
DECLARE_GL_FUNCTION_HEAD(void, BindVertexBuffers, GLuint first, GLsizei count, const GLuint* buffers, const GLintptr* offsets, const GLsizei* strides) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindVertexBuffers, first, count, buffers, offsets, strides) DECLARE_GL_FUNCTION_HEAD(void, BindVertexBuffers, GLuint first, GLsizei count, const GLuint* buffers, const GLintptr* offsets, const GLsizei* strides) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindVertexBuffers, first, count, buffers, offsets, strides)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ClipControl, GLenum origin, GLenum depth) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClipControl, origin, depth) DECLARE_GL_FUNCTION_STUB_HEAD(void, ClipControl, GLenum origin, GLenum depth) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClipControl, origin, depth)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CreateTransformFeedbacks, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CreateTransformFeedbacks, n, ids) DECLARE_GL_FUNCTION_HEAD(void, CreateTransformFeedbacks, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CreateTransformFeedbacks, n, ids)
DECLARE_GL_FUNCTION_STUB_HEAD(void, TransformFeedbackBufferBase, GLuint xfb, GLuint index, GLuint buffer) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TransformFeedbackBufferBase, xfb, index, buffer) DECLARE_GL_FUNCTION_HEAD(void, TransformFeedbackBufferBase, GLuint xfb, GLuint index, GLuint buffer) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TransformFeedbackBufferBase, xfb, index, buffer)
DECLARE_GL_FUNCTION_STUB_HEAD(void, TransformFeedbackBufferRange, GLuint xfb, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TransformFeedbackBufferRange, xfb, index, buffer, offset, size) DECLARE_GL_FUNCTION_HEAD(void, TransformFeedbackBufferRange, GLuint xfb, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TransformFeedbackBufferRange, xfb, index, buffer, offset, size)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetTransformFeedbackiv, GLuint xfb, GLenum pname, GLint* param) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetTransformFeedbackiv, xfb, pname, param) DECLARE_GL_FUNCTION_HEAD(void, GetTransformFeedbackiv, GLuint xfb, GLenum pname, GLint* param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetTransformFeedbackiv, xfb, pname, param)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetTransformFeedbacki_v, GLuint xfb, GLenum pname, GLuint index, GLint* param) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetTransformFeedbacki_v, xfb, pname, index, param) DECLARE_GL_FUNCTION_HEAD(void, GetTransformFeedbacki_v, GLuint xfb, GLenum pname, GLuint index, GLint* param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetTransformFeedbacki_v, xfb, pname, index, param)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetTransformFeedbacki64_v, GLuint xfb, GLenum pname, GLuint index, GLint64* param) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetTransformFeedbacki64_v, xfb, pname, index, param) DECLARE_GL_FUNCTION_HEAD(void, GetTransformFeedbacki64_v, GLuint xfb, GLenum pname, GLuint index, GLint64* param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetTransformFeedbacki64_v, xfb, pname, index, param)
DECLARE_GL_FUNCTION_HEAD(void, CreateBuffers, GLsizei n, GLuint* buffers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CreateBuffers, n, buffers) DECLARE_GL_FUNCTION_HEAD(void, CreateBuffers, GLsizei n, GLuint* buffers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CreateBuffers, n, buffers)
DECLARE_GL_FUNCTION_HEAD(void, NamedBufferStorage, GLuint buffer, GLsizeiptr size, const void* data, GLbitfield flags) DECLARE_GL_FUNCTION_END_NO_RETURN(void, NamedBufferStorage, buffer, size, data, flags) DECLARE_GL_FUNCTION_HEAD(void, NamedBufferStorage, GLuint buffer, GLsizeiptr size, const void* data, GLbitfield flags) DECLARE_GL_FUNCTION_END_NO_RETURN(void, NamedBufferStorage, buffer, size, data, flags)
DECLARE_GL_FUNCTION_HEAD(void, NamedBufferData, GLuint buffer, GLsizeiptr size, const void* data, GLenum usage) DECLARE_GL_FUNCTION_END_NO_RETURN(void, NamedBufferData, buffer, size, data, usage) DECLARE_GL_FUNCTION_HEAD(void, NamedBufferData, GLuint buffer, GLsizeiptr size, const void* data, GLenum usage) DECLARE_GL_FUNCTION_END_NO_RETURN(void, NamedBufferData, buffer, size, data, usage)
@@ -1049,8 +1049,8 @@ DECLARE_GL_FUNCTION_HEAD(void, NamedRenderbufferStorage, GLuint renderbuffer, GL
DECLARE_GL_FUNCTION_HEAD(void, NamedRenderbufferStorageMultisample, GLuint renderbuffer, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, NamedRenderbufferStorageMultisample, renderbuffer, samples, internalformat, width, height) DECLARE_GL_FUNCTION_HEAD(void, NamedRenderbufferStorageMultisample, GLuint renderbuffer, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, NamedRenderbufferStorageMultisample, renderbuffer, samples, internalformat, width, height)
DECLARE_GL_FUNCTION_HEAD(void, GetNamedRenderbufferParameteriv, GLuint renderbuffer, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetNamedRenderbufferParameteriv, renderbuffer, pname, params) DECLARE_GL_FUNCTION_HEAD(void, GetNamedRenderbufferParameteriv, GLuint renderbuffer, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetNamedRenderbufferParameteriv, renderbuffer, pname, params)
DECLARE_GL_FUNCTION_HEAD(void, CreateTextures, GLenum target, GLsizei n, GLuint* textures) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CreateTextures, target, n, textures) DECLARE_GL_FUNCTION_HEAD(void, CreateTextures, GLenum target, GLsizei n, GLuint* textures) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CreateTextures, target, n, textures)
DECLARE_GL_FUNCTION_STUB_HEAD(void, TextureBuffer, GLuint texture, GLenum internalformat, GLuint buffer) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TextureBuffer, texture, internalformat, buffer) DECLARE_GL_FUNCTION_HEAD(void, TextureBuffer, GLuint texture, GLenum internalformat, GLuint buffer) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureBuffer, texture, internalformat, buffer)
DECLARE_GL_FUNCTION_STUB_HEAD(void, TextureBufferRange, GLuint texture, GLenum internalformat, GLuint buffer, GLintptr offset, GLsizeiptr size) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TextureBufferRange, texture, internalformat, buffer, offset, size) DECLARE_GL_FUNCTION_HEAD(void, TextureBufferRange, GLuint texture, GLenum internalformat, GLuint buffer, GLintptr offset, GLsizeiptr size) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureBufferRange, texture, internalformat, buffer, offset, size)
DECLARE_GL_FUNCTION_HEAD(void, TextureStorage1D, GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureStorage1D, texture, levels, internalformat, width) DECLARE_GL_FUNCTION_HEAD(void, TextureStorage1D, GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureStorage1D, texture, levels, internalformat, width)
DECLARE_GL_FUNCTION_HEAD(void, TextureStorage2D, GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureStorage2D, texture, levels, internalformat, width, height) DECLARE_GL_FUNCTION_HEAD(void, TextureStorage2D, GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureStorage2D, texture, levels, internalformat, width, height)
DECLARE_GL_FUNCTION_HEAD(void, TextureStorage3D, GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureStorage3D, texture, levels, internalformat, width, height, depth) DECLARE_GL_FUNCTION_HEAD(void, TextureStorage3D, GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureStorage3D, texture, levels, internalformat, width, height, depth)
@@ -1090,14 +1090,14 @@ DECLARE_GL_FUNCTION_HEAD(void, VertexArrayVertexBuffers, GLuint vaobj, GLuint fi
DECLARE_GL_FUNCTION_HEAD(void, VertexArrayAttribBinding, GLuint vaobj, GLuint attribindex, GLuint bindingindex) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexArrayAttribBinding, vaobj, attribindex, bindingindex) DECLARE_GL_FUNCTION_HEAD(void, VertexArrayAttribBinding, GLuint vaobj, GLuint attribindex, GLuint bindingindex) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexArrayAttribBinding, vaobj, attribindex, bindingindex)
DECLARE_GL_FUNCTION_HEAD(void, VertexArrayAttribFormat, GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLboolean normalized, GLuint relativeoffset) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexArrayAttribFormat, vaobj, attribindex, size, type, normalized, relativeoffset) DECLARE_GL_FUNCTION_HEAD(void, VertexArrayAttribFormat, GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLboolean normalized, GLuint relativeoffset) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexArrayAttribFormat, vaobj, attribindex, size, type, normalized, relativeoffset)
DECLARE_GL_FUNCTION_HEAD(void, VertexArrayAttribIFormat, GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexArrayAttribIFormat, vaobj, attribindex, size, type, relativeoffset) DECLARE_GL_FUNCTION_HEAD(void, VertexArrayAttribIFormat, GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexArrayAttribIFormat, vaobj, attribindex, size, type, relativeoffset)
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexArrayAttribLFormat, GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexArrayAttribLFormat, vaobj, attribindex, size, type, relativeoffset) DECLARE_GL_FUNCTION_HEAD(void, VertexArrayAttribLFormat, GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexArrayAttribLFormat, vaobj, attribindex, size, type, relativeoffset)
DECLARE_GL_FUNCTION_HEAD(void, VertexArrayBindingDivisor, GLuint vaobj, GLuint bindingindex, GLuint divisor) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexArrayBindingDivisor, vaobj, bindingindex, divisor) DECLARE_GL_FUNCTION_HEAD(void, VertexArrayBindingDivisor, GLuint vaobj, GLuint bindingindex, GLuint divisor) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexArrayBindingDivisor, vaobj, bindingindex, divisor)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetVertexArrayiv, GLuint vaobj, GLenum pname, GLint* param) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetVertexArrayiv, vaobj, pname, param) DECLARE_GL_FUNCTION_HEAD(void, GetVertexArrayiv, GLuint vaobj, GLenum pname, GLint* param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetVertexArrayiv, vaobj, pname, param)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetVertexArrayIndexediv, GLuint vaobj, GLuint index, GLenum pname, GLint* param) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetVertexArrayIndexediv, vaobj, index, pname, param) DECLARE_GL_FUNCTION_HEAD(void, GetVertexArrayIndexediv, GLuint vaobj, GLuint index, GLenum pname, GLint* param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetVertexArrayIndexediv, vaobj, index, pname, param)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetVertexArrayIndexed64iv, GLuint vaobj, GLuint index, GLenum pname, GLint64* param) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetVertexArrayIndexed64iv, vaobj, index, pname, param) DECLARE_GL_FUNCTION_HEAD(void, GetVertexArrayIndexed64iv, GLuint vaobj, GLuint index, GLenum pname, GLint64* param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetVertexArrayIndexed64iv, vaobj, index, pname, param)
DECLARE_GL_FUNCTION_HEAD(void, CreateSamplers, GLsizei n, GLuint* samplers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CreateSamplers, n, samplers) DECLARE_GL_FUNCTION_HEAD(void, CreateSamplers, GLsizei n, GLuint* samplers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CreateSamplers, n, samplers)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CreateProgramPipelines, GLsizei n, GLuint* pipelines) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CreateProgramPipelines, n, pipelines) DECLARE_GL_FUNCTION_STUB_HEAD(void, CreateProgramPipelines, GLsizei n, GLuint* pipelines) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CreateProgramPipelines, n, pipelines)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CreateQueries, GLenum target, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CreateQueries, target, n, ids) DECLARE_GL_FUNCTION_HEAD(void, CreateQueries, GLenum target, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CreateQueries, target, n, ids)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetQueryBufferObjecti64v, GLuint id, GLuint buffer, GLenum pname, GLintptr offset) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetQueryBufferObjecti64v, id, buffer, pname, offset) DECLARE_GL_FUNCTION_STUB_HEAD(void, GetQueryBufferObjecti64v, GLuint id, GLuint buffer, GLenum pname, GLintptr offset) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetQueryBufferObjecti64v, id, buffer, pname, offset)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetQueryBufferObjectiv, GLuint id, GLuint buffer, GLenum pname, GLintptr offset) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetQueryBufferObjectiv, id, buffer, pname, offset) DECLARE_GL_FUNCTION_STUB_HEAD(void, GetQueryBufferObjectiv, GLuint id, GLuint buffer, GLenum pname, GLintptr offset) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetQueryBufferObjectiv, id, buffer, pname, offset)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetQueryBufferObjectui64v, GLuint id, GLuint buffer, GLenum pname, GLintptr offset) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetQueryBufferObjectui64v, id, buffer, pname, offset) DECLARE_GL_FUNCTION_STUB_HEAD(void, GetQueryBufferObjectui64v, GLuint id, GLuint buffer, GLenum pname, GLintptr offset) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetQueryBufferObjectui64v, id, buffer, pname, offset)
@@ -1143,6 +1143,7 @@ namespace MobileGL::MG_Impl::GLImpl {
const FramebufferAttachmentType attachmentType = MG_Util::ConvertGLEnumToFramebufferAttachmentType(attachment); const FramebufferAttachmentType attachmentType = MG_Util::ConvertGLEnumToFramebufferAttachmentType(attachment);
if (!FramebufferImpl::ValidateFramebufferAttachmentType(attachmentType)) return; if (!FramebufferImpl::ValidateFramebufferAttachmentType(attachmentType)) return;
if (!FramebufferImpl::ValidateColorAttachmentInRange(attachmentType, functionName)) return;
if (!TextureImpl::ValidateTextureName(texture, true)) return; if (!TextureImpl::ValidateTextureName(texture, true)) return;
if (texture == 0) { if (texture == 0) {
@@ -1239,6 +1240,7 @@ namespace MobileGL::MG_Impl::GLImpl {
FramebufferTarget framebufferTarget = MG_Util::ConvertGLEnumToFramebufferTarget(target); FramebufferTarget framebufferTarget = MG_Util::ConvertGLEnumToFramebufferTarget(target);
RenderbufferTarget rbTarget = MG_Util::ConvertGLEnumToRenderbufferTarget(renderbuffertarget); RenderbufferTarget rbTarget = MG_Util::ConvertGLEnumToRenderbufferTarget(renderbuffertarget);
if (!FramebufferImpl::ValidateFramebufferAttachmentType(attachmentType)) return; if (!FramebufferImpl::ValidateFramebufferAttachmentType(attachmentType)) return;
if (!FramebufferImpl::ValidateColorAttachmentInRange(attachmentType, "FramebufferRenderbuffer_State")) return;
if (!FramebufferImpl::ValidateFramebufferTarget(framebufferTarget)) return; if (!FramebufferImpl::ValidateFramebufferTarget(framebufferTarget)) return;
if (!FramebufferImpl::ValidateRenderbufferTarget(rbTarget)) return; if (!FramebufferImpl::ValidateRenderbufferTarget(rbTarget)) return;
auto& bindingSlot = MG_State::pGLContext->GetFramebufferBindingSlot(framebufferTarget); auto& bindingSlot = MG_State::pGLContext->GetFramebufferBindingSlot(framebufferTarget);
@@ -1283,6 +1285,8 @@ namespace MobileGL::MG_Impl::GLImpl {
FramebufferAttachmentType attachmentType = MG_Util::ConvertGLEnumToFramebufferAttachmentType(attachment); FramebufferAttachmentType attachmentType = MG_Util::ConvertGLEnumToFramebufferAttachmentType(attachment);
RenderbufferTarget rbTarget = MG_Util::ConvertGLEnumToRenderbufferTarget(renderbuffertarget); RenderbufferTarget rbTarget = MG_Util::ConvertGLEnumToRenderbufferTarget(renderbuffertarget);
if (!FramebufferImpl::ValidateFramebufferAttachmentType(attachmentType)) return; if (!FramebufferImpl::ValidateFramebufferAttachmentType(attachmentType)) return;
if (!FramebufferImpl::ValidateColorAttachmentInRange(attachmentType, "NamedFramebufferRenderbuffer_State"))
return;
if (!FramebufferImpl::ValidateRenderbufferTarget(rbTarget)) return; if (!FramebufferImpl::ValidateRenderbufferTarget(rbTarget)) return;
if (renderbuffer == 0) { if (renderbuffer == 0) {
@@ -2199,6 +2203,56 @@ namespace MobileGL::MG_Impl::GLImpl {
ReadPixels_Backend(x, y, width, height, format, type, pixels); ReadPixels_Backend(x, y, width, height, format, type, pixels);
} }
// Bytes glReadPixels would write for this rectangle under the current GL_PACK_* state
// (GL 4.6 core 18.2.8): rows are padded to GL_PACK_ALIGNMENT and laid out GL_PACK_ROW_LENGTH
// wide, and the skip parameters offset the first texel. The last row is not padded - nothing
// follows it to align - which is what makes a tightly-sized destination legal.
static SizeT ComputePackedReadSizeInBytes(GLsizei width, GLsizei height, GLenum format, GLenum type) {
const SizeT bytesPerPixel =
MG_Util::GetInputBytesPerPixel(MG_Util::ConvertGLEnumToTextureInputFormat(format),
MG_Util::ConvertGLEnumToTexturePixelDataType(type));
if (bytesPerPixel == 0 || width <= 0 || height <= 0) return 0;
const auto packParam = [](PixelStoreParam param) {
return static_cast<SizeT>(std::max(0, MG_State::pGLContext->GetPixelStoreParam(param)));
};
const SizeT rowLengthInPixels =
packParam(PixelStoreParam::PackRowLength) != 0
? packParam(PixelStoreParam::PackRowLength)
: static_cast<SizeT>(width);
const SizeT alignment = std::max<SizeT>(1, packParam(PixelStoreParam::PackAlignment));
const SizeT unalignedRowBytes = rowLengthInPixels * bytesPerPixel;
const SizeT paddedRowBytes = ((unalignedRowBytes + alignment - 1) / alignment) * alignment;
const SizeT skipBytes = packParam(PixelStoreParam::PackSkipRows) * paddedRowBytes +
packParam(PixelStoreParam::PackSkipPixels) * bytesPerPixel;
return skipBytes + paddedRowBytes * (static_cast<SizeT>(height) - 1) +
static_cast<SizeT>(width) * bytesPerPixel;
}
// glReadnPixels is glReadPixels with a bound on how much it may write (GL 4.6 core 18.2.8,
// originally GL_ARB_robustness). It 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.
void ReadnPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLsizei bufSize,
void* data) {
if (bufSize < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "bufSize must be non-negative."));
return;
}
if (!ReadPixels_State(x, y, width, height, format, type, data)) return;
if (ComputePackedReadSizeInBytes(width, height, format, type) > static_cast<SizeT>(bufSize)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"the data required for this read does not fit in bufSize."));
return;
}
ReadPixels_Backend(x, y, width, height, format, type, data);
}
void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) { void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) {
if (!ValidateClearBufferfi_State(buffer, drawbuffer)) return; if (!ValidateClearBufferfi_State(buffer, drawbuffer)) return;
ClearBufferfi_Backend(buffer, drawbuffer, depth, stencil); ClearBufferfi_Backend(buffer, drawbuffer, depth, stencil);
@@ -14,6 +14,8 @@
namespace MobileGL::MG_Impl::GLImpl { namespace MobileGL::MG_Impl::GLImpl {
/* @INSERTION_POINT:FUNCTION_DECLARATION@ */ /* @INSERTION_POINT:FUNCTION_DECLARATION@ */
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels); void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels);
void ReadnPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLsizei bufSize,
void* data);
void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil); void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value); void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value);
void ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value); void ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value);
@@ -7,6 +7,7 @@
// End of Source File Header // End of Source File Header
#include "Validators.h" #include "Validators.h"
#include <MG_Backend/BackendObjects.h>
#include <MG_State/GLState/Core.h> #include <MG_State/GLState/Core.h>
#include <MG_State/GLState/ErrorState/Error.h> #include <MG_State/GLState/ErrorState/Error.h>
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h> #include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
@@ -60,6 +61,26 @@ namespace MobileGL::MG_Impl::GLImpl::FramebufferImpl {
return true; return true;
} }
Bool ValidateColorAttachmentInRange(FramebufferAttachmentType attachment, const char* caller) {
const auto first = static_cast<SizeT>(FramebufferAttachmentType::Color0);
const auto index = static_cast<SizeT>(attachment);
if (index < first) return true;
const auto colorIndex = index - first;
const auto limit = static_cast<SizeT>(
MG_Backend::pActiveBackendObject ? MG_Backend::pActiveBackendObject->GetDynamicParameters()
.MaxColorAttachments
: static_cast<Int>(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS));
if (colorIndex >= limit) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl/FramebufferImpl", caller,
std::format("Colour attachment {} is beyond GL_MAX_COLOR_ATTACHMENTS ({}).", colorIndex, limit)));
return false;
}
return true;
}
Bool ValidateRenderbufferTarget(RenderbufferTarget target) { Bool ValidateRenderbufferTarget(RenderbufferTarget target) {
if (target == RenderbufferTarget::Unknown) { if (target == RenderbufferTarget::Unknown) {
using namespace MG_Util; using namespace MG_Util;
@@ -14,6 +14,10 @@ namespace MobileGL::MG_Impl::GLImpl::FramebufferImpl {
Bool ValidateFramebufferTarget(FramebufferTarget target); Bool ValidateFramebufferTarget(FramebufferTarget target);
Bool ValidateFramebufferName(Uint index, Bool allowZero = true); Bool ValidateFramebufferName(Uint index, Bool allowZero = true);
Bool ValidateFramebufferAttachmentType(FramebufferAttachmentType attachment); Bool ValidateFramebufferAttachmentType(FramebufferAttachmentType attachment);
// GL_COLOR_ATTACHMENTn is a token per n up to 31, but only the first GL_MAX_COLOR_ATTACHMENTS of
// them name an attachment point of a framebuffer object; the rest are INVALID_OPERATION for the
// attaching entry points (GL 4.6 core 9.2.7). Non-colour attachments pass through unchanged.
Bool ValidateColorAttachmentInRange(FramebufferAttachmentType attachment, const char* caller);
Bool ValidateRenderbufferTarget(RenderbufferTarget target); Bool ValidateRenderbufferTarget(RenderbufferTarget target);
Bool ValidateRenderbufferName(Uint index, Bool allowZero = true); Bool ValidateRenderbufferName(Uint index, Bool allowZero = true);
} // namespace MobileGL::MG_Impl::GLImpl::FramebufferImpl } // namespace MobileGL::MG_Impl::GLImpl::FramebufferImpl
+44 -8
View File
@@ -671,6 +671,40 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
switch (target) { switch (target) {
// The vertex buffer binding points of the vertex array object that is bound. Indexed by
// binding point, not by attribute (GL 4.6 core 10.3.1).
case GL_VERTEX_BINDING_BUFFER:
case GL_VERTEX_BINDING_DIVISOR:
case GL_VERTEX_BINDING_OFFSET:
case GL_VERTEX_BINDING_STRIDE: {
if (index >= VertexArrayImpl::GetMaxVertexAttribBindings()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"Vertex buffer binding index is out of range."));
return;
}
const auto& vao = MG_State::pGLContext->GetBoundVertexArray();
if (!vao) {
*data = 0;
return;
}
const auto& binding = vao->GetBindingPoint(index);
switch (target) {
case GL_VERTEX_BINDING_BUFFER:
*data = binding.Buffer ? static_cast<GLint>(binding.Buffer->GetExternalIndex()) : 0;
return;
case GL_VERTEX_BINDING_DIVISOR:
*data = static_cast<GLint>(binding.Divisor);
return;
case GL_VERTEX_BINDING_OFFSET:
*data = static_cast<GLint>(binding.Offset);
return;
default:
*data = static_cast<GLint>(binding.Stride);
return;
}
}
case GL_IMAGE_BINDING_NAME: case GL_IMAGE_BINDING_NAME:
case GL_IMAGE_BINDING_LEVEL: case GL_IMAGE_BINDING_LEVEL:
case GL_IMAGE_BINDING_LAYERED: case GL_IMAGE_BINDING_LAYERED:
@@ -1669,7 +1703,7 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = static_cast<GLint>(MG_State::pGLContext->GetHint(pname)); *params = static_cast<GLint>(MG_State::pGLContext->GetHint(pname));
return; return;
case GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT: case GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT:
*params = 0; // texture-buffer range entrypoints are stubbed *params = MG_Backend::pActiveBackendObject->GetDynamicParameters().TextureBufferOffsetAlignment;
return; return;
case GL_TIMESTAMP: { case GL_TIMESTAMP: {
Int64 timestamp = 0; Int64 timestamp = 0;
@@ -1738,20 +1772,22 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = vao ? static_cast<GLint>(vao->GetExternalIndex()) : 0; *params = vao ? static_cast<GLint>(vao->GetExternalIndex()) : 0;
return; return;
} }
// The vertex buffer binding points are per-binding-index state, so the non-indexed getter
// has nothing to answer with (GL 4.6 core table 23.4).
case GL_VERTEX_BINDING_BUFFER:
case GL_VERTEX_BINDING_DIVISOR: case GL_VERTEX_BINDING_DIVISOR:
*params = 0; // vertex-binding entrypoints are stubbed
return;
case GL_VERTEX_BINDING_OFFSET: case GL_VERTEX_BINDING_OFFSET:
*params = 0; // vertex-binding entrypoints are stubbed
return;
case GL_VERTEX_BINDING_STRIDE: case GL_VERTEX_BINDING_STRIDE:
*params = 0; // vertex-binding entrypoints are stubbed RecordIndexedOnlyGetterError(__func__, pname);
return; return;
case GL_MAX_VERTEX_ATTRIB_RELATIVE_OFFSET: case GL_MAX_VERTEX_ATTRIB_RELATIVE_OFFSET:
*params = 0; // vertex-binding entrypoints are stubbed *params = static_cast<GLint>(VertexArrayImpl::GetMaxVertexAttribRelativeOffset());
return; return;
case GL_MAX_VERTEX_ATTRIB_BINDINGS: case GL_MAX_VERTEX_ATTRIB_BINDINGS:
*params = 0; // vertex-binding entrypoints are stubbed *params = static_cast<GLint>(VertexArrayImpl::GetMaxVertexAttribBindings());
return;
case GL_MAX_VERTEX_ATTRIB_STRIDE:
*params = static_cast<GLint>(VertexArrayImpl::GetMaxVertexAttribStride());
return; return;
case GL_VIEWPORT: { case GL_VIEWPORT: {
const auto& vp = MG_State::pGLContext->GetViewport(); const auto& vp = MG_State::pGLContext->GetViewport();
@@ -205,6 +205,50 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
} }
// The GL_UNIFORM interface and glGetActiveUniform(s)iv are the same query in two
// spellings, so they answer from the same place - the frontend reflection. The backend
// program is not that place: it does not exist at all for a program whose types its
// shading language cannot express (a double-precision uniform has no ESSL form), and
// the interface queries would then describe a program with no uniforms.
//
// Writes the GL_UNIFORM value of `prop` for active uniform `index`; false for a prop
// the reflection does not model, which the caller forwards to the backend instead.
Bool GetUniformResourceProp(const SharedPtr<MG_State::GLState::ProgramObject>& programObject, Uint index,
GLenum prop, GLint* out) {
switch (prop) {
case GL_TYPE:
*out = static_cast<GLint>(programObject->GetActiveUniformType(index));
return true;
case GL_ARRAY_SIZE:
*out = programObject->GetActiveUniformArraySize(index);
return true;
case GL_NAME_LENGTH:
*out = static_cast<GLint>(programObject->GetActiveUniformName(index).length() + 1);
return true;
case GL_BLOCK_INDEX:
*out = programObject->GetActiveUniformBlockIndex(index);
return true;
case GL_OFFSET:
*out = programObject->GetActiveUniformOffset(index);
return true;
case GL_ARRAY_STRIDE:
*out = programObject->GetActiveUniformArrayStride(index);
return true;
case GL_MATRIX_STRIDE:
*out = programObject->GetActiveUniformMatrixStride(index);
return true;
case GL_IS_ROW_MAJOR:
*out = programObject->GetActiveUniformIsRowMajor(index);
return true;
case GL_LOCATION:
// A block member has no location; GetUniformLocation already reports -1 for one.
*out = programObject->GetUniformLocation(programObject->GetActiveUniformName(index));
return true;
default:
return false;
}
}
void CopyStr(GLsizei bufSize, GLsizei* length, GLchar* dst, const char* src, GLsizei srcLength) { void CopyStr(GLsizei bufSize, GLsizei* length, GLchar* dst, const char* src, GLsizei srcLength) {
if (bufSize <= 0) { if (bufSize <= 0) {
if (length) *length = 0; if (length) *length = 0;
@@ -2526,6 +2570,17 @@ namespace MobileGL::MG_Impl::GLImpl {
"Backend does not support program interface queries.")); "Backend does not support program interface queries."));
return; return;
} }
if (programInterface == GL_UNIFORM) {
if (pname == GL_ACTIVE_RESOURCES) {
*params = static_cast<GLint>(programObject->GetUniformCount());
return;
}
if (pname == GL_MAX_NAME_LENGTH) {
// Stored as the bare length; GL_MAX_NAME_LENGTH counts the terminator.
*params = programObject->GetUniformMaxLength() + 1;
return;
}
}
getProgramInterfaceiv(program, programInterface, pname, params); getProgramInterfaceiv(program, programInterface, pname, params);
} }
@@ -2534,6 +2589,10 @@ namespace MobileGL::MG_Impl::GLImpl {
if (!programObject) return GL_INVALID_INDEX; if (!programObject) return GL_INVALID_INDEX;
if (!ValidateNamedProgramResourceInterface(programInterface, __func__)) return GL_INVALID_INDEX; if (!ValidateNamedProgramResourceInterface(programInterface, __func__)) return GL_INVALID_INDEX;
if (!name) return GL_INVALID_INDEX; if (!name) return GL_INVALID_INDEX;
if (programInterface == GL_UNIFORM) {
const Int uniformIndex = programObject->GetActiveUniformIndex(name);
return uniformIndex < 0 ? GL_INVALID_INDEX : static_cast<GLuint>(uniformIndex);
}
auto getProgramResourceIndex = MG_Backend::gBackendFunctionsTable.GL.GetProgramResourceIndex; auto getProgramResourceIndex = MG_Backend::gBackendFunctionsTable.GL.GetProgramResourceIndex;
if (!getProgramResourceIndex) { if (!getProgramResourceIndex) {
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
@@ -2570,6 +2629,13 @@ namespace MobileGL::MG_Impl::GLImpl {
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "bufSize must be non-negative.")); MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "bufSize must be non-negative."));
return; return;
} }
if (programInterface == GL_UNIFORM) {
// Same index space GetProgramResourceIndex answers in, and the range check above
// already used it.
const String& uniformName = programObject->GetActiveUniformName(index);
CopyStr(bufSize, length, name, uniformName.c_str(), static_cast<GLsizei>(uniformName.length()));
return;
}
auto getProgramResourceName = MG_Backend::gBackendFunctionsTable.GL.GetProgramResourceName; auto getProgramResourceName = MG_Backend::gBackendFunctionsTable.GL.GetProgramResourceName;
if (!getProgramResourceName) { if (!getProgramResourceName) {
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
@@ -2591,6 +2657,36 @@ namespace MobileGL::MG_Impl::GLImpl {
"propCount and bufSize must be non-negative.")); "propCount and bufSize must be non-negative."));
return; return;
} }
if (programInterface == GL_UNIFORM) {
if (index >= programObject->GetUniformCount()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "index is out of range."));
return;
}
if (props == nullptr || params == nullptr) return;
GLsizei written = 0;
for (GLsizei i = 0; i < propCount && written < bufSize; ++i) {
GLint value = 0;
if (!GetUniformResourceProp(programObject, index, props[i], &value)) {
// GL_ATOMIC_COUNTER_BUFFER_INDEX and the GL_REFERENCED_BY_* stage props are
// not modelled here; ask the backend, which indexes resources by name.
auto backendGetIndex = MG_Backend::gBackendFunctionsTable.GL.GetProgramResourceIndex;
auto backendGetiv = MG_Backend::gBackendFunctionsTable.GL.GetProgramResourceiv;
if (backendGetIndex && backendGetiv) {
const GLuint backendIndex = backendGetIndex(program, GL_UNIFORM,
programObject->GetActiveUniformName(index).c_str());
if (backendIndex != GL_INVALID_INDEX) {
GLsizei one = 0;
backendGetiv(program, GL_UNIFORM, backendIndex, 1, &props[i], 1, &one, &value);
}
}
}
params[written++] = value;
}
if (length) *length = written;
return;
}
auto getProgramResourceiv = MG_Backend::gBackendFunctionsTable.GL.GetProgramResourceiv; auto getProgramResourceiv = MG_Backend::gBackendFunctionsTable.GL.GetProgramResourceiv;
if (!getProgramResourceiv) { if (!getProgramResourceiv) {
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
+43 -3
View File
@@ -23,6 +23,9 @@ namespace MobileGL::MG_Impl::GLImpl {
struct QueryObject { struct QueryObject {
GLuint id = 0; GLuint id = 0;
GLenum target = 0; // 0 = gen'd but never used with BeginQuery/QueryCounter GLenum target = 0; // 0 = gen'd but never used with BeginQuery/QueryCounter
// glCreateQueries makes the object outright; glGenQueries only reserves the name,
// and the object appears when the name is first used (GL 4.6 core 4.2.1).
Bool created = false;
MG_Backend::BackendQueryHandle backendHandle = nullptr; MG_Backend::BackendQueryHandle backendHandle = nullptr;
Bool active = false; Bool active = false;
Bool ended = false; Bool ended = false;
@@ -177,6 +180,41 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
} }
// glCreateQueries differs from glGenQueries in creating the objects outright, with their
// target already fixed and the rest of their state at the defaults (GL 4.6 core 4.2.1).
void CreateQueries(GLenum target, GLsizei n, GLuint* ids) {
switch (target) {
case GL_SAMPLES_PASSED:
case GL_ANY_SAMPLES_PASSED:
case GL_ANY_SAMPLES_PASSED_CONSERVATIVE:
case GL_TIME_ELAPSED:
case GL_TIMESTAMP:
case GL_PRIMITIVES_GENERATED:
case GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN:
break;
default:
RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "Query target is not accepted.");
return;
}
if (n < 0) {
RecordQueryError(ErrorCode::InvalidValue, __FUNCTION__, "n cannot be negative.");
return;
}
if (!ids) {
return;
}
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
for (GLsizei i = 0; i < n; ++i) {
const GLuint id = g_nextQueryId++;
auto* queryObject = new QueryObject;
queryObject->id = id;
queryObject->target = target;
queryObject->created = true;
g_liveQueryObjects[id] = queryObject;
ids[i] = id;
}
}
void DeleteQueries(GLsizei n, const GLuint* ids) { void DeleteQueries(GLsizei n, const GLuint* ids) {
if (n < 0) { if (n < 0) {
RecordQueryError(ErrorCode::InvalidValue, __FUNCTION__, "n cannot be negative."); RecordQueryError(ErrorCode::InvalidValue, __FUNCTION__, "n cannot be negative.");
@@ -228,9 +266,11 @@ namespace MobileGL::MG_Impl::GLImpl {
return GL_FALSE; return GL_FALSE;
} }
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex); const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
// Gen'd ids count as query objects here: the registry creates live // A name from glGenQueries is not yet a query object: it becomes one when it is first
// objects at GenQueries time. // used with BeginQuery/QueryCounter (which is what a non-zero target records), or
return FindQueryObjectLocked(id) != nullptr ? GL_TRUE : GL_FALSE; // immediately if it came from glCreateQueries.
const auto* queryObject = FindQueryObjectLocked(id);
return (queryObject != nullptr && (queryObject->created || queryObject->target != 0)) ? GL_TRUE : GL_FALSE;
} }
void BeginQuery(GLenum target, GLuint id) { void BeginQuery(GLenum target, GLuint id) {
+1
View File
@@ -11,6 +11,7 @@
namespace MobileGL::MG_Impl::GLImpl { namespace MobileGL::MG_Impl::GLImpl {
void GenQueries(GLsizei n, GLuint* ids); void GenQueries(GLsizei n, GLuint* ids);
void CreateQueries(GLenum target, GLsizei n, GLuint* ids);
void DeleteQueries(GLsizei n, const GLuint* ids); void DeleteQueries(GLsizei n, const GLuint* ids);
GLboolean IsQuery(GLuint id); GLboolean IsQuery(GLuint id);
void BeginQuery(GLenum target, GLuint id); void BeginQuery(GLenum target, GLuint id);
@@ -2035,6 +2035,128 @@ namespace MobileGL::MG_Impl::GLImpl {
MaybeAutoGenerateMipmap(target, textureObject, isProxy, level); MaybeAutoGenerateMipmap(target, textureObject, isProxy, level);
} }
// The work glTexBuffer[Range] and glTextureBuffer[Range] all share once the texture has been
// resolved - by binding for the target forms, by name for the DSA ones. `size` is
// kWholeBuffer for the non-Range entry points, which attach the buffer as it grows rather
// than freezing the size it happens to have now.
// The sized internal formats a buffer texture accepts (GL 4.6 core table 8.16). This is a much
// shorter list than the renderable or texturable formats, so it cannot be inferred from either.
static Bool IsBufferTextureInternalFormat(GLenum internalformat) {
switch (internalformat) {
case GL_R8:
case GL_R16:
case GL_R16F:
case GL_R32F:
case GL_R8I:
case GL_R16I:
case GL_R32I:
case GL_R8UI:
case GL_R16UI:
case GL_R32UI:
case GL_RG8:
case GL_RG16:
case GL_RG16F:
case GL_RG32F:
case GL_RG8I:
case GL_RG16I:
case GL_RG32I:
case GL_RG8UI:
case GL_RG16UI:
case GL_RG32UI:
case GL_RGB32F:
case GL_RGB32I:
case GL_RGB32UI:
case GL_RGBA8:
case GL_RGBA16:
case GL_RGBA16F:
case GL_RGBA32F:
case GL_RGBA8I:
case GL_RGBA16I:
case GL_RGBA32I:
case GL_RGBA8UI:
case GL_RGBA16UI:
case GL_RGBA32UI:
return true;
default:
return false;
}
}
static void AttachBufferToTexture(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
GLenum internalformat, GLuint buffer, GLintptr offset, SizeT size,
const char* caller) {
using MG_State::GLState::TextureObjectBuffer;
TextureInternalFormat textureInternalFormat = MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);
if (!IsBufferTextureInternalFormat(internalformat)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", caller,
std::format("internalformat 0x{:X} is not one of the sized formats a buffer texture accepts.",
internalformat)));
return;
}
if (!TextureImpl::ValidateTextureInternalFormat(textureInternalFormat)) return;
auto& bufferObject = MG_State::pGLContext->GetBufferObject(buffer);
if (buffer != 0 && !bufferObject) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"`buffer` is not zero and is not the name of an existing buffer object."));
return;
}
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
if (textureObject->GetStorageType() != TextureStorageType::Buffer) {
// A texture whose target is something else is a wrong object, not a wrong token
// (GL 4.6 core 8.9).
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"The effective target of `texture` is not `GL_TEXTURE_BUFFER`."));
return;
}
if (size != TextureObjectBuffer::kWholeBuffer) {
// GL 4.6 core 8.9: offset must be non-negative and aligned to
// GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT, and size must be positive.
if (offset < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "offset must be non-negative."));
return;
}
if (size == 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "size must be greater than zero."));
return;
}
const Int alignment = std::max(
1, MG_Backend::pActiveBackendObject->GetDynamicParameters().TextureBufferOffsetAlignment);
if (offset % alignment != 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"offset is not a multiple of GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT."));
return;
}
// The range has to lie inside the buffer that is being attached. Detaching (buffer
// zero) carries no range to check.
if (bufferObject && static_cast<SizeT>(offset) + size > bufferObject->GetSize()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"offset + size is greater than the buffer object's GL_BUFFER_SIZE."));
return;
}
}
auto* texBufferObject = static_cast<TextureObjectBuffer*>(textureObject.get());
texBufferObject->GetBufferBindingSlot().Bind(bufferObject);
texBufferObject->SetBufferRange(static_cast<SizeT>(offset < 0 ? 0 : offset), size);
texBufferObject->SetInternalFormat(textureInternalFormat);
}
void TexBuffer_State(GLenum target, GLenum internalformat, GLuint buffer) { void TexBuffer_State(GLenum target, GLenum internalformat, GLuint buffer) {
// ======================= Converting ================================ // ======================= Converting ================================
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
@@ -2081,6 +2203,7 @@ namespace MobileGL::MG_Impl::GLImpl {
auto& bufferSlot = texBufferObject->GetBufferBindingSlot(); auto& bufferSlot = texBufferObject->GetBufferBindingSlot();
bufferSlot.Bind(bufferObject); bufferSlot.Bind(bufferObject);
texBufferObject->SetBufferRange(0, MG_State::GLState::TextureObjectBuffer::kWholeBuffer);
texBufferObject->SetInternalFormat(textureInternalFormat); texBufferObject->SetInternalFormat(textureInternalFormat);
} }
@@ -3670,23 +3793,43 @@ namespace MobileGL::MG_Impl::GLImpl {
GetTextureImage(texture, level, format, type, bufSize, pixels); GetTextureImage(texture, level, format, type, bufSize, pixels);
} }
// A buffer texture carries none of the sampler or level state these queries report. Reached by
// name there is no target token to blame, so the wrong object is INVALID_OPERATION rather than
// the INVALID_ENUM the target forms report for an unaccepted target (GL 4.6 core 8.11).
static Bool ValidateNamedTextureHasParameters(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
const char* caller) {
if (!textureObject) return false;
if (textureObject->GetStorageType() == TextureStorageType::Buffer) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"The effective target of `texture` has no texture parameters."));
return false;
}
return true;
}
void GetTextureParameteriv(GLuint texture, GLenum pname, GLint* params) { void GetTextureParameteriv(GLuint texture, GLenum pname, GLint* params) {
auto textureObject = GetTextureObjectByName(texture, __func__); auto textureObject = GetTextureObjectByName(texture, __func__);
if (!ValidateNamedTextureHasParameters(textureObject, __func__)) return;
WithTemporarilyBoundNamedTexture(textureObject, [&](GLenum target) { GetTexParameteriv_State(target, pname, params); }); WithTemporarilyBoundNamedTexture(textureObject, [&](GLenum target) { GetTexParameteriv_State(target, pname, params); });
} }
void GetTextureParameterfv(GLuint texture, GLenum pname, GLfloat* params) { void GetTextureParameterfv(GLuint texture, GLenum pname, GLfloat* params) {
auto textureObject = GetTextureObjectByName(texture, __func__); auto textureObject = GetTextureObjectByName(texture, __func__);
if (!ValidateNamedTextureHasParameters(textureObject, __func__)) return;
WithTemporarilyBoundNamedTexture(textureObject, [&](GLenum target) { GetTexParameterfv_State(target, pname, params); }); WithTemporarilyBoundNamedTexture(textureObject, [&](GLenum target) { GetTexParameterfv_State(target, pname, params); });
} }
void GetTextureParameterIiv(GLuint texture, GLenum pname, GLint* params) { void GetTextureParameterIiv(GLuint texture, GLenum pname, GLint* params) {
auto textureObject = GetTextureObjectByName(texture, __func__); auto textureObject = GetTextureObjectByName(texture, __func__);
if (!ValidateNamedTextureHasParameters(textureObject, __func__)) return;
WithTemporarilyBoundNamedTexture(textureObject, [&](GLenum target) { GetTexParameterIiv_State(target, pname, params); }); WithTemporarilyBoundNamedTexture(textureObject, [&](GLenum target) { GetTexParameterIiv_State(target, pname, params); });
} }
void GetTextureParameterIuiv(GLuint texture, GLenum pname, GLuint* params) { void GetTextureParameterIuiv(GLuint texture, GLenum pname, GLuint* params) {
auto textureObject = GetTextureObjectByName(texture, __func__); auto textureObject = GetTextureObjectByName(texture, __func__);
if (!ValidateNamedTextureHasParameters(textureObject, __func__)) return;
WithTemporarilyBoundNamedTexture(textureObject, [&](GLenum target) { GetTexParameterIuiv_State(target, pname, params); }); WithTemporarilyBoundNamedTexture(textureObject, [&](GLenum target) { GetTexParameterIuiv_State(target, pname, params); });
} }
@@ -4088,6 +4231,33 @@ namespace MobileGL::MG_Impl::GLImpl {
TexBuffer_State(target, internalformat, buffer); TexBuffer_State(target, internalformat, buffer);
} }
// The buffer texture bound to `target` on the active unit - what the non-DSA range form
// operates on. Kept separate from TexBuffer_State because that one resolves the target
// itself and attaches the whole buffer.
static const SharedPtr<MG_State::GLState::ITextureObject>& GetBoundBufferTexture(GLenum target,
const char* caller) {
TextureUploadTarget uploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
if (!TextureImpl::ValidateTextureUploadTarget(uploadTarget)) return nullTextureObject;
(void)caller;
auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
return activeUnit.GetBindingSlot(MG_Util::ConvertGLEnumToTextureTarget(target)).GetBoundObject();
}
void TexBufferRange(GLenum target, GLenum internalformat, GLuint buffer, GLintptr offset, GLsizeiptr size) {
AttachBufferToTexture(GetBoundBufferTexture(target, __func__), internalformat, buffer, offset,
static_cast<SizeT>(size < 0 ? 0 : size), __func__);
}
void TextureBuffer(GLuint texture, GLenum internalformat, GLuint buffer) {
AttachBufferToTexture(GetTextureObjectByName(texture, __func__), internalformat, buffer, 0,
MG_State::GLState::TextureObjectBuffer::kWholeBuffer, __func__);
}
void TextureBufferRange(GLuint texture, GLenum internalformat, GLuint buffer, GLintptr offset, GLsizeiptr size) {
AttachBufferToTexture(GetTextureObjectByName(texture, __func__), internalformat, buffer, offset,
static_cast<SizeT>(size < 0 ? 0 : size), __func__);
}
GLboolean IsTexture(GLuint texture) { GLboolean IsTexture(GLuint texture) {
return IsTexture_State(texture); return IsTexture_State(texture);
} }
@@ -42,6 +42,9 @@ namespace MobileGL::MG_Impl::GLImpl {
void GenerateTextureMipmap(GLuint texture); void GenerateTextureMipmap(GLuint texture);
void BindTextureUnit(GLuint unit, GLuint texture); void BindTextureUnit(GLuint unit, GLuint texture);
void GetTextureImage(GLuint texture, GLint level, GLenum format, GLenum type, GLsizei bufSize, void* pixels); void GetTextureImage(GLuint texture, GLint level, GLenum format, GLenum type, GLsizei bufSize, void* pixels);
void TexBufferRange(GLenum target, GLenum internalformat, GLuint buffer, GLintptr offset, GLsizeiptr size);
void TextureBuffer(GLuint texture, GLenum internalformat, GLuint buffer);
void TextureBufferRange(GLuint texture, GLenum internalformat, GLuint buffer, GLintptr offset, GLsizeiptr size);
void GetTextureSubImage(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, void GetTextureSubImage(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width,
GLsizei height, GLsizei depth, GLenum format, GLenum type, GLsizei bufSize, void* pixels); GLsizei height, GLsizei depth, GLenum format, GLenum type, GLsizei bufSize, void* pixels);
void GetTextureParameterfv(GLuint texture, GLenum pname, GLfloat* params); void GetTextureParameterfv(GLuint texture, GLenum pname, GLfloat* params);
@@ -105,12 +105,45 @@ namespace MobileGL::MG_Impl::GLImpl {
return pname == GL_CURRENT_VERTEX_ATTRIB; return pname == GL_CURRENT_VERTEX_ATTRIB;
} }
// The stride a pointer-style call gives its binding point: the argument when it is non-zero,
// otherwise the tightly packed element size (GL 4.6 core 10.3.2). A packed 2_10_10_10 or
// 10F_11F_11F attribute is one 32-bit word regardless of its component count.
static int EffectiveVertexStride(GLsizei stride, GLint size, GLenum type) {
if (stride != 0) return static_cast<int>(stride);
switch (type) {
case GL_INT_2_10_10_10_REV:
case GL_UNSIGNED_INT_2_10_10_10_REV:
case GL_UNSIGNED_INT_10F_11F_11F_REV:
return 4;
default:
break;
}
return static_cast<int>(size * MG_Util::GetGLTypeSize(type));
}
// glBindVertexBuffers / glVertexArrayVertexBuffers take a range of binding points, and a
// range that runs past the last one is INVALID_OPERATION rather than the INVALID_VALUE a
// single out-of-range index gets (GL 4.6 core 10.3.1).
static bool ValidateVertexBindingRange(GLuint first, GLsizei count, const char* funcName) {
if (count < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName, "count must be non-negative."));
return false;
}
if (static_cast<Uint64>(first) + static_cast<Uint64>(count) >
VertexArrayImpl::GetMaxVertexAttribBindings()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
"first + count exceeds GL_MAX_VERTEX_ATTRIB_BINDINGS."));
return false;
}
return true;
}
static bool ValidateVertexBindingIndex(GLuint bindingindex, const char* funcName) { static bool ValidateVertexBindingIndex(GLuint bindingindex, const char* funcName) {
// Bound by the same dynamic limit as attribute indices: the default attribute -> binding if (bindingindex >= VertexArrayImpl::GetMaxVertexAttribBindings()) {
// mapping is the identity, so a binding point the backend cannot address as an attribute
// would resolve into an attribute the backend must then reject on every draw. Real drivers
// likewise report MAX_VERTEX_ATTRIB_BINDINGS == MAX_VERTEX_ATTRIBS.
if (bindingindex >= VertexArrayImpl::GetMaxVertexAttribs()) {
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue, ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
@@ -155,6 +188,17 @@ namespace MobileGL::MG_Impl::GLImpl {
SharedPtr<MG_State::GLState::VertexArrayObject> GetNamedVertexArrayObject_State(GLuint vaobj, SharedPtr<MG_State::GLState::VertexArrayObject> GetNamedVertexArrayObject_State(GLuint vaobj,
const char* caller) { const char* caller) {
// Name zero is not a vertex array object in a core profile: it names the default vertex
// array, which the by-name (direct state access) entry points never accept. MobileGL keeps a
// real object at index 0 for the compatibility paths, so the generic name validation below
// would otherwise let it through (GL 4.6 core 10.3.1).
if (vaobj == 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"Vertex array name 0 is not a vertex array object."));
return nullptr;
}
if (!VertexArrayImpl::ValidateVertexArrayName(vaobj)) return nullptr; if (!VertexArrayImpl::ValidateVertexArrayName(vaobj)) return nullptr;
if (!VertexArrayImpl::ValidateVertexArrayObject(vaobj)) return nullptr; if (!VertexArrayImpl::ValidateVertexArrayObject(vaobj)) return nullptr;
return MG_State::pGLContext->GetVertexArrayObject(vaobj); return MG_State::pGLContext->GetVertexArrayObject(vaobj);
@@ -210,7 +254,7 @@ namespace MobileGL::MG_Impl::GLImpl {
DataType dataType = MG_Util::ConvertGLEnumToDataType(type); DataType dataType = MG_Util::ConvertGLEnumToDataType(type);
// Integer path: never normalized, never BGRA/packed (the validator rejects those). // Integer path: never normalized, never BGRA/packed (the validator rejects those).
if (!VertexArrayImpl::ValidateVertexAttribFormat(index, size, dataType, false, stride, true)) return; if (!VertexArrayImpl::ValidateVertexAttribFormat(index, size, type, dataType, false, stride, true)) return;
auto& vao = MG_State::pGLContext->GetBoundVertexArray(); auto& vao = MG_State::pGLContext->GetBoundVertexArray();
if (!vao) { if (!vao) {
@@ -227,6 +271,7 @@ namespace MobileGL::MG_Impl::GLImpl {
vao->SetAttributeFormat(index, size, dataType, false, stride, offset, true, false); vao->SetAttributeFormat(index, size, dataType, false, stride, offset, true, false);
vao->BindAttributeBuffer(index, vbo); vao->BindAttributeBuffer(index, vbo);
vao->MirrorPointerIntoBinding(index, vbo, offset, EffectiveVertexStride(stride, size, type));
} }
void VertexAttribPointer_State(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, void VertexAttribPointer_State(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride,
@@ -234,7 +279,7 @@ namespace MobileGL::MG_Impl::GLImpl {
if (!VertexArrayImpl::ValidateVertexAttributeIndex(index)) return; if (!VertexArrayImpl::ValidateVertexAttributeIndex(index)) return;
DataType dataType = MG_Util::ConvertGLEnumToDataType(type); DataType dataType = MG_Util::ConvertGLEnumToDataType(type);
if (!VertexArrayImpl::ValidateVertexAttribFormat(index, size, dataType, normalized == GL_TRUE, stride, false)) if (!VertexArrayImpl::ValidateVertexAttribFormat(index, size, type, dataType, normalized == GL_TRUE, stride, false))
return; return;
auto& vao = MG_State::pGLContext->GetBoundVertexArray(); auto& vao = MG_State::pGLContext->GetBoundVertexArray();
@@ -256,6 +301,7 @@ namespace MobileGL::MG_Impl::GLImpl {
const int effectiveSize = isBgra ? 4 : size; const int effectiveSize = isBgra ? 4 : size;
vao->SetAttributeFormat(index, effectiveSize, dataType, normalized, stride, offset, false, isBgra); vao->SetAttributeFormat(index, effectiveSize, dataType, normalized, stride, offset, false, isBgra);
vao->BindAttributeBuffer(index, vbo); vao->BindAttributeBuffer(index, vbo);
vao->MirrorPointerIntoBinding(index, vbo, offset, EffectiveVertexStride(stride, effectiveSize, type));
} }
void BindVertexArray_State(GLuint array) { void BindVertexArray_State(GLuint array) {
@@ -359,6 +405,13 @@ namespace MobileGL::MG_Impl::GLImpl {
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "offset and stride must be non-negative.")); MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "offset and stride must be non-negative."));
return; return;
} }
if (static_cast<Uint>(stride) > VertexArrayImpl::GetMaxVertexAttribStride()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"stride exceeds GL_MAX_VERTEX_ATTRIB_STRIDE."));
return;
}
auto bufferObject = GetVertexArrayBufferObject_State(buffer, caller); auto bufferObject = GetVertexArrayBufferObject_State(buffer, caller);
if (buffer != 0 && !bufferObject) return; if (buffer != 0 && !bufferObject) return;
@@ -376,6 +429,7 @@ namespace MobileGL::MG_Impl::GLImpl {
const GLintptr* offsets, const GLsizei* strides) { const GLintptr* offsets, const GLsizei* strides) {
auto vao = GetNamedVertexArrayObject_State(vaobj, "VertexArrayVertexBuffers_State"); auto vao = GetNamedVertexArrayObject_State(vaobj, "VertexArrayVertexBuffers_State");
if (!vao) return; if (!vao) return;
if (!ValidateVertexBindingRange(first, count, "VertexArrayVertexBuffers_State")) return;
for (GLsizei i = 0; i < count; ++i) { for (GLsizei i = 0; i < count; ++i) {
if (!buffers) { if (!buffers) {
VertexBufferBinding_State(vao, first + i, 0, 0, 16, "VertexArrayVertexBuffers_State"); VertexBufferBinding_State(vao, first + i, 0, 0, 16, "VertexArrayVertexBuffers_State");
@@ -389,12 +443,36 @@ namespace MobileGL::MG_Impl::GLImpl {
static void VertexAttribFormatSeparate_State(const SharedPtr<MG_State::GLState::VertexArrayObject>& vao, static void VertexAttribFormatSeparate_State(const SharedPtr<MG_State::GLState::VertexArrayObject>& vao,
GLuint attribindex, GLint size, GLenum type, GLboolean normalized, GLuint attribindex, GLint size, GLenum type, GLboolean normalized,
GLuint relativeoffset, Bool isInteger, const char* caller) { GLuint relativeoffset, Bool isInteger, const char* caller) {
static_cast<void>(caller);
if (!VertexArrayImpl::ValidateVertexAttributeIndex(attribindex)) return; if (!VertexArrayImpl::ValidateVertexAttributeIndex(attribindex)) return;
DataType dataType = MG_Util::ConvertGLEnumToDataType(type); DataType dataType = MG_Util::ConvertGLEnumToDataType(type);
if (!VertexArrayImpl::ValidateVertexAttribPointerParams(attribindex, size, dataType, 0)) return; // The separate-format entry points take the same size/type rules as the pointer ones,
// GL_BGRA included, so they need the full format validation rather than the pointer-only
// subset - that one reports GL_BGRA as an out-of-range size.
if (!VertexArrayImpl::ValidateVertexAttribFormat(attribindex, size, type, dataType, normalized == GL_TRUE, 0,
isInteger))
return;
if (!VertexArrayImpl::ValidateVertexAttribRelativeOffset(relativeoffset)) return;
vao->SetAttributeFormatSeparate(attribindex, size, dataType, normalized, isInteger, relativeoffset); const Bool isBgra = (size == static_cast<GLint>(GL_BGRA));
vao->SetAttributeFormatSeparate(attribindex, isBgra ? 4 : size, dataType, normalized, isInteger,
relativeoffset, isBgra);
}
// The long (64-bit) attribute format. MobileGL has no 64-bit vertex attributes, so nothing is
// recorded; what the entry point owes the application is the parameter validation, which is
// observable through glGetError regardless of whether the format could be used in a draw.
static void VertexAttribLFormatSeparate_State(GLuint attribindex, GLint size, GLenum type,
GLuint relativeoffset) {
if (!VertexArrayImpl::ValidateVertexAttributeIndex(attribindex)) return;
if (!VertexArrayImpl::ValidateVertexAttribLFormat(attribindex, size, type)) return;
if (!VertexArrayImpl::ValidateVertexAttribRelativeOffset(relativeoffset)) return;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "VertexAttribLFormat",
"64-bit vertex attributes are not supported."));
} }
void VertexArrayAttribFormat_State(GLuint vaobj, GLuint attribindex, GLint size, GLenum type, void VertexArrayAttribFormat_State(GLuint vaobj, GLuint attribindex, GLint size, GLenum type,
@@ -1044,6 +1122,83 @@ namespace MobileGL::MG_Impl::GLImpl {
VertexArrayVertexBuffer_State(vaobj, bindingindex, buffer, offset, stride); VertexArrayVertexBuffer_State(vaobj, bindingindex, buffer, offset, stride);
} }
// glGetVertexArrayiv reports exactly one thing (GL 4.6 core table 23.4): which buffer the
// named vertex array takes its indices from. Everything else about a vertex array is
// per-attribute and belongs to the indexed queries below.
void GetVertexArrayiv(GLuint vaobj, GLenum pname, GLint* param) {
auto vao = GetNamedVertexArrayObject_State(vaobj, __func__);
if (!vao || !param) return;
if (pname != GL_ELEMENT_ARRAY_BUFFER_BINDING) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"pname must be GL_ELEMENT_ARRAY_BUFFER_BINDING."));
return;
}
const auto& indexBuffer = vao->GetIndexBufferBindingSlot().GetBoundObject();
*param = indexBuffer ? static_cast<GLint>(indexBuffer->GetExternalIndex()) : 0;
}
void GetVertexArrayIndexediv(GLuint vaobj, GLuint index, GLenum pname, GLint* param) {
auto vao = GetNamedVertexArrayObject_State(vaobj, __func__);
if (!vao || !param) return;
if (!VertexArrayImpl::ValidateVertexAttributeIndex(index)) return;
const auto& attr = vao->GetAttribute(index);
switch (pname) {
case GL_VERTEX_ATTRIB_ARRAY_ENABLED:
*param = attr.Enabled ? GL_TRUE : GL_FALSE;
return;
case GL_VERTEX_ATTRIB_ARRAY_SIZE:
*param = static_cast<GLint>(attr.Size);
return;
case GL_VERTEX_ATTRIB_ARRAY_STRIDE:
*param = static_cast<GLint>(attr.Stride);
return;
case GL_VERTEX_ATTRIB_ARRAY_TYPE:
*param = static_cast<GLint>(MG_Util::ConvertDataTypeToGLEnum(attr.Type));
return;
case GL_VERTEX_ATTRIB_ARRAY_NORMALIZED:
*param = attr.Normalized ? GL_TRUE : GL_FALSE;
return;
case GL_VERTEX_ATTRIB_ARRAY_INTEGER:
*param = attr.IsInteger ? GL_TRUE : GL_FALSE;
return;
case GL_VERTEX_ATTRIB_ARRAY_LONG:
// 64-bit attributes are not supported, so no attribute is ever a long one.
*param = GL_FALSE;
return;
case GL_VERTEX_ATTRIB_ARRAY_DIVISOR:
*param = static_cast<GLint>(attr.Divisor);
return;
case GL_VERTEX_ATTRIB_RELATIVE_OFFSET:
*param = static_cast<GLint>(vao->GetAttributeRelativeOffset(index));
return;
default:
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"pname is not an accepted indexed vertex array query."));
return;
}
}
// Only GL_VERTEX_BINDING_OFFSET needs 64 bits. Its `index` names a vertex buffer binding
// point directly (GL 4.6 core 10.3.1), not an attribute - unlike every pname the 32-bit
// indexed query above accepts, which is why this one does not go through an attribute's
// binding index.
void GetVertexArrayIndexed64iv(GLuint vaobj, GLuint index, GLenum pname, GLint64* param) {
auto vao = GetNamedVertexArrayObject_State(vaobj, __func__);
if (!vao || !param) return;
if (!VertexArrayImpl::ValidateVertexAttributeIndex(index)) return;
if (pname != GL_VERTEX_BINDING_OFFSET) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "pname must be GL_VERTEX_BINDING_OFFSET."));
return;
}
*param = static_cast<GLint64>(vao->GetBindingPoint(index).Offset);
}
void VertexArrayAttribFormat(GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLboolean normalized, void VertexArrayAttribFormat(GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLboolean normalized,
GLuint relativeoffset) { GLuint relativeoffset) {
VertexArrayAttribFormat_State(vaobj, attribindex, size, type, normalized, relativeoffset); VertexArrayAttribFormat_State(vaobj, attribindex, size, type, normalized, relativeoffset);
@@ -1076,6 +1231,7 @@ namespace MobileGL::MG_Impl::GLImpl {
const GLsizei* strides) { const GLsizei* strides) {
auto vao = GetBoundVertexArrayOrError("BindVertexBuffers"); auto vao = GetBoundVertexArrayOrError("BindVertexBuffers");
if (!vao) return; if (!vao) return;
if (!ValidateVertexBindingRange(first, count, "BindVertexBuffers")) return;
for (GLsizei i = 0; i < count; ++i) { for (GLsizei i = 0; i < count; ++i) {
if (!buffers) { if (!buffers) {
VertexBufferBinding_State(vao, first + i, 0, 0, 16, "BindVertexBuffers"); VertexBufferBinding_State(vao, first + i, 0, 0, 16, "BindVertexBuffers");
@@ -1100,6 +1256,18 @@ namespace MobileGL::MG_Impl::GLImpl {
"VertexAttribIFormat"); "VertexAttribIFormat");
} }
void VertexAttribLFormat(GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) {
auto vao = GetBoundVertexArrayOrError("VertexAttribLFormat");
if (!vao) return;
VertexAttribLFormatSeparate_State(attribindex, size, type, relativeoffset);
}
void VertexArrayAttribLFormat(GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) {
auto vao = GetNamedVertexArrayObject_State(vaobj, "VertexArrayAttribLFormat");
if (!vao) return;
VertexAttribLFormatSeparate_State(attribindex, size, type, relativeoffset);
}
void VertexAttribBinding(GLuint attribindex, GLuint bindingindex) { void VertexAttribBinding(GLuint attribindex, GLuint bindingindex) {
auto vao = GetBoundVertexArrayOrError("VertexAttribBinding"); auto vao = GetBoundVertexArrayOrError("VertexAttribBinding");
if (!vao) return; if (!vao) return;
@@ -92,9 +92,13 @@ namespace MobileGL::MG_Impl::GLImpl {
void EnableVertexArrayAttrib(GLuint vaobj, GLuint index); void EnableVertexArrayAttrib(GLuint vaobj, GLuint index);
void VertexArrayElementBuffer(GLuint vaobj, GLuint buffer); void VertexArrayElementBuffer(GLuint vaobj, GLuint buffer);
void VertexArrayVertexBuffer(GLuint vaobj, GLuint bindingindex, GLuint buffer, GLintptr offset, GLsizei stride); void VertexArrayVertexBuffer(GLuint vaobj, GLuint bindingindex, GLuint buffer, GLintptr offset, GLsizei stride);
void GetVertexArrayiv(GLuint vaobj, GLenum pname, GLint* param);
void GetVertexArrayIndexediv(GLuint vaobj, GLuint index, GLenum pname, GLint* param);
void GetVertexArrayIndexed64iv(GLuint vaobj, GLuint index, GLenum pname, GLint64* param);
void VertexArrayAttribFormat(GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLboolean normalized, void VertexArrayAttribFormat(GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLboolean normalized,
GLuint relativeoffset); GLuint relativeoffset);
void VertexArrayAttribIFormat(GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset); void VertexArrayAttribIFormat(GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset);
void VertexArrayAttribLFormat(GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset);
void VertexArrayAttribBinding(GLuint vaobj, GLuint attribindex, GLuint bindingindex); void VertexArrayAttribBinding(GLuint vaobj, GLuint attribindex, GLuint bindingindex);
void VertexArrayBindingDivisor(GLuint vaobj, GLuint bindingindex, GLuint divisor); void VertexArrayBindingDivisor(GLuint vaobj, GLuint bindingindex, GLuint divisor);
void VertexArrayVertexBuffers(GLuint vaobj, GLuint first, GLsizei count, const GLuint* buffers, void VertexArrayVertexBuffers(GLuint vaobj, GLuint first, GLsizei count, const GLuint* buffers,
@@ -104,6 +108,7 @@ namespace MobileGL::MG_Impl::GLImpl {
const GLsizei* strides); const GLsizei* strides);
void VertexAttribFormat(GLuint attribindex, GLint size, GLenum type, GLboolean normalized, GLuint relativeoffset); void VertexAttribFormat(GLuint attribindex, GLint size, GLenum type, GLboolean normalized, GLuint relativeoffset);
void VertexAttribIFormat(GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset); void VertexAttribIFormat(GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset);
void VertexAttribLFormat(GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset);
void VertexAttribBinding(GLuint attribindex, GLuint bindingindex); void VertexAttribBinding(GLuint attribindex, GLuint bindingindex);
void VertexBindingDivisor(GLuint bindingindex, GLuint divisor); void VertexBindingDivisor(GLuint bindingindex, GLuint divisor);
void VertexAttribDivisor(GLuint index, GLuint divisor); void VertexAttribDivisor(GLuint index, GLuint divisor);
@@ -23,6 +23,18 @@ namespace MobileGL::MG_Impl::GLImpl::VertexArrayImpl {
return std::min(static_cast<Uint>(backendLimit), capacity); return std::min(static_cast<Uint>(backendLimit), capacity);
} }
Uint GetMaxVertexAttribBindings() {
return GetMaxVertexAttribs();
}
Uint GetMaxVertexAttribRelativeOffset() {
return 2047;
}
Uint GetMaxVertexAttribStride() {
return 2048;
}
Bool ValidateVertexArrayName(Uint index) { Bool ValidateVertexArrayName(Uint index) {
Bool isValid = MG_State::pGLContext->ValidateVertexArrayName(index); Bool isValid = MG_State::pGLContext->ValidateVertexArrayName(index);
if (!isValid) { if (!isValid) {
@@ -90,9 +102,31 @@ namespace MobileGL::MG_Impl::GLImpl::VertexArrayImpl {
return true; return true;
} }
Bool ValidateVertexAttribFormat(Uint index, GLint sizeRaw, DataType type, Bool normalized, Int stride, Bool ValidateVertexAttribFormat(Uint index, GLint sizeRaw, GLenum glType, DataType type, Bool normalized,
Bool integerPath) { Int stride, Bool integerPath) {
constexpr const char* fn = "ValidateVertexAttribFormat"; constexpr const char* fn = "ValidateVertexAttribFormat";
// GL_UNSIGNED_INT_10F_11F_11F_REV is a three-component float-path-only packing that has no
// DataType of its own, so it has to be recognised by name before the conversion below turns
// it into Unknown and reports the wrong error (GL 4.6 core 10.3.2).
if (glType == GL_UNSIGNED_INT_10F_11F_11F_REV) {
if (integerPath) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", fn,
std::format("GL_UNSIGNED_INT_10F_11F_11F_REV is not an integer-path type (attribute {}).",
index)));
return false;
}
if (sizeRaw != 3) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", fn,
std::format("GL_UNSIGNED_INT_10F_11F_11F_REV requires size 3 (attribute {}).", index)));
return false;
}
}
if (type == DataType::Unknown) { if (type == DataType::Unknown) {
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum, ErrorCode::InvalidEnum,
@@ -170,4 +204,40 @@ namespace MobileGL::MG_Impl::GLImpl::VertexArrayImpl {
} }
return true; return true;
} }
Bool ValidateVertexAttribLFormat(Uint index, GLint size, GLenum type) {
constexpr const char* fn = "ValidateVertexAttribLFormat";
if (size < 1 || size > 4) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", fn,
std::format("Invalid size {} for attribute {}. Must be 1-4.", size, index)));
return false;
}
// GL 4.6 core 10.3.2: the long form takes GL_DOUBLE and nothing else.
if (type != GL_DOUBLE) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", fn,
std::format("Type 0x{:X} is not GL_DOUBLE (attribute {}).", type, index)));
return false;
}
return true;
}
Bool ValidateVertexAttribRelativeOffset(Uint relativeOffset) {
const Uint limit = GetMaxVertexAttribRelativeOffset();
if (relativeOffset > limit) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", "ValidateVertexAttribRelativeOffset",
std::format("relativeoffset {} exceeds GL_MAX_VERTEX_ATTRIB_RELATIVE_OFFSET ({}).", relativeOffset,
limit)));
return false;
}
return true;
}
} // namespace MobileGL::MG_Impl::GLImpl::VertexArrayImpl } // namespace MobileGL::MG_Impl::GLImpl::VertexArrayImpl
@@ -15,6 +15,20 @@ namespace MobileGL::MG_Impl::GLImpl::VertexArrayImpl {
// capacity). Falls back to the capacity when no backend is active (unit tests). // capacity). Falls back to the capacity when no backend is active (unit tests).
Uint GetMaxVertexAttribs(); Uint GetMaxVertexAttribs();
// GL_MAX_VERTEX_ATTRIB_BINDINGS. The default attribute -> binding mapping is the identity, so a
// binding point that cannot also be an attribute index would resolve into an attribute the
// backend has to reject on every draw; real drivers report the two limits equal as well.
Uint GetMaxVertexAttribBindings();
// GL_MAX_VERTEX_ATTRIB_RELATIVE_OFFSET. The relative offset is folded into the resolved
// attribute offset in the frontend and never reaches a backend limit, so this is the value the
// spec requires an implementation to support at minimum (GL 4.6 core table 23.63).
Uint GetMaxVertexAttribRelativeOffset();
// GL_MAX_VERTEX_ATTRIB_STRIDE. Like the relative offset above, the stride never reaches a
// backend limit of its own, so this is the spec minimum (GL 4.6 core table 23.63).
Uint GetMaxVertexAttribStride();
Bool ValidateVertexArrayName(Uint index); Bool ValidateVertexArrayName(Uint index);
Bool ValidateVertexArrayObject(Uint index); Bool ValidateVertexArrayObject(Uint index);
Bool ValidateVertexAttributeIndex(Uint index); Bool ValidateVertexAttributeIndex(Uint index);
@@ -22,6 +36,13 @@ namespace MobileGL::MG_Impl::GLImpl::VertexArrayImpl {
// Full glVertexAttribPointer / glVertexAttribIPointer format validation, including the packed // Full glVertexAttribPointer / glVertexAttribIPointer format validation, including the packed
// 2_10_10_10 types and GL_BGRA size. sizeRaw is the untranslated GL size (possibly GL_BGRA); // 2_10_10_10 types and GL_BGRA size. sizeRaw is the untranslated GL size (possibly GL_BGRA);
// integerPath selects the glVertexAttribIPointer rules. // integerPath selects the glVertexAttribIPointer rules.
Bool ValidateVertexAttribFormat(Uint index, GLint sizeRaw, DataType type, Bool normalized, Int stride, Bool ValidateVertexAttribFormat(Uint index, GLint sizeRaw, GLenum glType, DataType type, Bool normalized,
Bool integerPath); Int stride, Bool integerPath);
// glVertexAttribLFormat / glVertexArrayAttribLFormat: the only accepted type is GL_DOUBLE and
// the size range is 1-4 (GL_BGRA is a float-path size). Separate from the function above
// because the long path shares none of its type or size rules.
Bool ValidateVertexAttribLFormat(Uint index, GLint size, GLenum type);
// Shared by every *Format entry point: INVALID_VALUE once relativeoffset leaves the range the
// implementation advertises.
Bool ValidateVertexAttribRelativeOffset(Uint relativeOffset);
} // namespace MobileGL::MG_Impl::GLImpl::VertexArrayImpl } // namespace MobileGL::MG_Impl::GLImpl::VertexArrayImpl
@@ -31,6 +31,9 @@ namespace MobileGL::MG_State::GLState {
BindingSlot<BufferObject>& GetBindingSlot(BufferTarget target); BindingSlot<BufferObject>& GetBindingSlot(BufferTarget target);
// For glBindBufferBase / glBindBufferRange // For glBindBufferBase / glBindBufferRange
BindingSlotRange1D<BufferObject>& GetBindingPoint(BufferTarget target, Uint index); BindingSlotRange1D<BufferObject>& GetBindingPoint(BufferTarget target, Uint index);
const BindingSlotRange1D<BufferObject>& GetBindingPoint(BufferTarget target, Uint index) const {
return const_cast<BufferState*>(this)->GetBindingPoint(target, index);
}
constexpr SizeT GetBindingPointCount(const BufferTarget target) const { constexpr SizeT GetBindingPointCount(const BufferTarget target) const {
auto it = std::find(BufferBindPointTargets.begin(), BufferBindPointTargets.end(), target); auto it = std::find(BufferBindPointTargets.begin(), BufferBindPointTargets.end(), target);
auto index = std::distance(BufferBindPointTargets.begin(), it); auto index = std::distance(BufferBindPointTargets.begin(), it);
+58
View File
@@ -842,6 +842,64 @@ namespace MobileGL::MG_State {
const auto it = m_transformFeedbackObjects.find(index); const auto it = m_transformFeedbackObjects.find(index);
return it != m_transformFeedbackObjects.end() && it->second.hasCompletedSpan; return it != m_transformFeedbackObjects.end() && it->second.hasCompletedSpan;
} }
void GLContext::CreateTransformFeedbackObject(Uint index) {
// glCreateTransformFeedbacks has no bind step to infer existence from, so the name it
// hands out is already the name of an object (GL 4.6 core 13.2.1).
m_transformFeedbackObjects[index] = {};
m_transformFeedbackObjects[index].everBound = true;
}
Bool GLContext::IsNamedTransformFeedbackActive(Uint index) const {
if (index == m_boundTransformFeedback) return m_transformFeedbackActive;
const auto it = m_transformFeedbackObjects.find(index);
return it != m_transformFeedbackObjects.end() && it->second.active;
}
Bool GLContext::IsNamedTransformFeedbackPaused(Uint index) const {
if (index == m_boundTransformFeedback) return m_transformFeedbackPaused;
const auto it = m_transformFeedbackObjects.find(index);
return it != m_transformFeedbackObjects.end() && it->second.paused;
}
NamedTransformFeedbackBinding GLContext::GetNamedTransformFeedbackBinding(Uint index, Uint bufferIndex) const {
NamedTransformFeedbackBinding result;
if (bufferIndex >= MAX_TRANSFORM_FEEDBACK_BUFFERS) return result;
// The bound object's capture bindings live in the context's own binding points, not in
// the saved copy - that one is only written when the object is swapped out.
if (index == m_boundTransformFeedback) {
const auto& point = m_bufferState.GetBindingPoint(BufferTarget::TransformFeedback, bufferIndex);
result.Buffer = point.GetBoundObject();
result.Range = point.GetRange();
result.HasExplicitRange = point.HasExplicitRange();
return result;
}
const auto it = m_transformFeedbackObjects.find(index);
if (it == m_transformFeedbackObjects.end()) return result;
const auto& saved = it->second.bindings[bufferIndex];
result.Buffer = saved.buffer;
result.Range = saved.range;
result.HasExplicitRange = saved.hasExplicitRange;
return result;
}
void GLContext::SetNamedTransformFeedbackBinding(Uint index, Uint bufferIndex,
const SharedPtr<BufferObject>& buffer, Range1D range,
Bool hasExplicitRange) {
if (bufferIndex >= MAX_TRANSFORM_FEEDBACK_BUFFERS) return;
if (index == m_boundTransformFeedback) {
auto& point = m_bufferState.GetBindingPoint(BufferTarget::TransformFeedback, bufferIndex);
point.Bind(buffer);
if (buffer && hasExplicitRange) {
point.SetRange(range, true);
} else {
point.ClearRange();
}
return;
}
auto& object = m_transformFeedbackObjects[index];
object.bindings[bufferIndex] = {buffer, range, hasExplicitRange};
}
} // namespace GLState } // namespace GLState
// Leak-at-exit storage; see GlobalObjects.cpp. // Leak-at-exit storage; see GlobalObjects.cpp.
+19
View File
@@ -45,6 +45,14 @@ namespace MobileGL {
// translates the result into its own API call. // translates the result into its own API call.
VertexAttribTypeInfo ClassifyVertexAttribType(GLenum glType); VertexAttribTypeInfo ClassifyVertexAttribType(GLenum glType);
// One indexed capture binding of a transform feedback object, as the by-name queries
// report it. An empty Buffer means the binding point is unbound.
struct NamedTransformFeedbackBinding {
SharedPtr<BufferObject> Buffer;
Range1D Range{};
Bool HasExplicitRange = false;
};
class GLContext { class GLContext {
public: public:
GLContext() = default; GLContext() = default;
@@ -299,6 +307,17 @@ namespace MobileGL {
// cannot express: an empty completed span is legal and draws nothing. // cannot express: an empty completed span is legal and draws nothing.
Bool HasTransformFeedbackCompletedSpan(Uint index) const; Bool HasTransformFeedbackCompletedSpan(Uint index) const;
// The by-name (direct state access) view. A named object that happens to be the
// bound one is answered from the live copy, since that is where its state actually
// is until a bind swaps it out.
void CreateTransformFeedbackObject(Uint index);
Bool IsNamedTransformFeedbackActive(Uint index) const;
Bool IsNamedTransformFeedbackPaused(Uint index) const;
NamedTransformFeedbackBinding GetNamedTransformFeedbackBinding(Uint index, Uint bufferIndex) const;
void SetNamedTransformFeedbackBinding(Uint index, Uint bufferIndex,
const SharedPtr<BufferObject>& buffer, Range1D range,
Bool hasExplicitRange);
// Framebuffer // Framebuffer
void GenFramebufferNames(Uint number, Vector<Uint>& framebuffers); void GenFramebufferNames(Uint number, Vector<Uint>& framebuffers);
const SharedPtr<FramebufferObject>& GetFramebufferObject(Uint index); const SharedPtr<FramebufferObject>& GetFramebufferObject(Uint index);
@@ -348,6 +348,12 @@ namespace MobileGL {
// TODO: add other texture types as needed // TODO: add other texture types as needed
Bool SamplesAsIncompleteTexture(const ITextureObject* texture, const SamplerObject* effectiveSampler) {
const Bool mipmapped =
effectiveSampler != nullptr && effectiveSampler->GetMipmapMode() != SamplerMipmapMode::None;
return !IsMipmapCompleteForFilter(texture, mipmapped);
}
Bool IsMipmapCompleteForFilter(const ITextureObject* texture, Bool mipmapped) { Bool IsMipmapCompleteForFilter(const ITextureObject* texture, Bool mipmapped) {
if (texture == nullptr) return true; if (texture == nullptr) return true;
if (!texture->IsComplete()) return false; if (!texture->IsComplete()) return false;
@@ -164,6 +164,12 @@ namespace MobileGL::MG_State::GLState {
// IsComplete() already covers. Sampling an incomplete texture returns (0, 0, 0, 1). // IsComplete() already covers. Sampling an incomplete texture returns (0, 0, 0, 1).
Bool IsMipmapCompleteForFilter(const ITextureObject* texture, Bool mipmapped); Bool IsMipmapCompleteForFilter(const ITextureObject* texture, Bool mipmapped);
// The rule above asked as the backends need it: does a lookup on this texture read
// (0, 0, 0, 1) instead of its contents? `effectiveSampler` is the sampler object bound
// to the unit when there is one, otherwise the texture's own. A backend answers yes by
// routing the texture to whatever it already uses for "nothing is bound there".
Bool SamplesAsIncompleteTexture(const ITextureObject* texture, const SamplerObject* effectiveSampler);
inline const TextureObjectMipmap* AsMipmapTexture(const ITextureObject* texture) { inline const TextureObjectMipmap* AsMipmapTexture(const ITextureObject* texture) {
return (texture && texture->GetStorageType() == TextureStorageType::Mipmap) return (texture && texture->GetStorageType() == TextureStorageType::Mipmap)
? static_cast<const TextureObjectMipmap*>(texture) ? static_cast<const TextureObjectMipmap*>(texture)
@@ -21,10 +21,31 @@ namespace MobileGL {
BindingSlot<BufferObject>& GetBufferBindingSlot( BindingSlot<BufferObject>& GetBufferBindingSlot(
TextureUploadTarget target = TextureUploadTarget::TextureBuffer); TextureUploadTarget target = TextureUploadTarget::TextureBuffer);
// The window of the attached buffer the texture addresses. glTexBuffer attaches
// the whole buffer, which is expressed here as an offset of 0 and a size of
// kWholeBuffer so a later respecify of the buffer keeps being followed - a stored
// size would freeze the texture at the size the buffer happened to have.
static constexpr SizeT kWholeBuffer = ~static_cast<SizeT>(0);
void SetBufferRange(SizeT offset, SizeT size) {
m_bufferRangeOffset = offset;
m_bufferRangeSize = size;
}
SizeT GetBufferRangeOffset() const { return m_bufferRangeOffset; }
// Resolved against the buffer's current size, so kWholeBuffer tracks it.
SizeT GetBufferRangeSizeInBytes() const {
const auto& buffer = m_bufferBindingSlot.GetBoundObject();
const SizeT bufferSize = buffer != nullptr ? buffer->GetSize() : 0;
if (m_bufferRangeSize == kWholeBuffer) return bufferSize;
const SizeT available = bufferSize > m_bufferRangeOffset ? bufferSize - m_bufferRangeOffset : 0;
return std::min(m_bufferRangeSize, available);
}
protected: protected:
Uint GetIndexOfTextureUploadTarget(TextureUploadTarget target) const override; Uint GetIndexOfTextureUploadTarget(TextureUploadTarget target) const override;
BindingSlot<BufferObject> m_bufferBindingSlot = BindingSlot<BufferObject>(BufferTarget::Texture); BindingSlot<BufferObject> m_bufferBindingSlot = BindingSlot<BufferObject>(BufferTarget::Texture);
SizeT m_bufferRangeOffset = 0;
SizeT m_bufferRangeSize = kWholeBuffer;
const Vector<TextureUploadTarget> m_uploadTargets{TextureUploadTarget::TextureBuffer}; const Vector<TextureUploadTarget> m_uploadTargets{TextureUploadTarget::TextureBuffer};
}; };
} // namespace GLState } // namespace GLState
@@ -77,6 +77,26 @@ namespace MobileGL::MG_State::GLState {
BumpAttributeFormatVersion(index); BumpAttributeFormatVersion(index);
} }
void VertexArrayObject::MirrorPointerIntoBinding(Uint index, const SharedPtr<BufferObject>& buffer, SizeT offset,
int effectiveStride) {
if (index >= MAX_VERTEX_ATTRIBS || index >= MAX_VERTEX_ATTRIB_BINDINGS) return;
// glVertexAttribPointer is defined in terms of the binding model (GL 4.6 core 10.3.2): it
// also sets binding point `index` to the buffer, the pointer as the offset, and the
// *effective* stride, and points the attribute at that binding point with relative offset 0.
// The flat attribute view keeps the raw stride, because VERTEX_ATTRIB_ARRAY_STRIDE reports
// that argument verbatim, so the binding point is recorded alongside the resolved attribute
// rather than being resolved into it.
m_attributeBindingIndex[index] = index;
m_attributeRelativeOffset[index] = 0;
auto& binding = m_bindingPoints[index];
binding.Buffer = buffer;
binding.Offset = offset;
binding.Stride = effectiveStride;
binding.Divisor = m_attributes[index].Divisor;
}
void VertexArrayObject::BindAttributeBuffer(Uint index, const SharedPtr<BufferObject>& buffer) { void VertexArrayObject::BindAttributeBuffer(Uint index, const SharedPtr<BufferObject>& buffer) {
if (index >= MAX_VERTEX_ATTRIBS) return; if (index >= MAX_VERTEX_ATTRIBS) return;
@@ -111,6 +131,11 @@ namespace MobileGL::MG_State::GLState {
void VertexArrayObject::SetAttributeDivisor(Uint index, Uint divisor) { void VertexArrayObject::SetAttributeDivisor(Uint index, Uint divisor) {
if (index >= MAX_VERTEX_ATTRIBS) return; if (index >= MAX_VERTEX_ATTRIBS) return;
// glVertexAttribDivisor is VertexBindingDivisor on the attribute's own binding point
// (GL 4.6 core 10.3.2), so the binding-point view has to follow the resolved attribute.
if (index < MAX_VERTEX_ATTRIB_BINDINGS && m_attributeBindingIndex[index] == index) {
m_bindingPoints[index].Divisor = divisor;
}
if (m_attributes[index].Divisor == divisor) return; if (m_attributes[index].Divisor == divisor) return;
m_attributes[index].Divisor = divisor; m_attributes[index].Divisor = divisor;
BumpAttributeFormatVersion(index); BumpAttributeFormatVersion(index);
@@ -187,18 +212,18 @@ namespace MobileGL::MG_State::GLState {
} }
void VertexArrayObject::SetAttributeFormatSeparate(Uint attribIndex, int size, DataType type, Bool normalized, void VertexArrayObject::SetAttributeFormatSeparate(Uint attribIndex, int size, DataType type, Bool normalized,
Bool isInteger, Uint relativeOffset) { Bool isInteger, Uint relativeOffset, Bool isBgra) {
if (attribIndex >= MAX_VERTEX_ATTRIBS) return; if (attribIndex >= MAX_VERTEX_ATTRIBS) return;
if (size < 1 || size > 4) return; if (size < 1 || size > 4) return;
auto& attr = m_attributes[attribIndex]; auto& attr = m_attributes[attribIndex];
if (attr.Size != size || attr.Type != type || attr.Normalized != normalized || attr.IsInteger != isInteger || if (attr.Size != size || attr.Type != type || attr.Normalized != normalized || attr.IsInteger != isInteger ||
attr.IsBgra || m_attributeRelativeOffset[attribIndex] != relativeOffset) { attr.IsBgra != isBgra || m_attributeRelativeOffset[attribIndex] != relativeOffset) {
attr.Size = size; attr.Size = size;
attr.Type = type; attr.Type = type;
attr.Normalized = normalized; attr.Normalized = normalized;
attr.IsInteger = isInteger; attr.IsInteger = isInteger;
attr.IsBgra = false; // the binding-format path (glVertexAttribFormat) does not carry BGRA attr.IsBgra = isBgra;
m_attributeRelativeOffset[attribIndex] = relativeOffset; m_attributeRelativeOffset[attribIndex] = relativeOffset;
BumpAttributeFormatVersion(attribIndex); BumpAttributeFormatVersion(attribIndex);
} }
@@ -64,6 +64,12 @@ namespace MobileGL {
void BindAttributeBuffer(Uint index, const SharedPtr<BufferObject>& buffer); void BindAttributeBuffer(Uint index, const SharedPtr<BufferObject>& buffer);
// Record what the pointer-style API implies for the binding-point view: attribute
// `index` bound to binding point `index` with relative offset 0, and that binding
// point carrying the buffer, the pointer offset and the effective stride.
void MirrorPointerIntoBinding(Uint index, const SharedPtr<BufferObject>& buffer, SizeT offset,
int effectiveStride);
BindingSlot<BufferObject>& GetIndexBufferBindingSlot(); BindingSlot<BufferObject>& GetIndexBufferBindingSlot();
const BindingSlot<BufferObject>& GetIndexBufferBindingSlot() const; const BindingSlot<BufferObject>& GetIndexBufferBindingSlot() const;
@@ -82,7 +88,22 @@ namespace MobileGL {
void SetBindingDivisor(Uint bindingIndex, Uint divisor); void SetBindingDivisor(Uint bindingIndex, Uint divisor);
void SetAttributeBinding(Uint attribIndex, Uint bindingIndex); void SetAttributeBinding(Uint attribIndex, Uint bindingIndex);
void SetAttributeFormatSeparate(Uint attribIndex, int size, DataType type, Bool normalized, void SetAttributeFormatSeparate(Uint attribIndex, int size, DataType type, Bool normalized,
Bool isInteger, Uint relativeOffset); Bool isInteger, Uint relativeOffset, Bool isBgra = false);
// The binding-point view the attributes were resolved from. Kept queryable
// because glGetVertexArrayIndexed[64]iv reports it verbatim, and the resolved
// flat attribute cannot always be inverted back into it.
Uint GetAttributeRelativeOffset(Uint attribIndex) const {
return attribIndex < m_attributeRelativeOffset.size() ? m_attributeRelativeOffset[attribIndex] : 0;
}
Uint GetAttributeBindingIndex(Uint attribIndex) const {
return attribIndex < m_attributeBindingIndex.size() ? m_attributeBindingIndex[attribIndex]
: attribIndex;
}
const VertexBufferBindingPoint& GetBindingPoint(Uint bindingIndex) const {
static const VertexBufferBindingPoint kEmpty{};
return bindingIndex < m_bindingPoints.size() ? m_bindingPoints[bindingIndex] : kEmpty;
}
const VertexAttributeVersion& GetAttributeVersion(Uint index) const; const VertexAttributeVersion& GetAttributeVersion(Uint index) const;
const Array<VertexAttributeVersion, MAX_VERTEX_ATTRIBS>& GetAllAttributeVersions() const; const Array<VertexAttributeVersion, MAX_VERTEX_ATTRIBS>& GetAllAttributeVersions() const;
+1 -1
View File
@@ -1870,7 +1870,7 @@ TEST_F(TextureTest, DirectGLESTreats2DArrayAsSupportedTextureTarget) {
// Every desktop-only target is stored on an ES one (MapToBackendTextureTarget): 1D and // Every desktop-only target is stored on an ES one (MapToBackendTextureTarget): 1D and
// 1D-array as 2D / 2D-array, matching SPIRV-Cross's ES 1D-as-2D shader emission, and // 1D-array as 2D / 2D-array, matching SPIRV-Cross's ES 1D-as-2D shader emission, and
// rectangle as a plain 2D - it is single-level and already clamps, so only the // rectangle as a plain 2D - it is single-level and already clamps, so only the
// non-normalized coordinates differ and LowerRectImagesForEssl handles those. // non-normalized coordinates differ and LowerRectImages handles those.
EXPECT_TRUE(IsSupportedTextureTarget(TextureTarget::Texture1D)); EXPECT_TRUE(IsSupportedTextureTarget(TextureTarget::Texture1D));
EXPECT_TRUE(IsSupportedTextureTarget(TextureTarget::Texture1DArray)); EXPECT_TRUE(IsSupportedTextureTarget(TextureTarget::Texture1DArray));
EXPECT_TRUE(IsSupportedTextureTarget(TextureTarget::TextureRectangle)); EXPECT_TRUE(IsSupportedTextureTarget(TextureTarget::TextureRectangle));
@@ -1056,6 +1056,18 @@ namespace MobileGL::MG_Util::BackendLoader {
caps.MaxComputeWorkGroupInvocations = maxComputeWorkGroupInvocations; caps.MaxComputeWorkGroupInvocations = maxComputeWorkGroupInvocations;
caps.MaxShaderStorageBufferBindings = maxShaderStorageBufferBindings; caps.MaxShaderStorageBufferBindings = maxShaderStorageBufferBindings;
caps.MaxTextureBufferSize = maxTextureBufferSize; caps.MaxTextureBufferSize = maxTextureBufferSize;
// Through glesFuncs, like every other capability query here: a bare glGetIntegerv resolves
// to MobileGL's own exported entry point, which answers this pname from the very
// capability table being filled in - so the driver's real alignment never arrived and the
// backend reported an unconstrained offset it cannot honour.
GLint textureBufferOffsetAlignment = 1;
glesFuncs.glGetIntegerv(GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT, &textureBufferOffsetAlignment);
// Core in ES 3.2 and in EXT_texture_buffer; an older context rejects the pname and leaves
// the default in place.
if (glesFuncs.glGetError) {
while (glesFuncs.glGetError() != GL_NO_ERROR) {}
}
caps.TextureBufferOffsetAlignment = std::max(1, textureBufferOffsetAlignment);
caps.MaxUniformBufferBindings = maxUniformBufferBindings; caps.MaxUniformBufferBindings = maxUniformBufferBindings;
caps.MaxUniformBlockSize = maxUniformBlockSize; caps.MaxUniformBlockSize = maxUniformBlockSize;
caps.MaxImageUnits = maxImageUnits; caps.MaxImageUnits = maxImageUnits;
@@ -1117,6 +1117,8 @@ namespace MobileGL {
Int MaxComputeWorkGroupInvocations = 128; Int MaxComputeWorkGroupInvocations = 128;
Int MaxShaderStorageBufferBindings = 8; Int MaxShaderStorageBufferBindings = 8;
Int MaxTextureBufferSize = 65536; Int MaxTextureBufferSize = 65536;
// GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT; 1 means the offset is unconstrained.
Int TextureBufferOffsetAlignment = 1;
Int MaxUniformBufferBindings = 24; Int MaxUniformBufferBindings = 24;
Int MaxUniformBlockSize = 16384; Int MaxUniformBlockSize = 16384;
Int MaxImageUnits = 8; Int MaxImageUnits = 8;
@@ -177,6 +177,8 @@ namespace MobileGL::MG_Util::BackendLoader {
caps.MaxComputeWorkGroupInvocations = static_cast<Int>(p.limits.maxComputeWorkGroupInvocations); caps.MaxComputeWorkGroupInvocations = static_cast<Int>(p.limits.maxComputeWorkGroupInvocations);
caps.MaxShaderStorageBufferBindings = static_cast<Int>(p.limits.maxDescriptorSetStorageBuffers); caps.MaxShaderStorageBufferBindings = static_cast<Int>(p.limits.maxDescriptorSetStorageBuffers);
caps.MaxTextureBufferSize = static_cast<Int>(p.limits.maxTexelBufferElements); caps.MaxTextureBufferSize = static_cast<Int>(p.limits.maxTexelBufferElements);
caps.TextureBufferOffsetAlignment =
static_cast<Int>(std::max<VkDeviceSize>(1, p.limits.minTexelBufferOffsetAlignment));
caps.MaxUniformBufferBindings = static_cast<Int>(p.limits.maxDescriptorSetUniformBuffers); caps.MaxUniformBufferBindings = static_cast<Int>(p.limits.maxDescriptorSetUniformBuffers);
caps.MaxUniformBlockSize = static_cast<Int>(p.limits.maxUniformBufferRange); caps.MaxUniformBlockSize = static_cast<Int>(p.limits.maxUniformBufferRange);
caps.MaxImageUnits = static_cast<Int>(p.limits.maxPerStageDescriptorStorageImages); caps.MaxImageUnits = static_cast<Int>(p.limits.maxPerStageDescriptorStorageImages);
@@ -268,6 +270,8 @@ namespace MobileGL::MG_Util::BackendLoader {
caps.MaxComputeWorkGroupInvocations = static_cast<Int>(properties.limits.maxComputeWorkGroupInvocations); caps.MaxComputeWorkGroupInvocations = static_cast<Int>(properties.limits.maxComputeWorkGroupInvocations);
caps.MaxShaderStorageBufferBindings = static_cast<Int>(properties.limits.maxDescriptorSetStorageBuffers); caps.MaxShaderStorageBufferBindings = static_cast<Int>(properties.limits.maxDescriptorSetStorageBuffers);
caps.MaxTextureBufferSize = static_cast<Int>(properties.limits.maxTexelBufferElements); caps.MaxTextureBufferSize = static_cast<Int>(properties.limits.maxTexelBufferElements);
caps.TextureBufferOffsetAlignment =
static_cast<Int>(std::max<VkDeviceSize>(1, properties.limits.minTexelBufferOffsetAlignment));
caps.MaxUniformBufferBindings = static_cast<Int>(properties.limits.maxDescriptorSetUniformBuffers); caps.MaxUniformBufferBindings = static_cast<Int>(properties.limits.maxDescriptorSetUniformBuffers);
caps.MaxUniformBlockSize = static_cast<Int>(properties.limits.maxUniformBufferRange); caps.MaxUniformBlockSize = static_cast<Int>(properties.limits.maxUniformBufferRange);
caps.MaxImageUnits = static_cast<Int>(properties.limits.maxPerStageDescriptorStorageImages); caps.MaxImageUnits = static_cast<Int>(properties.limits.maxPerStageDescriptorStorageImages);
@@ -54,6 +54,8 @@ namespace MobileGL {
Int MaxComputeWorkGroupInvocations = 128; Int MaxComputeWorkGroupInvocations = 128;
Int MaxShaderStorageBufferBindings = 8; Int MaxShaderStorageBufferBindings = 8;
Int MaxTextureBufferSize = 65536; Int MaxTextureBufferSize = 65536;
// GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT; 1 means the offset is unconstrained.
Int TextureBufferOffsetAlignment = 1;
Int MaxUniformBufferBindings = 24; Int MaxUniformBufferBindings = 24;
Int MaxUniformBlockSize = 16384; Int MaxUniformBlockSize = 16384;
Int MaxImageUnits = 8; Int MaxImageUnits = 8;
@@ -38,6 +38,10 @@ namespace MobileGL {
return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST_WITH_ADJACENCY; return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST_WITH_ADJACENCY;
case GL_TRIANGLE_STRIP_ADJACENCY: case GL_TRIANGLE_STRIP_ADJACENCY:
return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP_WITH_ADJACENCY; return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP_WITH_ADJACENCY;
case GL_PATCHES:
// The tessellator decides what a patch becomes; its vertex count is pipeline
// state (patchControlPoints), not part of the topology.
return VK_PRIMITIVE_TOPOLOGY_PATCH_LIST;
default: default:
MGLOG_W("Unrecognized primitive topology"); MGLOG_W("Unrecognized primitive topology");
return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
+52
View File
@@ -298,6 +298,25 @@ namespace MobileGL::MG_Util::SelfTest {
"not supported; no impact: the native indirect path deliberately does not " "not supported; no impact: the native indirect path deliberately does not "
"rely on it (shader-side emulation handles baseInstance semantics)"); "rely on it (shader-side emulation handles baseInstance semantics)");
} }
if (glesFuncs.glPatchParameteri != nullptr) {
builder.Pass("Tessellation patch parameters",
"glPatchParameteri present (GL_PATCH_VERTICES reaches the driver)");
} else {
builder.Warn("Tessellation patch parameters",
"glPatchParameteri missing (pre-ES 3.2 without GL_EXT_tessellation_shader); "
"GL_PATCH_VERTICES stays at the driver default of 3 and a patch draw of any "
"other size renders nothing");
}
if (glesFuncs.glGenTransformFeedbacks != nullptr && glesFuncs.glBindTransformFeedback != nullptr &&
glesFuncs.glPauseTransformFeedback != nullptr && glesFuncs.glResumeTransformFeedback != nullptr) {
builder.Pass("Transform feedback objects",
"supported (each GL transform feedback object gets one of the driver's, so "
"several can hold a paused capture at once)");
} else {
builder.Warn("Transform feedback objects",
"entry points missing; every GL transform feedback object shares the driver's "
"default one, so a second object cannot open a capture while the first is paused");
}
if (caps.SupportsNorm16Texture) { if (caps.SupportsNorm16Texture) {
builder.Pass("GL_EXT_texture_norm16", "supported"); builder.Pass("GL_EXT_texture_norm16", "supported");
} else { } else {
@@ -1412,6 +1431,39 @@ namespace MobileGL::MG_Util::SelfTest {
"hard-fails at draw"); "hard-fails at draw");
} }
// Core 1.0 features the backend turns GL stages into pipeline stages with.
VkPhysicalDeviceFeatures coreFeatures{};
vkGetPhysicalDeviceFeatures(physicalDevice, &coreFeatures);
if (coreFeatures.tessellationShader == VK_TRUE) {
builder.Pass("tessellationShader",
"supported (GL_PATCHES draws run the tessellation control/evaluation stages)");
} else {
builder.Warn("tessellationShader",
"unsupported; a program with a tessellation control/evaluation shader cannot build a "
"pipeline, so GL_PATCHES draws render nothing");
}
Bool vertexAttributeInstanceRateDivisor = false;
if (vkGetPhysicalDeviceFeatures2Fn != nullptr &&
HasVkExtension(deviceExtensions, VK_EXT_VERTEX_ATTRIBUTE_DIVISOR_EXTENSION_NAME)) {
VkPhysicalDeviceVertexAttributeDivisorFeaturesEXT divisorFeatures{};
divisorFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VERTEX_ATTRIBUTE_DIVISOR_FEATURES_EXT;
VkPhysicalDeviceFeatures2 features2{};
features2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2;
features2.pNext = &divisorFeatures;
vkGetPhysicalDeviceFeatures2Fn(physicalDevice, &features2);
vertexAttributeInstanceRateDivisor = divisorFeatures.vertexAttributeInstanceRateDivisor == VK_TRUE;
}
if (vertexAttributeInstanceRateDivisor) {
builder.Pass("vertexAttributeInstanceRateDivisor",
"supported (glVertexAttribDivisor advances an attribute every N instances)");
} else {
builder.Warn("vertexAttributeInstanceRateDivisor",
"unsupported; Vulkan's instance input rate can only advance once per instance, so "
"every non-zero glVertexAttribDivisor behaves as 1 and instanced attributes meant to "
"change every N instances change every one");
}
if (vkGetPhysicalDeviceProperties2Fn != nullptr && properties.apiVersion >= VK_API_VERSION_1_1) { if (vkGetPhysicalDeviceProperties2Fn != nullptr && properties.apiVersion >= VK_API_VERSION_1_1) {
VkPhysicalDeviceSubgroupProperties subgroupProperties{}; VkPhysicalDeviceSubgroupProperties subgroupProperties{};
subgroupProperties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES; subgroupProperties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES;
@@ -19,6 +19,7 @@
#include "SpirvPasses/DecoratePositionInvariantPass.h" #include "SpirvPasses/DecoratePositionInvariantPass.h"
#include "SpirvPasses/LowerDrawParametersPass.h" #include "SpirvPasses/LowerDrawParametersPass.h"
#include "SpirvPasses/RebaseInstanceIndexPass.h" #include "SpirvPasses/RebaseInstanceIndexPass.h"
#include "SpirvPasses/NormalizeRectCoordinatesPass.h"
#include "SpirvPasses/StripUboMemberRelaxedPrecisionPass.h" #include "SpirvPasses/StripUboMemberRelaxedPrecisionPass.h"
#include "SpirvPasses/StripNoPerspectivePass.h" #include "SpirvPasses/StripNoPerspectivePass.h"
#include "SpirvPasses/EmulateNoPerspectivePass.h" #include "SpirvPasses/EmulateNoPerspectivePass.h"
@@ -363,90 +364,16 @@ namespace MobileGL {
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options); return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
} }
bool ShaderCompiler::LowerRectImagesForEssl(const Vector<Uint32>& inputBinary, bool ShaderCompiler::LowerRectImages(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary) { Vector<uint32_t>& outputBinary) {
constexpr SizeT kSpirvHeaderWordCount = 5; using namespace spvtools;
// OpTypeImage: [0] opcode/wordcount, [1] result id, [2] sampled type, [3] Dim, ... OptimizerOptions options;
constexpr SizeT kTypeImageDimWordIndex = 3; options.set_run_validator(false);
constexpr SizeT kTypeImageMinWordCount = 9;
outputBinary.clear();
if (inputBinary.size() < kSpirvHeaderWordCount || inputBinary[0] != spv::MagicNumber) {
return false;
}
Vector<SizeT> rectDimWordOffsets; Optimizer optimizer(SPV_ENV_VULKAN_1_1);
Vector<SizeT> rectCapabilityWordOffsets; optimizer.RegisterPass(NormalizeRectCoordinatesPass::CreateNormalizeRectCoordinatesPass());
Bool hasNormalizedCoordinateLookup = false;
for (SizeT offset = kSpirvHeaderWordCount; offset < inputBinary.size();) {
const Uint32 instructionWord = inputBinary[offset];
const SizeT wordCount = instructionWord >> 16u;
const auto opcode = static_cast<spv::Op>(instructionWord & 0xffffu);
if (wordCount == 0 || offset + wordCount > inputBinary.size()) {
return false;
}
if (opcode == spv::Op::OpTypeImage && wordCount >= kTypeImageMinWordCount) { return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
if (static_cast<spv::Dim>(inputBinary[offset + kTypeImageDimWordIndex]) == spv::Dim::Rect) {
rectDimWordOffsets.push_back(offset + kTypeImageDimWordIndex);
}
} else if (opcode == spv::Op::OpCapability && wordCount >= 2) {
const auto capability = static_cast<spv::Capability>(inputBinary[offset + 1]);
if (capability == spv::Capability::SampledRect ||
capability == spv::Capability::ImageRect) {
rectCapabilityWordOffsets.push_back(offset + 1);
}
} else {
switch (opcode) {
// Normalized-coordinate lookups whose ESSL form the backend's
// NormalizeRectSamplerCoordinates post-pass cannot repair: the
// coordinate is either fused with something else in a single argument
// (the Dref sample forms carry the compare value in coord.z) or the
// divide would have to happen after a projective divide. Tracing each
// one back to its image type would let a module mix a normalized 2D
// lookup with a rectangle fetch, but the extra reach is not worth the
// risk of getting the trace wrong: decline the whole module instead.
//
// OpImageSampleImplicitLod, OpImageGather and OpImageDrefGather are
// absent because all three become an ESSL call whose argument 1 is the
// bare texel-space coordinate, which the post-pass divides by the
// texture size.
case spv::Op::OpImageSampleExplicitLod:
case spv::Op::OpImageSampleDrefImplicitLod:
case spv::Op::OpImageSampleDrefExplicitLod:
case spv::Op::OpImageSampleProjImplicitLod:
case spv::Op::OpImageSampleProjExplicitLod:
case spv::Op::OpImageSampleProjDrefImplicitLod:
case spv::Op::OpImageSampleProjDrefExplicitLod:
case spv::Op::OpImageSparseSampleImplicitLod:
case spv::Op::OpImageSparseSampleExplicitLod:
case spv::Op::OpImageSparseSampleDrefImplicitLod:
case spv::Op::OpImageSparseSampleDrefExplicitLod:
case spv::Op::OpImageSparseGather:
case spv::Op::OpImageSparseDrefGather:
hasNormalizedCoordinateLookup = true;
break;
default:
break;
}
}
offset += wordCount;
}
if (rectDimWordOffsets.empty() || hasNormalizedCoordinateLookup) {
return false;
}
outputBinary.assign(inputBinary.begin(), inputBinary.end());
for (const SizeT dimWordOffset : rectDimWordOffsets) {
outputBinary[dimWordOffset] = static_cast<Uint32>(spv::Dim::Dim2D);
}
// The rectangle capabilities describe types that no longer exist. Shader is always
// declared by a graphics module, so restating it keeps the word count intact
// without leaving a capability SPIRV-Cross would key off.
for (const SizeT capabilityWordOffset : rectCapabilityWordOffsets) {
outputBinary[capabilityWordOffset] = static_cast<Uint32>(spv::Capability::Shader);
}
return true;
} }
bool ShaderCompiler::RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary, bool ShaderCompiler::RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
@@ -43,20 +43,15 @@ namespace MobileGL {
// devices lacking GL_NV_shader_noperspective_interpolation. See EmulateNoPerspectivePass. // devices lacking GL_NV_shader_noperspective_interpolation. See EmulateNoPerspectivePass.
static bool EmulateNoPerspectiveForEssl(const Vector<Uint32>& inputBinary, static bool EmulateNoPerspectiveForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary); Vector<uint32_t>& outputBinary);
// Rewrites rectangle images (Dim::Rect) to plain 2D so SPIRV-Cross can emit ESSL
// for them at all - it refuses outright ("Rectangle textures are not supported on
// OpenGL ES"), which left the whole program unlinkable. Only valid while every use
// of the image takes integer texel coordinates (texelFetch / textureSize), where a
// rectangle target and a 2D target are indistinguishable; a normalized-coordinate
// lookup would also need its coordinates divided by the texture size, so the pass
// declines those modules instead of emitting something subtly wrong. Returns false
// when it changed nothing or cannot safely convert. DirectGLES only.
static bool LowerRectImagesForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary);
// Rebases loads of the InstanceIndex builtin to (InstanceIndex - BaseInstance) so // Rebases loads of the InstanceIndex builtin to (InstanceIndex - BaseInstance) so
// shaders see GL's zero-based gl_InstanceID. Vertex shaders only; DirectVulkan // shaders see GL's zero-based gl_InstanceID. Vertex shaders only; DirectVulkan
// backend only (glslang's relaxed mode aliases gl_InstanceID to gl_InstanceIndex, // backend only (glslang's relaxed mode aliases gl_InstanceID to gl_InstanceIndex,
// which wrongly includes baseInstance). // which wrongly includes baseInstance).
// GL_TEXTURE_RECTANGLE emulated on a plain 2D texture, for every backend:
// divides the coordinate of each normalized-coordinate lookup by the texture
// size and rewrites the image type to 2D. See NormalizeRectCoordinatesPass for
// what it declines and why.
static bool LowerRectImages(const Vector<Uint32>& inputBinary, Vector<uint32_t>& outputBinary);
static bool RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary, static bool RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary); Vector<uint32_t>& outputBinary);
// Adds the Invariant decoration to every Position builtin output. GL apps // Adds the Invariant decoration to every Position builtin output. GL apps
@@ -0,0 +1,193 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "NormalizeRectCoordinatesPass.h"
#include "spirv.hpp"
#include "source/opt/constants.h"
#include "source/opt/def_use_manager.h"
#include "source/opt/instruction.h"
#include "source/opt/ir_builder.h"
#include "source/opt/ir_context.h"
#include "source/opt/module.h"
#include "source/opt/type_manager.h"
#include "source/opt/types.h"
#include "source/util/make_unique.h"
#include <unordered_set>
#include <vector>
namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
namespace {
using spvtools::opt::Instruction;
using spvtools::opt::IRContext;
using spvtools::opt::InstructionBuilder;
// Image operations whose coordinate operand (in-operand 1) is a plain
// normalized coordinate and nothing else. The Dref *sample* forms are absent:
// they pack the compare value into the coordinate's last component, so the
// divide cannot be applied componentwise. OpImageDrefGather is here because it
// carries the compare value in a separate operand.
bool TakesPlainNormalizedCoordinate(spv::Op opcode) {
switch (opcode) {
case spv::Op::OpImageSampleImplicitLod:
case spv::Op::OpImageSampleExplicitLod:
case spv::Op::OpImageGather:
case spv::Op::OpImageDrefGather:
return true;
default:
return false;
}
}
// The OpTypeImage behind whatever an image operation was handed - a sampled
// image, a bare image, or a pointer to either. Returns nullptr when the operand
// is not an image at all.
Instruction* ResolveImageType(IRContext* context, uint32_t objectId) {
auto* defUseMgr = context->get_def_use_mgr();
Instruction* object = defUseMgr->GetDef(objectId);
if (object == nullptr) return nullptr;
Instruction* type = defUseMgr->GetDef(object->type_id());
while (type != nullptr) {
switch (type->opcode()) {
case spv::Op::OpTypeImage:
return type;
case spv::Op::OpTypeSampledImage:
case spv::Op::OpTypePointer:
// Both name their element type in their last in-operand.
type = defUseMgr->GetDef(type->GetSingleWordInOperand(type->NumInOperands() - 1));
continue;
default:
return nullptr;
}
}
return nullptr;
}
bool IsRectImageType(const Instruction* imageType) {
// OpTypeImage in-operands: sampled type, Dim, Depth, Arrayed, MS, Sampled, Format.
return imageType != nullptr && imageType->NumInOperands() >= 2 &&
static_cast<spv::Dim>(imageType->GetSingleWordInOperand(1)) == spv::Dim::Rect;
}
// The bare image an OpImageQuerySizeLod needs. An operation on a sampled image
// has to unwrap it first; one already holding a bare image is used as is.
uint32_t GetQueryableImage(IRContext* context, InstructionBuilder& builder, uint32_t imageOperandId,
uint32_t imageTypeId) {
auto* defUseMgr = context->get_def_use_mgr();
Instruction* object = defUseMgr->GetDef(imageOperandId);
if (object == nullptr) return 0;
Instruction* type = defUseMgr->GetDef(object->type_id());
if (type != nullptr && type->opcode() == spv::Op::OpTypeImage) {
return imageOperandId;
}
Instruction* unwrapped = builder.AddUnaryOp(imageTypeId, spv::Op::OpImage, imageOperandId);
return unwrapped != nullptr ? unwrapped->result_id() : 0;
}
} // namespace
spvtools::opt::Pass::Status NormalizeRectCoordinatesPass::Process() {
auto* irContext = context();
auto* typeMgr = irContext->get_type_mgr();
auto* constantMgr = irContext->get_constant_mgr();
// Nothing to do unless the module actually declares a rectangle image.
bool hasRectImageType = false;
for (const Instruction& type : irContext->types_values()) {
if (type.opcode() == spv::Op::OpTypeImage && IsRectImageType(&type)) {
hasRectImageType = true;
break;
}
}
if (!hasRectImageType) {
return Status::SuccessWithoutChange;
}
spvtools::opt::analysis::Integer signedInt(32, true);
spvtools::opt::analysis::Float float32(32);
spvtools::opt::analysis::Vector int2(&signedInt, 2);
spvtools::opt::analysis::Vector float2(&float32, 2);
const uint32_t int2TypeId = typeMgr->GetTypeInstruction(&int2);
const uint32_t float2TypeId = typeMgr->GetTypeInstruction(&float2);
const uint32_t lodZeroId = constantMgr->GetSIntConstId(0);
if (int2TypeId == 0 || float2TypeId == 0 || lodZeroId == 0) {
return Status::Failure;
}
bool rewroteCoordinate = false;
for (auto& function : *irContext->module()) {
for (auto& block : function) {
for (auto& instruction : block) {
if (!TakesPlainNormalizedCoordinate(instruction.opcode()) ||
instruction.NumInOperands() < 2) {
continue;
}
const uint32_t imageOperandId = instruction.GetSingleWordInOperand(0);
Instruction* imageType = ResolveImageType(irContext, imageOperandId);
if (!IsRectImageType(imageType)) {
continue;
}
const uint32_t coordinateId = instruction.GetSingleWordInOperand(1);
InstructionBuilder builder(
irContext, &instruction,
IRContext::kAnalysisDefUse | IRContext::kAnalysisInstrToBlockMapping);
const uint32_t queryableImageId =
GetQueryableImage(irContext, builder, imageOperandId, imageType->result_id());
if (queryableImageId == 0) {
return Status::Failure;
}
// A rectangle image has exactly one level, so the query's level is 0.
// The lod form is what the 2D type this becomes accepts.
Instruction* size = builder.AddBinaryOp(int2TypeId, spv::Op::OpImageQuerySizeLod,
queryableImageId, lodZeroId);
Instruction* sizeFloat =
builder.AddUnaryOp(float2TypeId, spv::Op::OpConvertSToF, size->result_id());
Instruction* normalized = builder.AddBinaryOp(
float2TypeId, spv::Op::OpFDiv, coordinateId, sizeFloat->result_id());
instruction.SetInOperand(1, {normalized->result_id()});
irContext->UpdateDefUse(&instruction);
rewroteCoordinate = true;
}
}
}
// Now that no lookup depends on the rectangle semantics any more, the type can
// become the 2D one both targets accept. Done unconditionally, because a module
// that only ever fetched texels still has to lose the type.
for (Instruction& type : irContext->types_values()) {
if (type.opcode() == spv::Op::OpTypeImage && IsRectImageType(&type)) {
type.SetInOperand(1, {static_cast<uint32_t>(spv::Dim::Dim2D)});
}
}
// The rectangle capabilities describe types that no longer exist. Shader is
// always declared by a graphics module, so restating it keeps the instruction
// valid without leaving a capability a consumer would key off.
for (Instruction& capability : irContext->capabilities()) {
const auto value = static_cast<spv::Capability>(capability.GetSingleWordInOperand(0));
if (value == spv::Capability::SampledRect || value == spv::Capability::ImageRect) {
capability.SetInOperand(0, {static_cast<uint32_t>(spv::Capability::Shader)});
}
}
if (rewroteCoordinate) {
irContext->AddCapability(spv::Capability::ImageQuery);
}
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
return Status::SuccessWithChange;
}
spvtools::Optimizer::PassToken NormalizeRectCoordinatesPass::CreateNormalizeRectCoordinatesPass() {
return spvtools::Optimizer::PassToken(spvtools::MakeUnique<NormalizeRectCoordinatesPass>());
}
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
@@ -0,0 +1,38 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include "spirv-tools/optimizer.hpp"
#include "source/opt/pass.h"
namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
// GL_TEXTURE_RECTANGLE has no counterpart in either target API: ESSL has no
// rectangle sampler at all, and Vulkan's SPIR-V environment does not allow
// Dim::Rect. Both emulate it on a plain 2D texture, which differs in exactly one
// way - a rectangle lookup addresses texels while a 2D one addresses [0,1].
//
// This pass closes that difference in the module itself, so neither backend has
// to reason about it: every lookup that takes normalized coordinates gets its
// coordinate divided by the texture's size, and the image type is then rewritten
// to 2D. texelFetch is untouched (integer texel coordinates mean the same thing
// on both), and so is the Dref *sample* form, whose coordinate carries the
// compare value in the last component - the module keeps its rectangle type and
// the caller declines it.
class NormalizeRectCoordinatesPass final : public spvtools::opt::Pass {
public:
const char* name() const override { return "mobilegl-normalize-rect-coordinates"; }
Status Process() override;
static spvtools::Optimizer::PassToken CreateNormalizeRectCoordinatesPass();
};
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
+63 -1
View File
@@ -1,12 +1,16 @@
# Running the OpenGL CTS against MobileGL # Running the OpenGL CTS against MobileGL
This directory contains two supported paths: This directory contains three supported paths:
- Android arm64 / MobileGL EGL: the KHR-GL33 workflow documented below and in - Android arm64 / MobileGL EGL: the KHR-GL33 workflow documented below and in
`skills/gl-cts-on-mobilegl/SKILL.md`. `skills/gl-cts-on-mobilegl/SKILL.md`.
- Windows x64 / MobileGL WGL: the GL30-GL46 pipeline in - Windows x64 / MobileGL WGL: the GL30-GL46 pipeline in
`scripts/wgl_glcts_pipeline.py`, documented by `scripts/wgl_glcts_pipeline.py`, documented by
`skills/wgl-gl-cts-on-mobilegl/SKILL.md`. `skills/wgl-gl-cts-on-mobilegl/SKILL.md`.
- Desktop Linux x64 / MobileGL EGL: `scripts/run_cts_local.py` against the
`mobilegl-desktop` VK-GL-CTS target, documented in "Desktop Linux workflow"
below and in `skills/linux-gl-cts-on-mobilegl/SKILL.md`. This is the path that
needs no device and no GPU.
Windows prerequisites are Git, Python 3.9+, CMake, Visual Studio 2022's Desktop Windows prerequisites are Git, Python 3.9+, CMake, Visual Studio 2022's Desktop
C++ workload, and a Vulkan SDK visible to CMake. DirectVulkan also needs a C++ workload, and a Vulkan SDK visible to CMake. DirectVulkan also needs a
@@ -25,6 +29,64 @@ resumes individual suites after crashes/timeouts, and writes Markdown plus JSON
reports below the printed `runs/<first-16-of-run-fingerprint>` directory. Its reports below the printed `runs/<first-16-of-run-fingerprint>` directory. Its
manifest records provenance and the runner settings used to validate a resume. manifest records provenance and the runner settings used to validate a resume.
## Desktop Linux workflow
The `mobilegl-desktop` target builds `glcts` as an ordinary host executable that
reaches OpenGL only through `libMobileGL.so`. Both backends run headless with no
GPU at all, which makes this the cheapest way to measure a single test group
while working on it.
Apt packages: `mesa-vulkan-drivers` (lavapipe, for DirectVulkan),
`libegl1-mesa-dev` and `libgles2-mesa-dev` (the system EGL/ES that DirectGLES
drives), `libvulkan-dev`, `ninja-build`.
```sh
cmake -S . -B build-linux -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DMOBILEGL_BUILD_TEST=OFF -DMOBILEGL_BUILD_BENCHMARK=OFF
cmake --build build-linux --parallel "$(nproc)"
python tools/cts/scripts/sync_to_cts.py "$CTS"
git -C "$CTS" apply <path-to>/tools/cts/patches/0001-fbo-color-texture-attachment.patch
cmake -S "$CTS" -B "$CTS/build-cts" -G Ninja -DDEQP_TARGET=mobilegl-desktop \
-DCMAKE_BUILD_TYPE=Release
ninja -C "$CTS/build-cts" glcts
python tools/cts/scripts/run_cts_local.py --backend DirectGLES \
--glcts "$CTS/build-cts/external/openglcts/modules/glcts" \
--lib build-linux/libMobileGL.so \
--caselist cases.txt --outdir runs/gles --env EGL_PLATFORM=surfaceless
python tools/cts/scripts/qpa_report.py runs/gles --label DirectGLES
```
`EGL_PLATFORM=surfaceless` is not optional for DirectGLES: without a `/dev/dri`
node Mesa's EGL fails `eglInitialize` on the default display, and MobileGL
reports that as `EGL_BAD_ALLOC` out of `eglCreatePbufferSurface`. DirectVulkan
needs nothing extra - lavapipe exposes `VK_EXT_headless_surface`, which is what
the desktop platform port's pbuffer path requires.
Two known differences from a real GPU, both MobileGL's rather than the harness's:
DirectVulkan reads back zeros from the **default** framebuffer (a user FBO,
renderbuffer- or texture-attached, is correct on both backends), and lavapipe
supports renderbuffer formats that Adreno reports as unsupported, so the Android
runs see `NotSupported` where these do not.
### Reference results: KHR-GL45.direct_state_access
`opengl-cts-4.6.8.1`, the 371 `direct_state_access` cases of the `gl45-main`
mustpass list, lavapipe / Mesa 25.2.8, `--deqp-surface-type=fbo`.
| backend | conformance | strict Pass | Fail | InternalError | Crash |
| --- | ---: | ---: | ---: | ---: | ---: |
| DirectGLES | **74.93%** | 67.39% | 85 | 8 | 0 |
| DirectVulkan | **73.32%** | 73.05% | 88 | 8 | 3 |
`textures_storage_multisample_*` is 54 of what remains on either backend and
needs a feature rather than a fix: a multisample texture has to be attachable to
a framebuffer and then sampleable through `sampler2DMS`, and the array half of
the group additionally needs layered attachments, which the framebuffer
attachment model does not represent yet. `program_pipelines_*` needs
ARB_separate_shader_objects, which is stubbed throughout.
## Android KHR-GL33 workflow ## Android KHR-GL33 workflow
Goal: measure how much of the OpenGL 3.3 core-profile conformance suite MobileGL Goal: measure how much of the OpenGL 3.3 core-profile conformance suite MobileGL
+1
View File
@@ -17,3 +17,4 @@ Each skill is a self-contained package, matching the layout used by
| --- | --- | | --- | --- |
| [gl-cts-on-mobilegl](gl-cts-on-mobilegl/SKILL.md) | Build VK-GL-CTS `glcts` as a standalone Android arm64 binary against MobileGL's own EGL, run KHR-GL33, and report a per-backend OpenGL 3.3 core conformance rate. | | [gl-cts-on-mobilegl](gl-cts-on-mobilegl/SKILL.md) | Build VK-GL-CTS `glcts` as a standalone Android arm64 binary against MobileGL's own EGL, run KHR-GL33, and report a per-backend OpenGL 3.3 core conformance rate. |
| [wgl-gl-cts-on-mobilegl](wgl-gl-cts-on-mobilegl/SKILL.md) | Build MobileGL's Windows x64 WGL drop-in, run GL30-GL46 core CTS against DirectGLES and DirectVulkan, resume safely, and emit validated reports. | | [wgl-gl-cts-on-mobilegl](wgl-gl-cts-on-mobilegl/SKILL.md) | Build MobileGL's Windows x64 WGL drop-in, run GL30-GL46 core CTS against DirectGLES and DirectVulkan, resume safely, and emit validated reports. |
| [linux-gl-cts-on-mobilegl](linux-gl-cts-on-mobilegl/SKILL.md) | Build `glcts` as a desktop Linux host binary against MobileGL, run any CTS group headlessly with no GPU, and report Espryt and Magma separately. The path to reach for while iterating on a fix. |
@@ -0,0 +1,159 @@
---
name: linux-gl-cts-on-mobilegl
description: Run the Khronos OpenGL CTS (VK-GL-CTS glcts) against MobileGL on desktop Linux with no GPU and no device, and report a per-backend conformance rate for Espryt (DirectGLES) and Magma (DirectVulkan). Use when measuring or iterating on the conformance of one test group, when a fix needs a before/after number, or when neither an Android device nor a Windows GPU box is available.
---
# OpenGL CTS on MobileGL (desktop Linux)
## Overview
`glcts` is built as an ordinary x86-64 host executable that reaches OpenGL only
through `libMobileGL.so`, using the `mobilegl-desktop` VK-GL-CTS target and the
`tcu::Platform` port in `tools/cts/platform/`. Nothing links `libGL` or `libEGL`,
so a result is unambiguously MobileGL's.
Both backends run headless on software rendering, so this needs no GPU at all:
- **Espryt** (`DirectGLES`) drives Mesa's OpenGL ES through the system EGL.
- **Magma** (`DirectVulkan`) runs on lavapipe, whose `VK_EXT_headless_surface`
is what the desktop platform port's pbuffer path requires.
Always report the two backends **separately**. They are different
implementations of the same front end, they fail different cases, and a single
combined number hides which one a change moved.
## Prerequisites
```sh
sudo apt-get install -y ninja-build cmake libvulkan-dev \
libegl1-mesa-dev libgles2-mesa-dev mesa-vulkan-drivers
```
`mesa-vulkan-drivers` is what installs lavapipe; without it DirectVulkan has no
ICD and `eglInitialize` fails inside the backend. A C++23 toolchain is required
(GCC 13+, or Clang 20+ — Clang 18 defines `__cpp_concepts` as 201907L, which
switches libstdc++'s `<expected>` off and the build fails in
`MG_Util/ShaderTranspiler/Types.h`).
```sh
export MG=<path-to-MobileGL-worktree>
export CTS=<path-to-VK-GL-CTS-checkout>
```
## Step 1 — build libMobileGL.so
```sh
git -C "$MG" submodule update --init --recursive
python3 "$MG/3rdparty/glslang/update_glslang_sources.py" # SPIRV-Tools; ENABLE_OPT is forced on
cmake -S "$MG" -B "$MG/build-linux" -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DMOBILEGL_BUILD_TEST=OFF -DMOBILEGL_BUILD_BENCHMARK=OFF \
-DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache
cmake --build "$MG/build-linux" --parallel "$(nproc)"
```
Add `-DCMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE=FALSE` when iterating. The
Release configuration turns LTO on, which makes every relink cost minutes for no
behavioural difference; a conformance number is identical either way.
## Step 2 — get VK-GL-CTS and build glcts
Use a release tag so the mustpass list, and therefore the reported rate, is
citable.
```sh
git -C "$CTS" checkout opengl-cts-4.6.8.1
python3 "$CTS/external/fetch_sources.py"
python3 "$MG/tools/cts/scripts/sync_to_cts.py" "$CTS"
git -C "$CTS" apply "$MG/tools/cts/patches/0001-fbo-color-texture-attachment.patch"
cmake -S "$CTS" -B "$CTS/build-cts" -G Ninja -DDEQP_TARGET=mobilegl-desktop \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache
ninja -C "$CTS/build-cts" glcts
```
Confirm the configure output says `*** Using MobileGL desktop target`. Budget a
couple of hours for the `glcts` link on a small machine; it is a one-time cost
that ccache makes cheap afterwards.
If `fetch_sources.py` dies with `HTTP Error 403` it is an egress policy blocking
the GitHub archive downloads (zlib, libpng), not a broken checkout: `git clone`
still works, so clone the package at the tag the script pins into
`external/<pkg>/src` by hand — for libpng also copy
`scripts/pnglibconf.h.prebuilt` to `src/pnglibconf.h`, which is what the
script's post-extract step does.
## Step 3 — run, once per backend
```sh
cd "$MG"
for BACKEND in DirectGLES DirectVulkan; do
python3 tools/cts/scripts/run_cts_local.py --backend "$BACKEND" \
--glcts "$CTS/build-cts/external/openglcts/modules/glcts" \
--lib build-linux/libMobileGL.so \
--caselist cases.txt --outdir "runs/${BACKEND}" \
--env EGL_PLATFORM=surfaceless
python3 tools/cts/scripts/qpa_report.py "runs/${BACKEND}" --label "$BACKEND"
done
```
`cases.txt` is any subset of a mustpass list. For one group, filter the list
rather than running the whole suite:
```sh
grep direct_state_access \
"$CTS"/external/openglcts/data/gl_cts/data/mustpass/gl/khronos_mustpass/main/gl45-main.txt \
> cases.txt
```
`run_cts_local.py` re-invokes `glcts` with only the cases that have no result
yet, so a crash costs one case rather than the run, and it records the case that
was open when the process died as `Crash`. `qpa_report.py` scores `Pass`,
`NotSupported` and the warning statuses as non-failures, the way Khronos scores
a submission, and reports the strict `Pass`-only rate alongside.
## Required flags, and why
| Flag | Why it is not optional |
| --- | --- |
| `--env EGL_PLATFORM=surfaceless` | For DirectGLES. With no `/dev/dri` node Mesa's EGL fails `eglInitialize` on the default display, and MobileGL surfaces that as `EGL_BAD_ALLOC` out of `eglCreatePbufferSurface` — which points at the wrong call entirely. Harmless for DirectVulkan, so pass it to both. |
| `--deqp-surface-type=fbo` (the runner's default) | DirectVulkan reads back zeros from the **default** framebuffer. dEQP verifies nearly everything through `glReadPixels`, so rendering to the surface scores Magma near zero for a reason unrelated to conformance. Use it for both backends so the two numbers stay comparable. |
| `--deqp-terminate-on-device-lost=disable` (supplied by the runner) | Its default calls `glGetGraphicsResetStatus()` after every case. That is GL 4.5 / `KHR_robustness`, absent from what MobileGL exports, so the pointer is null and the process segfaults on the first case. |
## What this environment does and does not tell you
Reproducible here, and MobileGL's own rather than a driver quirk:
- DirectVulkan's default-framebuffer readback returns zeros; a user FBO,
renderbuffer- or texture-attached, is correct on both backends. This is the
same defect the Android runs work around, so it can be debugged without a
phone.
Different from a real GPU, so do not read conformance into it:
- lavapipe supports renderbuffer formats Adreno reports as unsupported, so the
Android runs see `NotSupported` where these do not, and vice versa.
- Backend limits differ. `GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT` is 16 on llvmpipe
and on lavapipe; a device that reports 1 will not exercise the same paths.
- Everything is a software rasterizer, so a case that fails only under real
timing or real tiling will not fail here.
## Reference results: KHR-GL45.direct_state_access
`opengl-cts-4.6.8.1`, the 371 `direct_state_access` cases of the `gl45-main`
mustpass list, Mesa 25.2.8, `--deqp-surface-type=fbo`.
| backend | renderer | conformance | strict Pass | Fail | InternalError | Crash |
| --- | --- | ---: | ---: | ---: | ---: | ---: |
| DirectGLES | Espryt | **74.93%** | 67.39% | 85 | 8 | 0 |
| DirectVulkan | Magma | **73.32%** | 73.05% | 88 | 8 | 3 |
## Contents
platform/tcuMobileGLPlatform.{cpp,hpp} dEQP tcu::Platform for MobileGL
targets/mobilegl-desktop.cmake VK-GL-CTS target (-DDEQP_TARGET=mobilegl-desktop)
scripts/sync_to_cts.py inject the port into a CTS checkout
scripts/run_cts_local.py crash-resuming local-host runner
scripts/qpa_report.py .qpa -> conformance rate
@@ -0,0 +1,4 @@
interface:
display_name: "OpenGL CTS on MobileGL (desktop Linux)"
short_description: "Run VK-GL-CTS glcts against MobileGL headlessly and report Espryt and Magma separately"
default_prompt: "Use $linux-gl-cts-on-mobilegl to run the selected OpenGL CTS group against MobileGL on this Linux host and report the conformance rate for DirectGLES (Espryt) and DirectVulkan (Magma) separately."