Compare commits

...
Author SHA1 Message Date
BZLZHH 588ddba722 [Fix] (MG_Backend): scale a depth blit, keep going after one declines, and mip a 1D texture
Three DirectVulkan gaps found together.

glBlitFramebuffer's depth/stencil path refused any blit whose source and
destination extents differ, because vkCmdCopyImage cannot resize. vkCmdBlitImage
can, and VK_FILTER_NEAREST is the only filter Vulkan allows for depth/stencil
anyway - which is what the GL front end already requires. A same-size pair keeps
the cheaper copy.

Worse, that refusal and four others were `return`, not `continue`, so a
depth/stencil aspect this backend could not handle abandoned the whole function -
including the colour blit that only starts after the aspect loop. The CTS's
scaling blits therefore lost their colour as well, which is why
direct_state_access.framebuffers_blit failed all three of its checks rather than
one.

VulkanRenderer::GenerateMipmap declined GL_TEXTURE_1D. It needed nothing else:
the blit loop derives every offset from the storage extent, and a 1D texture's is
{width, 1, 1}, which is exactly the y and z offsets a 1D image requires.

Also: IsTimerQueryResultReady now asks the query pool before the frame serial.
The pool polls with VK_QUERY_RESULT_WITH_AVAILABILITY_BIT and is the authority;
the frame serial only advances at Present and neither completion notifier will
mark the current serial done, so a timestamp written and fence-waited inside one
GL frame could never be read back within it.

Takes framebuffers_blit and textures_generate_mipmaps from failing to passing on
DirectVulkan. queries_functional still fails there on a value.
2026-08-05 06:50:13 -04:00
BZLZHH 62301b1061 [Fix] (MG_State): let a double-typed varying be captured by transform feedback
ResolveXfbSymbolType accepted only float, int and uint, and its caller reports
anything it rejects as "Transform feedback varying 'x' is not an output of the
vertex stage" - which is a misleading thing to say about a varying that is right
there in the shader, just declared `double`. Program linkage failed outright.

Doubles are now resolved to the GL_DOUBLE* types, in vector and matrix form, and
the per-element size is computed from an 8-byte component rather than a hardcoded
4 (GL 4.6 core 11.1.2.1), so the byte-based limit checks charge a double what GL
says it costs.

direct_state_access.vertex_arrays_attribute_format stops throwing on both
backends and fails on the captured values instead: the capture layout still owes
the 8-byte alignment doubles require, and neither backend feeds a 64-bit vertex
attribute yet - DirectGLES cannot at all, ESSL having no double.
2026-08-05 06:43:41 -04:00
BZLZHH f3a846d336 [Docs] (README): carry the 4.2 short-term target into the status note
The compatibility section already said 4.2; the status note at the top of the
README still said 3.3, so the two disagreed depending on how far a reader got.
2026-08-05 06:10:37 -04:00
BZLZHH 9cdc82fbdd [Fix] (MG_Backend): actually bind the sampler object DirectGLES just synced
BindCurrentTextures' program-driven path synced a bound sampler object's
parameters to its backend object and then never put it on the texture unit, so
every sampler object was inert and the driver kept sampling with the texture's
own parameters - direct_state_access.samplers_functional read black where the
sampler's NEAREST filtering should have given red.

The bind alone is a regression, and the CTS says so loudly: a sampler left on a
unit by an earlier draw keeps being applied, and a multisample texture takes no
sampler object at all, so the next draw against one is rejected and all 27
textures_storage_multisample_3d_* cases fail. The sibling path in the same
function had an empty else branch where the unbind belonged; it now unbinds,
making the two symmetric.

Takes samplers_functional from failing to passing on Espryt, with no other case
moving in either direction.
2026-08-05 06:08:59 -04:00
BZLZHH 4a9d20c49f [Fix] (MG_Backend): resolve a framebuffer attachment's layer in the Vulkan blit bindings
ResolveAttachmentBaseArrayLayer answered zero for everything but a cube map face,
so every blit, copy and glReadPixels against a layered attachment read layer zero
whatever was attached. It reads the attachment's layer now.

A 3D texture needs the other half of the distinction: its image has arrayLayers
== 1 and the GL layer is a z slice, which VkBufferImageCopy will not take as a
base array layer. BlitImageBinding carries it separately as depthOffset, and the
readback copy region uses it as the image offset's z.

Takes textures_copy from failing to passing on DirectVulkan, which is what
glCopyTextureSubImage3D needs to see the slice the CTS attached rather than
slice zero.
2026-08-05 05:56:09 -04:00
BZLZHH 394d1ce748 [Feat] (MG_Impl, MG_Util): copy into 1D and 3D textures, and accept the BPTC and ETC2 enums
Two unrelated texture gaps.

glCopyTextureSubImage1D and 3D validated their arguments and then did nothing:
CopyTexSubImage1D_State and CopyTexSubImage3D_State were empty TODOs and no
backend exposes anything but a 2D blit. But a texture's contents live in its CPU
storage - the backends sync from it - so the copy does not need a blit at all.
CopyReadFramebufferIntoMipmapRegion reads the region out of the read framebuffer
through the existing ReadPixels path, in the destination's own canonical client
layout so the bytes need no second conversion, and writes them straight into the
level. GL 4.6 core 8.6 says the copy ignores pixel-store state and any bound pack
buffer, which the borrowed readback does not, so both are neutralised for the
duration and restored after. A cube map destination addresses its faces as
separate upload targets, so its zoffset picks the target rather than a slice.

ConvertGLEnumToTextureInternalFormat had arms for the six generic compressed
formats and the four RGTC ones, all resolving to uncompressed storage, but none
for BPTC or ETC2/EAC - so glTexImage2D with one of those fourteen enums answered
INVALID_ENUM, which was never a legal reply for formats core GL has required
since 4.2 and 4.3. They follow the same deviation for the same reason: nothing in
this stack can compress them, and uncompressed storage is the trade the RGTC
formats already take.

Takes textures_compressed_subimage from failing to passing on both backends and
textures_copy on Espryt. textures_copy still fails on Magma, where the readback
of a layered attachment does not yet resolve the attached layer.
2026-08-05 05:50:32 -04:00
BZLZHH 300b458132 [Feat] (MG_State, MG_Impl): give program pipelines their object and their state
Every program pipeline entry point was an export stub, and the stub macro's
`return (type)1` made glIsProgramPipeline answer GL_TRUE for anything - including
the names glGenProgramPipelines had never written. All four
direct_state_access.program_pipelines cases failed.

ProgramPipelineObject holds what GL 4.6 core 7.4 says a pipeline is: a program
reference per shader stage, the active program glProgramUniform* addresses, a
validate status and an info log. Its validate status starts false, unlike
ProgramObject's, because a pipeline that has never been validated must report
GL_VALIDATE_STATUS as 0.

The name rules follow the shape queries and transform feedbacks already use, and
which the CTS checks first: glGenProgramPipelines only RESERVES a name and
glIsProgramPipeline answers GL_FALSE for it; the object appears on first bind, or
immediately from glCreateProgramPipelines. Map membership is object existence -
a pipeline, unlike a transform feedback, has no stateful default object zero, so
no everBound flag is needed.

glGet(GL_PROGRAM_PIPELINE_BINDING) reports the real binding now instead of a
hardcoded zero whose comment said the entry points were stubbed.

This is the state half only. program_pipelines_functional needs mixed-stage
rendering - a vertex-only and a fragment-only program drawn together - and stays
failing; glCreateShaderProgramv is deliberately left stubbed until that lands, so
nothing can half-work in between.

Takes program_pipelines_creation, _defaults and _errors from failing to passing
on both backends.
2026-08-05 05:43:13 -04:00
BZLZHH 1f1a331a44 [Feat] (MG_Impl, MG_State): implement the framebuffer parameter getters and setters
glFramebufferParameteri, glGetFramebufferParameteriv and their two by-name
siblings were all export stubs - the GL_ARB_framebuffer_no_attachments entry
points. The stub raises no error and writes nothing, so
direct_state_access.framebuffers_get_parameter_errors saw GL_NO_ERROR for all
three conditions it checks.

FramebufferObject gains the five DEFAULT_* parameters as real state, initialised
to GL 4.6 core table 23.24 and bumping the object version on a write like the
read buffer does. The getter answers those plus the six derived names -
GL_SAMPLES and GL_SAMPLE_BUFFERS from the attachments' sample counts,
GL_IMPLEMENTATION_COLOR_READ_FORMAT/_TYPE from the read buffer's internal format,
GL_DOUBLEBUFFER true only for the window-system framebuffer, GL_STEREO false
because stereo surfaces are not exposed - which is what glGetIntegerv already
reports for the bound framebuffer.

The pname rules live in ValidateFramebufferParameterPname, and their ORDER is
load-bearing: a name outside the table is INVALID_ENUM, and only a name that IS
in the table but that the default framebuffer cannot answer is INVALID_OPERATION.
Testing the framebuffer kind first would answer INVALID_ENUM for
GL_FRAMEBUFFER_DEFAULT_WIDTH on framebuffer zero, which is exactly the third
thing the case checks. The by-name forms take zero as the default framebuffer,
like the other DSA framebuffer entry points.

Rendering to a framebuffer with no attachments is deliberately NOT enabled by
this: CheckCompleteness still reports INCOMPLETE_MISSING_ATTACHMENT, because no
backend can rasterize one. The state is real and the queries are honest; the
draw path is a separate piece of work.

Takes framebuffers_get_parameter_errors from failing to passing on both backends,
with framebuffers_get_parameters - which passed only because both getters were
stubs leaving the CTS's zero-initialised comparands untouched - still passing.
2026-08-05 05:33:31 -04:00
BZLZHH 817091641c [Fix] (MG_Impl): give a cube map the storage and the layered attachment it asks for
direct_state_access.framebuffers_texture_attachment threw on both backends, and
three separate things were wrong on the way to a cube map framebuffer.

glTexStorage1D/2D/3D validated their target by converting it to a single
TextureUploadTarget. GL_TEXTURE_CUBE_MAP has no single upload target - it
allocates all six faces - so the conversion produced Unknown and a legal
glTexStorage2D(GL_TEXTURE_CUBE_MAP, ...) was rejected with INVALID_ENUM, which is
where the case threw. The accepted set for these entry points is the dimension's
storage targets, which IsTextureStorageTargetForDimension already spells out, so
that is what they check now.

TextureStorage2D then allocated only the primary upload target, leaving a cube
map with one face out of six - cube-incomplete, so every framebuffer it was
attached to answered GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT. It allocates every
upload target the object has; for every other 2D target that is the same single
target as before.

ResolveRepresentableFramebufferTextureUploadTarget declined every layered target
but 2D array, so glNamedFramebufferTexture on a cube map reported "not
represented by the current framebuffer attachment model". Cube maps, cube map
arrays, 1D arrays, 2D multisample arrays and 3D textures are all the same shape
as the 2D array that already worked - glFramebufferTexture binds the whole
texture and the attachment records a representative upload target - so they are
all handled now. DirectGLES routes a layered attachment to glFramebufferTexture,
which is exactly this.

Takes framebuffers_texture_attachment from failing to passing on both backends.
2026-08-05 05:24:21 -04:00
BZLZHH e64c7c7e65 [Fix] (MG_Backend): never back a multisample texture with a one-sample Vulkan image
Every one of the sixty direct_state_access.textures_storage_multisample_2d_* and
_3d_* cases failed on DirectVulkan, for every internal format, with no GL error
anywhere - a pure data mismatch.

The CTS asks for glTextureStorage2DMultisample(tex, samples = 1, ...), which is
legal GL, and MobileGL carried the 1 faithfully through to
VkImageCreateInfo::samples = VK_SAMPLE_COUNT_1_BIT. It then binds that image to
the auxiliary program's sampler2DMS, whose SPIR-V is OpTypeImage with MS = 1.
VUID-RuntimeSpirv-samples-08726 forbids exactly that pairing: an MS access must
come from an image created with more than one sample. The texelFetch therefore
read undefined data - which is why it looked format-independent and raised
nothing.

GL only promises "at least the requested number of samples", so a multisample
texture is now floored at two. GL_TEXTURE_SAMPLES still reports what the
application asked for; that is read off the texture object, not off the image.
The device-capability round below it is bounded at two for the same reason -
letting it land back on one sample would recreate the violation silently for any
format whose only supported count is one.

Takes all 60 textures_storage_multisample_* cases from failing to passing on
DirectVulkan, which goes from 296/371 to 356/371. DirectGLES is untouched.
2026-08-05 04:46:15 -04:00
BZLZHH dd60ff39ce [Feat] (MG_State, MG_Impl, MG_Backend, MG_Util): make the border colour real sampler state
glGetSamplerParameterfv(sampler, GL_TEXTURE_BORDER_COLOR) raised INVALID_ENUM,
because MobileGL kept the border colour on the texture object and
GetSamplerParam_State had no case for it at all. That is the first thing
direct_state_access.samplers_defaults asks, so the case threw before reaching
any of the defaults it was written to check.

GL 4.6 core table 23.18 lists TEXTURE_BORDER_COLOR as sampler state, so it moves
to SamplerParameters and TextureObjectBase reaches it through the SamplerObject
it already owns - one source of truth, and a sampler object bound over a texture
now supplies its own border colour, which is what GL says should happen. The
texture params version still moves on a write, because the DirectGLES texture
sync memoises on it. glSamplerParameter{fv,Iiv,Iuiv} and their getters read and
write all four components in whichever representation the caller used, and the
three representations are kept in step so any getter has an answer. The bogus
[0,1] and [0,255] range checks are gone: GL clamps a border colour when a
fixed-point format is sampled, it does not reject it.

DirectVulkan's ResolveVkBorderColor now reads the sampler rather than the
texture. DirectGLES gained a glSamplerParameterfv in its sampler sync, and both
that and the pre-existing glTexParameterfv are gated on a new
SupportsTextureBorderClamp capability - ES 3.2 core, or EXT/OES_texture_border_clamp
before it - since without the extension every such call is INVALID_ENUM on the
driver. DriverPost gains the matching row per the POST rule, saying what a user
actually loses when it is missing.

Takes direct_state_access.samplers_defaults from failing to passing on both
backends.
2026-08-05 04:45:55 -04:00
BZLZHH 96ad7ca0cc [Fix] (MG_Impl): asking a renderbuffer for more samples than it has is INVALID_OPERATION
ValidateRenderbufferStorageSamples_State answered INVALID_VALUE for a sample
count above GL_MAX_SAMPLES. GL 4.6 core 9.2.4 reserves INVALID_VALUE for a
negative count: a count that is well formed but larger than the format can
deliver is INVALID_OPERATION, because the argument is fine and the format is
what cannot honour it.

Takes direct_state_access.renderbuffers_storage_multisample_errors from failing
to passing on both backends.
2026-08-05 04:13:59 -04:00
BZLZHH e80a23eae6 [Fix] (MG_Backend): read a multi-slice glGetTexImage off the GPU instead of the CPU shadow
DirectGLES served every multi-slice glGetTexImage from the CPU shadow copy, on
the grounds that its scratch FBO can only expose one layer at a time. But the
shadow only holds what was uploaded, so any slice that was rendered to rather
than written by glTexSubImage came back stale - and a layered framebuffer
produces exactly that.

The scratch FBO can expose one layer at a time repeatedly. The read now attaches
each layer in turn and takes the slice off the GPU, walking the destination over
GL_PACK_SKIP_IMAGES / GL_PACK_IMAGE_HEIGHT itself so each per-slice call packs a
plain 2D image with the same layout StoreWideRowsToClient computes for the whole
stack. The shadow stays as the fallback for the formats a colour attachment
cannot represent at all, and for any slice whose attachment comes back
incomplete.

Takes all 27 remaining direct_state_access.textures_storage_multisample_3d_*
cases from failing to passing on Espryt - they render into a
TEXTURE_2D_MULTISAMPLE_ARRAY one layer per colour attachment and then read the
whole array back. DirectVulkan is untouched.
2026-08-05 03:55:30 -04:00
BZLZHH 088f263495 [Feat] (MG_Impl): answer the two query parameters the getters were missing
GetQueryObjectValue implemented GL_QUERY_RESULT_AVAILABLE and GL_QUERY_RESULT and
rejected everything else, so direct_state_access.queries_functional threw on its
very first probe - GL_QUERY_TARGET - and never reached any of the checks it was
written for.

GL_QUERY_TARGET is state the object has carried all along; it just had no case.
GL_QUERY_RESULT_NO_WAIT is GL_QUERY_RESULT with the backend asked not to block,
and it brings a wrinkle the shared getter could not express: when the result has
not landed, GL_ARB_query_buffer_object leaves the destination untouched rather
than writing a placeholder. GetQueryObjectValue now reports "succeeded but
produced no value" through an optional out-parameter, and all five callers - the
four buffer forms and the four client-memory forms - skip the write on it.

The switch is deliberately widened by exactly these two names: its default
INVALID_ENUM is what the GL33 and GL40 query error cases rely on.

queries_functional passes on Espryt. On Magma it stops throwing and fails on a
value instead, which is a separate problem in the query results themselves.
2026-08-05 03:48:14 -04:00
BZLZHH 3b3b6e5b8b [Fix] (MG_Backend): read back the stencil half, and clear an sRGB target to the value asked for
Two reasons a framebuffer's contents came back wrong, both on the read/clear
side rather than the write side.

Stencil, on both backends. The CTS reads stencil with glReadPixels(GL_STENCIL_INDEX,
GL_INT), which is as legal as the unsigned widths, and neither backend accepted
it: DirectGLES's ReadPixelsStencilViaNative rejected every signed type, after
which the call fell through to a native ES read the driver refuses and nothing
was written at all, so the caller kept its zeros; DirectVulkan's pack switch had
no GL_INT case, and of the cases it did have only GL_UNSIGNED_INT sourced the
stencil plane - GL_FLOAT and GL_UNSIGNED_SHORT emitted a depth value, which is
meaningless for a stencil-only image. Both now take the signed and float widths,
and DirectVulkan decides "this is a stencil read" once rather than per type.
DirectGLES also gains the GL_FLOAT_32_UNSIGNED_INT_24_8_REV fallback a
DEPTH32F_STENCIL8 attachment needs, which rejects the 24_8 packed type.

sRGB, on DirectVulkan. Every other write path goes through the UNORM twin view
while GL_FRAMEBUFFER_SRGB is off, storing the raw value GL asked for, but a
deferred clear is materialised with vkCmdClearColorImage - which names the image,
so the driver applied the sRGB transfer function and a clear to 0.25 landed at
0.537. PreCompensateSrgbClearColor hands it the linear colour whose encoding is
the requested value instead. It is a no-op for non-sRGB destinations, for integer
clear encodings, and when GL_FRAMEBUFFER_SRGB is on and GL really does want the
encode.

Takes renderbuffers_storage from failing to passing on both backends, plus
renderbuffers_storage_multisample and framebuffers_blit on Espryt.
2026-08-05 03:41:23 -04:00
BZLZHH 9eda2147b1 [Fix] (MG_Impl, MG_Backend): let the backend that can honour a layered attachment have it
NamedFramebufferTextureLayer declined every attachment but layer zero, on both
backends. That was right for DirectVulkan, which maps a GL layer onto a Vulkan
array layer with no notion of a 3D depth slice, but wrong for DirectGLES:
SyncAttachmentObject already routes a layered upload target to
glFramebufferTextureLayer with the attachment's layer passed straight through,
and array storage already carries the real layer count into glTexStorage3D. The
one backend that could render to the layer was being told it could not.

The decision now lives in a DynamicBackendParameters flag, so it is the backend
that answers rather than the entry point guessing. DirectGLES sets it when the
driver resolved glFramebufferTextureLayer; DirectVulkan leaves it false until
VkRenderPassManager tells a depth slice from an array layer.

framebuffers_texture_layer_attachment's colour checks now pass on Espryt for 3D,
2D array and 2D multisample array textures - the case still fails there on cube
map arrays, which DirectGLES gives no storage at all, and on the depth and
stencil halves. No case changes on DirectVulkan, which keeps the old behaviour.
2026-08-05 03:40:59 -04:00
BZLZHH a63699cde6 [Fix] (MG_Impl, MG_Backend): reject incomplete cube maps in mipmap generation instead of crashing on them
Both direct_state_access.textures_generate_mipmap* cases crashed DirectVulkan.
Two causes, neither of them a broken invariant:

glGenerateMipmap and glGenerateTextureMipmap never checked cube completeness, so
an incomplete cube map went straight to the backend, which asserts that the
texture it is handed is complete. GL 4.6 core 8.14.4 makes that call
INVALID_OPERATION - there is no consistent set of faces to filter down - and both
entry points now say so through a shared check.

VulkanRenderer::GenerateMipmap asserted that the target was one of the four it
implements. 1D, 1D array and cube map array are legal GL and the front end passes
them through, so meeting one is a gap in this backend's coverage; it now logs and
declines, leaving the generated levels unwritten rather than aborting.

textures_generate_mipmap_errors passes on both backends now. textures_generate_mipmaps
stops crashing but still fails: DirectVulkan does not generate the 1D mip chain
the case checks - the frontend's storage allocation gives the levels the right
sizes, which is why the case passes when run on its own, but not the descending
content the full-run state leaves it looking for.
2026-08-05 02:55:54 -04:00
BZLZHH 765aaec6dc [Fix] (MG_Impl, MG_Backend): stop the new layer attachment from reaching backends that cannot back it
Implementing NamedFramebufferTextureLayer made layered attachments reachable for
the first time, and direct_state_access.framebuffers_texture_layer_attachment
went from Fail to Crash on DirectVulkan. Two separate gaps sat behind it, both
of them asserted on rather than reported:

- The renderer resolves an attachment's GL layer straight onto a Vulkan array
  layer. A 3D texture's z-slice therefore lands outside its image, which has one
  array layer by construction, and the array texture objects are still the
  one-image stubs in TextureObjectStubs.h, so their image has a single layer
  whatever GL believes. MaterializePendingClearForTexture tripped over a clear
  whose layer span was outside the image it was given.
- A cube map array has no image shape in VkTextureManager at all, so
  SyncTextureAndGetDescriptor returns null for it.

NamedFramebufferTextureLayer now answers the full error set for every target and
layer - which is what took the two error cases green - and then declines to
attach anything but layer zero of a non-cube-array texture, through the same
RecordUnsupportedFramebufferTextureAttachmentError the by-target entry point
already uses. Layer zero of the other targets is the plain first-slice
attachment glFramebufferTextureLayer already backs, so it still goes through.

SyncTextureResource's assertion on an unsupported texture shape is also gone: it
is a gap in this backend's coverage, not a broken invariant, and the code below
it already handles the failure by declining the sync. It logs a warning instead.

framebuffers_texture_layer_attachment goes back to Fail on DirectVulkan rather
than Crash; no case changes in either direction beyond that.
2026-08-05 02:47:27 -04:00
BZLZHH bcd669bd25 [Feat] (MG_Impl): complete the by-name framebuffer attachment and buffer-selection entry points
Four direct_state_access framebuffer cases failed on one shared cause and three
local ones.

The shared cause: every DSA framebuffer entry point resolved its name through
GetNamedFramebufferObject_State, which rejects zero outright. But zero names the
default framebuffer to these functions, so glGetNamedFramebufferAttachmentParameteriv,
glNamedFramebufferDrawBuffer(s) and glNamedFramebufferReadBuffer answered
INVALID_VALUE for every default-framebuffer query the CTS makes. They now resolve
zero to the default framebuffer object and tell the two kinds apart explicitly,
which is what the accepted-name rules key off anyway.

Attachment queries: the accepted attachment names differ between the default
framebuffer (FRONT/BACK variants, DEPTH, STENCIL) and a framebuffer object
(COLOR_ATTACHMENTi, DEPTH/STENCIL/DEPTH_STENCIL_ATTACHMENT), and a name outside
the relevant list is INVALID_ENUM. Both getters share ResolveAttachmentQueryName
for that, so the by-target form no longer aliases GL_FRONT onto a framebuffer
object's colour attachment 0. The TEXTURE_* parameters are also rejected with
INVALID_ENUM when the attached object is a renderbuffer.

Buffer selection: naming a buffer that belongs to the other kind of framebuffer
is INVALID_OPERATION, not INVALID_ENUM - the enum is accepted, the framebuffer
just has no such buffer. glDrawBuffers additionally rejects the multi-buffer
names (FRONT, LEFT, RIGHT, FRONT_AND_BACK) with INVALID_ENUM on both kinds,
takes BACK only when n is one, and glReadBuffer treats the multi-buffer names as
accepted-but-unselectable. Both colour-attachment range checks now go through
ValidateColorAttachmentInRange instead of comparing against MAX_DRAW_BUFFERS with
an off-by-one.

NamedFramebufferTextureLayer was a stub that reported "not represented by the
current framebuffer attachment model" for every call, even though the attachment
model stores a layer and the by-target glFramebufferTextureLayer already uses it.
It is implemented against the same model, with the per-target layer limits and
the INVALID_OPERATION-for-a-bad-name rule that separates it from
NamedFramebufferTexture. NamedFramebufferTexture itself gained the two checks it
lacked: colour attachment range, and a negative level.

Takes framebuffers_get_attachment_parameters, framebuffers_get_attachment_parameter_errors,
framebuffers_texture_attachment_errors and framebuffers_draw_read_buffers_errors
from failing to passing on both backends.
2026-08-05 02:34:12 -04:00
Claude f3405d1d53 [Fix] (CI): narrow the trace fixture Git LFS fallback to the files mirrors lost
The fetch script tries git.hit.moe, then the repo.miawa.cn mirror, and only
then Git LFS, but it bailed out of the mirror loop on the first file no
mirror could serve and then pulled the whole case from GitHub. A case whose
mirrors served every file but one paid GitHub's LFS bandwidth for all of
them.

Collect the files that survived every mirror and every retry instead, and
scope the LFS fallback to just those, matching what the local macOS retrace
helper already does.
2026-08-05 05:59:25 +00:00
BZLZHH 81604d5596 [Feat] (MG_Impl, MG_Test): validate the direct-state-access texture copies
CopyTextureSubImage1D and 3D were do-nothing stubs and the 2D form checked only
its effective target, so all 28 conditions in
direct_state_access.textures_copy_errors went unreported: level and region
bounds, and every read-framebuffer precondition.

The read-framebuffer half lands in FramebufferImpl as ValidateReadFramebufferForCopy -
incomplete read framebuffer (INVALID_FRAMEBUFFER_OPERATION), a read buffer that
names no attachment, and a multisampled read buffer (both INVALID_OPERATION). It
decides multisampledness by attachment kind rather than by sample count alone,
because a TEXTURE_2D_MULTISAMPLE attachment sets SAMPLE_BUFFERS even when its
sample count is one - which is exactly what the CTS attaches, and what a
renderbuffer-only check would have missed.

The texture half is ValidateCopyTextureSubImage, shared by all three forms; 1D
and 3D also get the effective-target rule their form specifies.

NOTE: the copy itself is still not implemented for 1D and 3D - CopyTexSubImage1D_State
and CopyTexSubImage3D_State remain TODOs and no backend exposes anything but a
2D blit - so direct_state_access.textures_copy stays red. Only the errors are
complete, which is what un-stubbing these two entry points buys; both carry a
comment saying so.

CopyTextureSubImage2DUsesNamedObjectAndRestoresBinding had been passing a
storage-less texture and no read framebuffer, which the new validation correctly
rejects. It now sets up a legal copy, so it still measures the by-name plumbing
it was written for.

Takes direct_state_access.textures_copy_errors from failing to passing on both
backends.
2026-08-05 01:54:39 -04:00
BZLZHH 31ea6aa5a3 [Feat] (MG_Impl): give the by-name texture image queries their error set
glGetTextureImage resolved a texture by name and went straight to the read,
skipping every object-level rule glGetTexImage enforces through
GetTexImage_State - and on DirectVulkan it skipped the level checks in
CopyTextureImageToClientOrPBO_State as well, because that backend answers
GetTextureImage itself. Fifteen of the sixteen conditions in
direct_state_access.textures_image_query_errors went unreported.

The object-level half of that error set now lives in ValidateTextureImageQuery
and both entry points run it. Three rules are new rather than merely relocated:

- Multisample and buffer textures are not in the accepted target list; neither
  has a single image to return.
- The destination-size checks (bufSize, and the span written into a bound pixel
  pack buffer) move ahead of the read. They existed, but downstream of it, where
  any early bail-out - an unmapped level, a pack step that declines the format -
  swallowed them. Both measure the tightly packed span summed over the object's
  faces, which is the least a query can produce, so nothing that would have fit
  is rejected.
- IsDepthLikeInternalFormat had no case for StencilIndex8, so a colour client
  format read back against a stencil-only texture looked like a matching pair.

glGetCompressedTextureImage was a do-nothing stub. It validates the name and the
level, then reports INVALID_OPERATION: no format MobileGL can hold is
compressed, and answering GL_NO_ERROR without writing would hand the caller
stale memory - the same reasoning GetCompressedTexImage_State already follows.

Takes direct_state_access.textures_image_query_errors from failing to passing on
both backends.
2026-08-05 01:46:10 -04:00
BZLZHH 7d6f6603c1 [Feat] (MG_Impl): enforce the unpack-buffer rules on texture sub-image uploads
TexSubImage1D/2D/3D_State each carried a TODO for the three INVALID_OPERATION
conditions GL 4.6 core 8.5 attaches to sourcing an upload from a bound
PIXEL_UNPACK_BUFFER: the store being mapped, an offset that is not a multiple of
the size of one datum of `type`, and reads that would run past the end of the
store. None of them was checked, so every such call was quietly accepted.

ValidatePixelUnpackBufferSource now covers all three and returns true when no
unpack buffer is bound, so the callers can run it unconditionally. Persistent
mappings stay legal sources, matching what ReadPixels already does on the pack
side. The overrun check measures the tightly packed span, which is the smallest
the unpack can read - pixel store parameters only ever widen it - so it cannot
reject an upload that would have fit.

TextureSubImage2D needed the call of its own: unlike its 1D and 3D siblings it
does not route through TexSubImage2D_State.

Takes direct_state_access.textures_subimage_errors from failing to passing on
both backends.
2026-08-05 01:32:37 -04:00
BZLZHH 39c17c0b1b [Fix] (MG_Impl): validate the float texture parameter setter and the compressed size query
Two independent gaps in the texture parameter paths, both reported by
direct_state_access:

TexParameterf_State never ran ValidateTextureParameterForTarget. The integer
setter reaches it through TextureParameterObject_State and the scalar float
setter through TextureParameterObjectf_State, but glTexParameterfv and
glTextureParameterfv funnel every non-vector pname straight into
TexParameterf_State - so in float form MobileGL accepted sampler state on a
multisample texture, a mipmapping min filter or a REPEAT wrap on a rectangle
texture, and a negative TEXTURE_BASE_LEVEL/TEXTURE_MAX_LEVEL, all of which the
integer form rejected. It now validates first, passing the same
anisotropy-exempt param the by-object float setter uses so the anisotropy range
check is not run twice.

GL_TEXTURE_COMPRESSED_IMAGE_SIZE answered 0 for every texture. GL 4.6 core 8.11
makes the query INVALID_OPERATION on an image whose internal format is
uncompressed and on any proxy target. TextureInternalFormat has no compressed
enumerator, so that is every texture MobileGL can hold today; the condition is
still written against an IsCompressedTextureFormat predicate so both level
getters answer consistently once compressed formats land, and
GL_TEXTURE_COMPRESSED now reads from the same predicate instead of a hardcoded
false.

Takes textures_parameter_setup_errors and textures_level_parameter_errors from
failing to passing on both backends.
2026-08-05 01:24:29 -04:00
BZLZHH 88138b48ec [Test] (MG_Test): follow the backends to an advertised GL 4.0
Both AdvertisesVoxyRequiredRenderingExtensions cases pinned TargetGLVersion at
3.3, which was the reported version until V_OpenGL40 joined the advertised
extension lists. The version assertion is incidental to what these cases are
for - Voxy needs the individual ARB extensions, not a version - so it just
tracks the new report instead of holding the old one.
2026-08-05 01:24:05 -04:00
BZLZHH c114ce750b [Feat] (MG_Backend): advertise OpenGL 4.0 on both backends
Both backends stopped their advertised version list at V_OpenGL33, so an
application - or the CTS - asking what MobileGL supports was told 3.3 even
though the 4.0 entry points and the KHR-GL40 suite already pass on both.
Adding V_OpenGL40 lets that work be reached through the ordinary version query
instead of only through the individual ARB extension strings.
2026-08-05 13:19:41 +08:00
BZLZHH 1ca2d3c0fe [Docs] (README): move the short-term target to OpenGL 4.2
The 3.3 line is done - GL30 through GL33 conform on both backends - and the
work in flight (GL40, direct state access) is already past it, so the stated
short-term target now reads 4.2 and MG_State/MG_Impl are focused there.
Performance work joins the focus list alongside the two backends.
2026-08-05 01:15:26 -04:00
BZLZHH 58c17f85a5 [Fix] (MG_State, MG_Backend): start TEXTURE_COMPARE_FUNC at LEQUAL
SamplerParameters defaulted compareFunc to ALWAYS, but GL 4.6 core table 23.18
and GLES 3.2 table 21.16 both say the initial value is LEQUAL - for sampler
objects and for the sampler state a texture object carries alike. Every freshly
created texture and sampler therefore answered GL_ALWAYS to
glGetTextureParameteriv(GL_TEXTURE_COMPARE_FUNC).

The Vulkan backend had been papering over it: ResolveCompareFunc substituted
LESS_EQUAL whenever a depth texture was sampled in compare mode and the func
still read ALWAYS, which fixed the rendering but also made an explicitly
requested GL_ALWAYS unreachable. With the default corrected that special case is
both unnecessary and wrong, so it is gone and the compare op is taken straight
from the sampler.

Takes direct_state_access.textures_defaults from failing to passing on both
backends.
2026-08-05 01:13:04 -04:00
BZLZHH 4873da6844 [Fix] (MG_Impl): accept COLOR when invalidating the default framebuffer
The validation added with the invalidation entry points took the default framebuffer's
buffers to be only FRONT_LEFT, FRONT_RIGHT, BACK_LEFT, BACK_RIGHT, DEPTH and STENCIL, so a
call naming COLOR came back INVALID_ENUM. The by-name forms spell the colour buffer the way
glClearNamedFramebuffer does - COLOR, DEPTH, STENCIL - while the target forms use the
individual left/right tokens, and both spellings arrive at the same validation, so both sets
belong there (GL 4.6 core 17.4.4).

Caught by framebuffers_invalidate_data and framebuffers_invalidate_subdata, which had been
passing while the entry points were stubs doing nothing at all. Those two plus
invalidate_data_and_subdata_errors now pass together on both backends.
2026-08-05 00:51:12 -04:00
BZLZHH efeb24ff9b [Fix] (MG_Impl, MG_State): answer the texture parameters the getters were missing
glGetTexParameter and its by-name form rejected several parameters GL 4.6 core table 8.20
lists, with INVALID_ENUM as if the application had made them up. GL_DEPTH_STENCIL_TEXTURE_MODE
was the worst of them: the float setter accepted it, validated it and then threw the value
away, the integer setter did not accept it at all, and neither getter could report it - so
the mode could be set and never read back, and setting it through glTextureParameteri was an
error.

It is real state now, defaulting to DEPTH_COMPONENT, set by both setters and readable from
both getters. GL_TEXTURE_LOD_BIAS was in the same position: settable, not gettable.

The by-name getters reach the target-based ones through a temporary binding rather than the
per-object path, so both had to learn these; the per-object path gained the swizzle
components, the target, the image format compatibility type and the texture-view parameters
at the same time, since they were missing there for the same reason.

direct_state_access.textures_get_set_parameter passes on both backends, and textures_defaults
stops raising an internal error and reports an ordinary failure it can be diagnosed from.
2026-08-05 00:49:12 -04:00
BZLZHH 18c1a4d586 [Feat] (MG_Impl): implement the query getters that write into a buffer object
glGetQueryBufferObjectiv and its three siblings were stubs. They are the ordinary query
getters with the destination changed from client memory to a buffer object, so everything
about the query itself - the name, whether it is still active, the parameter - is already
answered by the shared GetQueryObjectValue, including the errors it raises.

What was left is the destination: a negative offset is INVALID_VALUE, a name that is not a
buffer object is INVALID_OPERATION, and so is a write that would run past the end of the
buffer. The four differ only in the width they store, so they share one template.

direct_state_access.queries_errors passes on both backends, putting the group at 4 of 5.
queries_functional now reaches further into the test and ends in an unrelated InternalError
rather than a plain failure.
2026-08-05 00:36:53 -04:00
BZLZHH 66ac3486e1 [Feat] (MG_Impl): validate the framebuffer invalidation entry points
glInvalidateFramebuffer, glInvalidateSubFramebuffer and their two by-name forms were all
stubs, so every call - including the malformed ones - returned quietly with no error.

These four only grant permission to throw the named attachments' contents away, and keeping
them satisfies "the contents become undefined", so the frontend validates the call and
leaves the contents alone. Actually discarding is a bandwidth optimisation that would need a
backend dependency; it can be added later without changing what any of these promise.

The validation is where the real content is. Which tokens name an attachment depends on
which framebuffer is affected: the default framebuffer has buffers (FRONT_LEFT and company)
and a framebuffer object has attachment points, so a token from the wrong set is
INVALID_ENUM. A COLOR_ATTACHMENTm past GL_MAX_COLOR_ATTACHMENTS is different in kind - a
well-formed enum naming a point that does not exist - and is INVALID_OPERATION, which the
existing colour-attachment range validator already expresses. Negative counts and negative
sub-region extents are INVALID_VALUE.

direct_state_access.invalidate_data_and_subdata_errors passes on both backends.
2026-08-05 00:31:46 -04:00
BZLZHH 764a44f589 [Fix] (MG_Impl): stop treating an empty buffer mapping access mask as a bad enum
glMapBufferRange and glMapNamedBufferRange rejected an access of zero with INVALID_ENUM.
Zero is a perfectly well-formed bitfield value - it contains no invalid flags - and what it
violates is the separate rule that a mapping has to ask for read or write access, which GL
reports as INVALID_OPERATION. Both callers already checked that rule immediately after, so
the validator was reporting the wrong error for a case its callers were about to handle
correctly.

direct_state_access.buffers_errors passes, which puts the whole buffers group at 4 of 4 on
both backends.
2026-08-05 00:13:51 -04:00
BZLZHH d96acb7972 [Fix] (MG_Impl): let a buffer clear name any format the spec allows
glClearBufferData and friends accepted exactly two argument triples - R8UI with
UNSIGNED_BYTE and R32UI with UNSIGNED_INT, both through RED_INTEGER - and raised
INVALID_ENUM for everything else. That is most of the entry point missing rather than a
narrow gap: GL takes any of the sized formats in the buffer-texture table, which is what an
application clearing an RGBA8 or R32F buffer uses.

The wrong error also hid the checks behind it. A test clearing a mapped buffer, or one
passing a misaligned offset, never reached those rules because the format tuple was rejected
first, so INVALID_ENUM came back where INVALID_OPERATION or INVALID_VALUE was due - the
validation was there and correct all along, just unreachable.

internalformat now goes through the same table the buffer textures use (shared rather than
written out twice, since it is the same list for the same reason), and format and type
through the ordinary pixel format converters. The element size comes from the internal
format, which is what offset and size have to be multiples of. Note that a bad format or
type here is INVALID_VALUE, not INVALID_ENUM (GL 4.6 core 6.3) - the odd one out among the
enum arguments, and what the conformance tests check for.

The pattern is still replicated verbatim, which is correct while the client layout matches
the internal format - every real caller, and every conformance case. When they differ it now
says so instead of quietly writing a differently-sized pattern.

direct_state_access.buffers_clear and buffers_functional pass on both backends;
buffers_errors is down to one unrelated complaint about glMapNamedBufferRange.
2026-08-05 00:09:11 -04:00
BZLZHH bd710078fc [Feat] (MG_Impl): implement glGetNamedBufferSubData
The by-name read was a stub, so it left the caller's buffer untouched and a test comparing
it against a reference saw whatever that memory already held. Its by-target sibling
glGetBufferSubData was already implemented, so this is that function with the buffer
resolved by name instead of through a binding: the same non-negative offset and size check,
the same bound-by-the-buffer's-size check, the same refusal to read a buffer mapped without
GL_MAP_PERSISTENT_BIT, and the same SyncGpuWrites before the download so a GPU-side write
that has not landed yet is not missed.

Resolving by name reports INVALID_OPERATION for a name that is not a buffer, which the
by-target form expresses as "target is bound to no buffer object" instead.

direct_state_access.buffers_get_named_buffer_subdata passes on both backends.
2026-08-05 00:01:20 -04:00
BZLZHH 95a7b17d45 [Fix] (DirectVulkan): clear an integer colour buffer with an integer value
glClearBufferiv and glClearBufferuiv flattened their values into the payload's float vector,
and every clear was later written into VkClearColorValue::float32. Vulkan reads that union
according to the destination image's format rather than converting between its members, so
an R8I attachment cleared to -16 received the bit pattern of -16.0f. On top of that,
QueueRenderbufferClear copied only the float vector into the pending clear, so even the
flattened value was dropped and the attachment kept reading zero - which is what the
conformance tests actually observed.

The payload now records which of the three entry points supplied the colour and keeps the
value in that form, and one helper builds the union member the encoding calls for. GL's rule
that a format with no alpha channel reads as one has to be applied in the value's own type,
so the "does this format lack alpha" question is now asked separately from the substitution
and the helper applies it to whichever member is live. glClear is left on the float path
explicitly: ClearFramebufferPayload has no other form.

Takes every integer renderbuffer format in direct_state_access.renderbuffers_storage from
failing to passing on Magma - 115 reported mismatches down to 20, the rest being the stencil
formats Espryt fails too and SRGB8_ALPHA8 - and makes framebuffers_clear pass on both
backends.
2026-08-04 23:55:43 -04:00
BZLZHH 19932f9e49 [Feat] (MG_Impl, MG_Backend): implement the integer direct state access framebuffer clears
glClearNamedFramebufferiv and glClearNamedFramebufferuiv were stubs, so a clear through
them was silently dropped and the attachment kept whatever it held. Their float siblings
were already implemented, which is what made the gap look like a rendering bug rather than
a missing entry point.

Which buffers they accept is narrower than glClearNamedFramebufferfv and differs between
the two: signed values clear COLOR or STENCIL, unsigned only COLOR (GL 4.6 core 17.4.3.1).
Only the colour buffer is indexed, so a stencil clear naming any drawbuffer other than 0 is
INVALID_VALUE rather than merely ignored, and anything else is INVALID_ENUM. Resolving the
framebuffer by name goes through the same helper the float forms use, which is what reports
INVALID_OPERATION for a name that is neither zero nor an existing framebuffer.

Both backends express them the way they already express the float forms: DirectGLES binds
the named framebuffer and forwards to glClearBuffer*, Magma queues the payload against the
named framebuffer rather than the bound one.

direct_state_access.framebuffers_clear_errors passes on both backends, and
framebuffers_clear passes on Espryt. Magma still fails that one, for a separate reason on
the materialization side rather than in these entry points.
2026-08-04 23:41:28 -04:00
BZLZHH 1011d9fea1 [Test] (MG_Test): follow the query-name and incomplete-texture rules the CTS pinned down
Two unit tests asserted behaviour the conformance tests had since contradicted, so they
were testing MobileGL's old answer rather than GL's.

QueryTest expected glIsQuery to report a name straight out of glGenQueries as a query
object. It is not one: GenQueries reserves names, and they "acquire query state only when
they are first used by calling BeginQuery" (GL 4.6 core 4.2.1). The test now checks that a
reserved name reads FALSE, that BeginQuery is what turns it into an object, and that a
sibling name left untouched stays FALSE. A companion case covers the direct state access
half, where glCreateQueries does create the object outright - which is the whole reason the
two entry points both exist.

The DirectGLES binding test built its texture with glGenTextures and glBindTexture and
nothing else, then expected BindCurrentTextures to bind it natively. A texture with no
image is incomplete and samples as (0, 0, 0, 1), which DirectGLES expresses by leaving the
native target unbound, so the setup no longer produced the binding the test then went on to
clear. It now gives the texture a format and a 1x1 level 0 - one level is the entire mip
chain at that size, so it is complete under any filter - and asserts that directly, so a
future completeness change fails on the setup line instead of on the assertion three calls
later.
2026-08-04 23:13:02 -04:00
BZLZHH b5565ae503 [Docs] (tools/cts): refresh the DSA reference tables after the multisample storage fix 2026-08-05 02:50:22 +00:00
BZLZHH b06ad3f877 [Fix] (MG_Impl): make multisample texture storage immutable, and validate it by name
glTexStorage2DMultisample and glTexStorage3DMultisample forwarded straight to the
glTexImage*Multisample allocation and stopped there. The allocation is indeed the same;
what the storage forms add is that it is final - TEXTURE_IMMUTABLE_FORMAT becomes TRUE and
any later call on that texture is INVALID_OPERATION (GL 4.6 core 8.19). MobileGL left the
texture mutable forever, so it reported TEXTURE_IMMUTABLE_FORMAT as FALSE and accepted
being respecified any number of times, silently discarding storage a test or an
application had already rendered into.

The by-name forms had no validation of their own either. The target forms get their target
checked when the binding is resolved; reached by name there is no binding, so
glTextureStorage2DMultisample took any texture, any extent and any sample count. It now
rejects a target that belongs to the other entry point (INVALID_OPERATION), extents
outside 1..GL_MAX_TEXTURE_SIZE and a depth past GL_MAX_ARRAY_TEXTURE_LAYERS
(INVALID_VALUE), and a sample count above GL_MAX_SAMPLES (INVALID_OPERATION) - measured
against the limit the getter reports rather than the backend parameter it is derived from,
since the frontend raises that number.

glTextureStorage1D/2D/3D gained the same treatment: a target belonging to a different one
of the three is INVALID_OPERATION, a zero extent is INVALID_VALUE (immutable storage
describes a real image, unlike glTexImage*D where an empty level is legal), and a level
count longer than the level-zero size admits is INVALID_OPERATION. Which dimensions take
part in that mip chain is per target: a 1D array keeps its layer count in height, so its
height does not halve.

Takes direct_state_access.textures_storage_multisample_2d_* from 0 to 30 of 30 on Espryt,
and the whole group from 74.93% to 82.48%. Magma still fails them for a separate reason.
2026-08-05 02:49:45 +00:00
BZLZHH 3311e6034a [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.
2026-08-05 02:36:31 +00:00
BZLZHH 027c1bd4ab [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.
2026-08-05 02:31:46 +00:00
BZLZHH da52cc3906 [Docs] (tools/cts): refresh the DSA reference table for the fixes in this branch 2026-08-05 02:29:42 +00:00
BZLZHH ebe4fe133f [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.
2026-08-05 02:29:25 +00:00
BZLZHH 534ec65dda [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.
2026-08-05 02:26:50 +00:00
BZLZHH 35ad1ae7fc [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.
2026-08-05 02:22:09 +00:00
BZLZHH 6152ee933f [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.
2026-08-05 02:21:13 +00:00
BZLZHH dac02ca044 [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.
2026-08-05 02:16:48 +00:00
BZLZHH 42fd02d82f [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.
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
BZLZHH 28c3cfc1d6 [Fix] (DirectVulkan, MG_State): make a shader-written storage buffer readable on Magma
Reading a buffer a compute shader wrote gave zeros: the frontend shadow that MapBuffer
resolves against is only maintained by uploads, and Magma had no path back. Every
KHR-GL40.texture_gather case ends by dispatching a compute shader into an SSBO and
comparing the mapped result, so 66 of 75 failed on it.

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

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

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

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

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

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

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

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

transform_feedback* on Magma: 4 failures -> 3. Espryt stays at 38/38.
2026-08-04 11:42:44 -04:00
BZLZHH 38497174c8 [Feat] (MG_Impl): implement the double-precision uniform state
glUniform*d, glUniformMatrix*dv, their glProgramUniform twins and glGetUniformdv were
all stubs - 35 entry points - so a GL 4.0 program's double uniforms could be declared
and located but never set or read. Worse, glGetUniformfv on one did reach the storage:
the generic getter memcpy'd the uniform's declared size into the caller's buffer, so a
4-byte float pointer received 8 bytes. That overrun is what took the process down in
KHR-GL40.gpu_shader_fp64.fp64.state_query.

The upload path is already templated on the component type, so the vector forms are
wiring. A matrix is not: the column stride the linker used for a double matrix is not
the 16 bytes a float one gets. It is not guessed - the slot the uniform was given is
exactly `columns` columns wide, so dividing states the stride the rest of the pipeline
already agreed on, for both the upload and the readback.

The four getters now convert instead of reinterpreting when the uniform holds doubles,
following GL 4.6 core 7.6: round to nearest for the integer queries, and clamp into the
queried type's range so a negative double read through glGetUniformuiv is 0 rather than
its two's complement.

The case still fails one step further on, where it queries the same uniforms through
GL_ARB_program_interface_query: those calls are answered by the backend program, and an
fp64 shader has none - ESSL has no doubles, so it never links. Answering them from the
frontend reflection is a separate change.
2026-08-04 11:06:15 -04:00
BZLZHH b95fcb7bca [Feat] (MG_State, MG_Impl, DirectGLES): implement glPatchParameteri
GL_PATCH_VERTICES decides how many vertices one tessellation patch consumes, and
glPatchParameteri was a stub - so the value stayed at the driver's default of 3 no
matter what the application asked for. KHR-GL40.texture_gather.gather-tesselation-shader
sets it to 1 and then draws a single patch: with the request dropped the draw had too
few vertices for one patch, produced nothing at all, and the case read back the clear
colour.

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

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

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

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

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

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

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

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

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

Which samplers were declared rectangle is no longer visible in the transpiled source -
they are plain sampler2D by then - so the names come from the frontend program's
reflection.
2026-08-04 10:38:01 -04:00
BZLZHH f38dbf018d [Fix] (MG_State): give a rectangle texture its own initial sampler state
Every texture object started from the shared defaults, which are the 2D ones:
TEXTURE_MIN_FILTER of NEAREST_MIPMAP_LINEAR and TEXTURE_WRAP_S/T of REPEAT. A
rectangle texture has no mip chain at all, so GL gives it a different initial state -
LINEAR and CLAMP_TO_EDGE (GL 4.6 core table 23.15) - and a mipmapped minification
filter is not even a legal value to set on one.

With the 2D default in place a rectangle texture was mipmap-incomplete the moment it
was created, and an application that (correctly) never touches the filters read
(0, 0, 0, 1) out of every lookup. That is what the eleven
KHR-GL40.texture_gather.*-2drect cases saw: they set only the wrap modes, because the
filters are already what a rectangle texture needs.
2026-08-04 10:37:46 -04:00
BZLZHH 2dcc15bb0e [Feat] (MG_Impl, DirectGLES): advertise GL_ARB_get_program_binary with no binary format
glProgramParameteri is not core before GL 4.1, so in the 4.0 context the CTS runs it
only exists through GL_ARB_get_program_binary or GL_ARB_separate_shader_objects.
MobileGL advertised neither, so dEQP's loader left the entry point null - and
KHR-GL40.api.coverage, which registers glProgramParameteri from GL 3.2 upwards, called
straight through the null pointer and took the process down.

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

Applications that ask for a binary get the documented "no formats" answer and fall
back, which is what they already had to do - only now they can ask.
2026-08-04 10:27:23 -04:00
BZLZHH ff76af9df7 [Fix] (MG_State, MG_Impl): a transform feedback name is only an object once it is bound
glIsTransformFeedback answered GL_TRUE for any name glGenTransformFeedbacks had handed
out. A generated name is reserved but does not denote an object until the first
glBindTransformFeedback (GL 4.6 core 13.2.1) - the same rule the other object types
follow - and KHR-GL40.api.coverage checks exactly the window in between.

The two questions are now asked separately: whether a name may be bound or deleted
(reserved, which is what the delete and bind paths need) and whether it is an object
(reserved and bound at least once).
2026-08-04 10:27:23 -04:00
BZLZHH 76f37a18e6 [Feat] (MG_State, MG_Impl, DirectGLES): transform feedback objects, pause/resume and the special capture names
GL 4.0 folds ARB_transform_feedback2 and _3 into core, and neither existed:
glGenTransformFeedbacks, glBindTransformFeedback, glDeleteTransformFeedbacks,
glIsTransformFeedback, glPause/ResumeTransformFeedback, the whole
glDrawTransformFeedback family and glBegin/EndQueryIndexed were all stubs, and
gl_NextBuffer / gl_SkipComponents1..4 failed the link as "not an output of the vertex
stage". Seven KHR-GL40.transform_feedback* cases failed on it, three of them by
leaving a capture open at deinit and taking the process down.

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

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

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

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

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

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

KHR-GL40.transform_feedback, transform_feedback2 and transform_feedback3: 38/38.
2026-08-04 10:18:53 -04:00
BZLZHH 8d1a734c22 [Fix] (MG_State, MG_Impl): reject a draw mode the geometry stage cannot accept
A geometry shader declares the primitive type it consumes, and a draw may only present
a mode that decomposes into it - points for `points`, the three triangle modes for
`triangles`, and so on (GL 4.6 core 11.3.1). Anything else is GL_INVALID_OPERATION.
Nothing checked it, so KHR-GL40.draw_indirect.negative-gshIncompatible-arrays and
-elements drew points through a `layout(triangles) in` shader and got no error.

The program object had no notion of the geometry input primitive at all: glslang knows
it right after the link, so it is read off the geometry intermediate and kept as the
GL enum (this is also what GL_GEOMETRY_INPUT_TYPE would report). Resolved on every
link rather than only when transform feedback captures the stage, since every draw
consults it, and cleared with the rest of the link artifacts.

The check sits on the shared pre-draw gate next to the transform feedback primitive
rule, which is the same shape of constraint. GL_PATCHES is deliberately exempt: it is
the tessellation pipeline's input and has already become the tessellator's output
primitive by the time the geometry stage sees it.

draw_indirect is now at 70/70.
2026-08-04 09:55:36 -04:00
BZLZHH 7d215028fb [Fix] (MG_Impl): validate the draw mode and the indirect draw's command source
Two classes of draw-time error were never raised, which the KHR-GL40.draw_indirect
negative-* cases check one by one:

- `mode` was passed through unexamined, so glDrawArraysIndirect(GL_FLOAT, ...) reached
  the backend instead of raising GL_INVALID_ENUM. The check belongs on the shared
  pre-draw gate, so it now covers every draw entry point rather than just the indirect
  pair. Nothing that used to render stops rendering: a mode the frontend now rejects is
  a mode the backend driver was rejecting anyway, silently.
- The indirect commands read their arguments out of the buffer bound to
  GL_DRAW_INDIRECT_BUFFER, and all three of that source's preconditions were unchecked
  (GL 4.6 core 10.3.10): a 4-byte-aligned offset, a bound buffer at all, and enough room
  left in it for the whole 16- or 20-byte command. glDrawElementsIndirect also never
  validated its index type, which is the same accepted set as the rest of the
  DrawElements family.

Takes the group from 24 failures to 2 - both of the remaining ones are the geometry
shader input-primitive compatibility rule, which needs reflection the program object
does not keep yet.
2026-08-04 09:52:40 -04:00
BZLZHH 00534d8bbc [Fix] (MG_Impl): report the draw-indirect binding and the buffer access state
Three pieces of queryable buffer state were missing, all of them read by the
KHR-GL40.draw_indirect basic-binding-* and basic-buffer-* cases:

- GL_DRAW_INDIRECT_BUFFER_BINDING had no case in glGetIntegerv, so it raised
  GL_INVALID_ENUM and left the caller's variable untouched (the test read back its own
  -9999 sentinel). GL_DISPATCH_INDIRECT_BUFFER_BINDING right next to it was already
  handled; this is the same two lines against BufferTarget::DrawIndirect. Because
  glGetBooleanv/glGetFloatv/glGetDoublev all widen from the integer path, one case
  fixes all four getters.
- GL_BUFFER_ACCESS answered 0 for an unmapped buffer. Its initial value is
  GL_READ_WRITE and glUnmapBuffer restores it (GL 4.6 core table 6.2); 0 is not a legal
  value of that state at all, and the test threw on the unrecognised enum.
- GL_BUFFER_ACCESS_FLAGS was not implemented, so it fell through to the invalid-pname
  arm. It is the MapBufferRange bitfield verbatim, which the mapping access flags
  already hold in normalised form - glMapBuffer's access enum is converted on the way
  in - so it converts straight back out, and reads zero while unmapped.
2026-08-04 09:52:40 -04:00
BZLZHH d81a6a0998 [Fix] (MG_Impl): silently ignore program and shader name zero on delete
glDeleteProgram and glDeleteShader are the two entry points in the program/shader name
space where 0 is not "a name GL never handed out" but an explicit no-op: "if program is
zero, it is silently ignored" (GL 4.6 core 7.3, and 7.1 for shaders). Both went through
the shared name validator instead and recorded GL_INVALID_VALUE.

Only tests that never got as far as creating a program noticed, because they still run
their cleanup path: the five KHR-GL40.texture_gather.*-cube-array cases bail out of
Init with "GL_ARB_texture_cube_map_array not supported", then Cleanup deletes its
zero-initialised handles and the leftover error fails the case after the fact - the
downstream-error-misattribution shape. Every array-taking delete already skipped 0.
2026-08-04 09:52:40 -04:00
BZLZHH 9bf23d7ffd [Fix] (MG_State, DirectGLES): read a shader-written storage buffer back before mapping it
Buffer contents live in a CPU shadow that every read - MapBuffer, MapBufferRange,
GetBufferSubData, CopyBufferSubData - resolves against, and backend transfer ops only
ever push the shadow outwards. Two paths already knew the GPU can write a buffer on
its own and mirrored the result back by hand (ReadPixels into a pixel-pack buffer,
the transform feedback capture at EndTransformFeedback); a shader storage buffer
written by a draw or a dispatch had no such path at all, so the map handed the
application the bytes from before the dispatch.

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

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

Drops the texture_gather failures from 71/75 to 25/75 with no crashes left.
2026-08-04 09:40:53 -04:00
BZLZHH 41e45f7d48 [Fix] (MG_Impl): let an indexed buffer bind reach the generic binding point too
BindBufferBase and BindBufferRange bind the buffer to the indexed point AND to the
generic binding point of the same target (GL 4.6 core 6.1.1); only the indexed half
was implemented. Applications lean on the second half constantly, because it is what
makes the set-up idiom work:

    glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, ssbo);
    glBufferData(GL_SHADER_STORAGE_BUFFER, size, nullptr, GL_DYNAMIC_DRAW);

With the generic point left at 0 the glBufferData raised GL_INVALID_OPERATION and
the buffer kept its zero size, so the later glMapBufferRange over it failed the
offset+length bound and returned nullptr. The whole KHR-GL40.texture_gather group
verifies its result through exactly that sequence and dereferences the map's return
value without checking it, so 51 of its 75 cases took the process down with a
SIGSEGV inside the test.

Unbinding propagates the same way: buffer 0 clears both points.
2026-08-04 09:40:28 -04:00
BZLZHH 3dc6a1b6db [Fix] (MG_Impl, DirectGLES): answer the texture-gather offset limit queries
glGetIntegerv(GL_MIN_PROGRAM_TEXTURE_GATHER_OFFSET) and its GL_MAX_ counterpart fell
through to the default arm of the getter and raised GL_INVALID_ENUM, leaving the
caller's variable untouched - KHR-GL40.texture_gather.api-enums read back the
uninitialised 32764 that happened to be on its stack and failed on the error alone.

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

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

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

Keeps the single-level backing and its memory saving; ssim goes back to 0.9992.
2026-08-02 13:09:48 -04:00
BZLZHH 86c00bdf18 [Test] (MG_Test): catch the unit tests up with three deliberate behaviour changes
ctest -L unit had been failing 13 of its 418 cases, all of them tests left asserting
what the code did before a commit that changed it on purpose:

- "restore target GL version to 3.3" put the advertised target back after the
  experimental 4.6 run, but the two Voxy sanity tests still demanded 4.6. The
  extensions they really care about are all still advertised, so assert 3.3 and drop
  the now-meaningless AtExperimentalCTSVersion from their names.
- "support rectangle textures where the emulation is exact" made every desktop-only
  target supported - rectangle included, stored as a plain 2D - while the texture
  test still expected rectangle to be rejected.
- "keep declared modern GLSL versions strict" changed two things at once: a
  normalized legacy directive now carries a marker on its line, so the ten tests
  matching "#version 330 core\n" whole no longer match; and a version the
  application declared itself is no longer raised to 460, so the sources declaring
  330/400 keep their own number and only MobileGL's own normalization is retargeted.

Test expectations follow, rather than the implementation being bent back: each of
the three changes is the intended behaviour and is argued for where it was made. The
retry test now drives the 460 escalation from a legacy "#version 130" source, which
is the only thing that is still rescued, and gained a case pinning the other half of
that contract - an application-declared "#version 330" stays at 330.

418/418 unit tests pass.
2026-08-02 09:04:16 -04:00
BZLZHH 2f2f95498f [Fix] (MG_State): detach a deleted texture from the framebuffer that is bound
GL 3.3 core 4.4.2: deleting a texture whose image is attached to the framebuffer
currently bound acts as if FramebufferTexture* had been called with texture zero for
every attachment point it occupied there. Framebuffers that are not bound keep the
orphaned attachment, so only the bound ones are touched.

MobileGL unbound a deleted texture from every texture unit and image binding but
left framebuffer attachments alone, so the framebuffer went on holding the dead
texture alive as its attachment and reads through it returned that texture's
contents rather than those of whatever the application put in its place - and since
the deleted name usually comes straight back out of the next glGenTextures, the two
are indistinguishable from the outside.
2026-08-02 07:25:07 -04:00
BZLZHH 7105c2ebdc [Fix] (DirectGLES): never skip a framebuffer bind on a stale version snapshot
BindCurrentFBO returned early when the framebuffer binding slot's version matched
g_fboBindVersions - but nothing on that path ever writes that entry. Only
ForceBindCurrentFBO stamps it, so the comparison was against an arbitrarily old
snapshot, and any later slot version that happened to land on the same 16-bit value
read as "already bound". The driver was then left on whatever framebuffer it had
last been given.

That is how KHR-GL32.packed_pixels.varied_rectangle.rg8i_format_rg_integer read its
gradient back out of the previous subtest's framebuffer, seeing 18 where 127 was
expected. It only shows up after a few thousand cases have gone by - long enough for
the counter to come back around - which is why it reproduced exactly under one
caselist and not at all in isolation.

Drop the fast path. Skipping redundant work is BindFramebufferId's job: it shadows
the driver's own draw and read bindings and drops the glBindFramebuffer when the
target already holds that id, which is where the cost actually is. What is left here
is one registry lookup.

Takes GL32 to 100% conformance; GL30, GL31 and GL33 stay at 100%.
2026-08-02 07:24:59 -04:00
BZLZHH 13bab780f2 [Fix] (DirectGLES): gate the replicate blit's stencil pass on ES 3.1
Reading the stencil half of a packed depth/stencil texture goes through
GL_DEPTH_STENCIL_TEXTURE_MODE, which is ES 3.1 state. On an older driver the pname
would raise GL_INVALID_ENUM and the shader would go on sampling depth bits as if
they were stencil, so decline the emulation instead.
2026-08-02 04:55:05 -04:00
BZLZHH 027310f993 [Fix] (MG_Impl): ask whether a colour format is renderable per target
The framebuffer-completeness check scanned every row of the backend's
format-capability cache and called the format renderable if any target said so. That
was already loose, and it broke outright once DirectGLES started widening
three-channel formats so they stay renderable as multisample storage: the caveat
capability recorded for the multisample target made GL_RGB8_SNORM look renderable
everywhere, so an ordinary 2D GL_RGB8_SNORM texture attachment reported
GL_FRAMEBUFFER_COMPLETE while the driver's own framebuffer was
INCOMPLETE_ATTACHMENT.

KHR-GL3x.packed_pixels stopped skipping those formats and read a framebuffer that
could not be read, so all 18 of its rgb8_snorm cases got back an untouched buffer.

Pass the row the attachment actually lives in - the texture's target, or the
renderbuffer row - and consult only that one; a format is still asked about in
general when the caller has no target.
2026-08-02 04:54:48 -04:00
BZLZHH c741a938bc [Feat] (DirectGLES): emulate a depth/stencil blit into a multisample framebuffer
Desktop GL replicates the source sample into every destination sample when the read
framebuffer is single-sampled and the draw framebuffer is not. ES forbids the call
outright - "an INVALID_OPERATION error is generated if SAMPLE_BUFFERS for the draw
framebuffer is greater than zero" - so the blit did nothing at all, and every one of
KHR-GL3x.packed_depth_stencil.blit's replicate iterations verified a destination
that still held its clear values.

Emulate it by drawing a full-screen triangle into the multisample framebuffer: every
pixel is fully covered, so every sample of it receives the same value, which is
precisely the replicate rule. The source rectangle is first copied into a scratch
texture of its own format (both sides single-sampled, which ES does allow), then
depth is written through gl_FragDepth and stencil - which has no shader output on ES
- one bit plane at a time with REPLACE and a discard for the pixels whose source bit
is clear.

The draw runs inside the caller's framebuffer, so every piece of pipeline state it
touches is read back and restored, including the per-draw-buffer colour masks the
non-indexed glColorMask does not cover: the sync layer's shadow of the driver state
has to stay true across this.

Colour replicate is not emulated (it would need a sampler variant per component
type); it now says so instead of failing silently.
2026-08-02 04:34:25 -04:00
BZLZHH 01d0f01d13 [Fix] (DirectGLES): report the alpha added by the multisample widening as ONE
A three-channel format widened to four for a multisample target gains an alpha
channel the application never asked for, and it holds whatever the draw that filled
the texture happened to write there. GL says a format without alpha reads back as
1.0, so KHR-GL33.texture_swizzle - which fills such a texture by rendering
vec4(r, g, b, 0.0) and then swizzles red from alpha - read 0 where it expected the
maximum.

Fold ONE into the texture's swizzle for exactly those textures, composed with the
swizzle the application set, so the promotion stays invisible.
2026-08-02 04:25:08 -04:00
BZLZHH e45f7ae5d4 [Fix] (DirectGLES, MG_Util): keep 16-bit SNORM precision through the widening
GL_RGB16_SNORM widened to GL_RGBA16F to stay renderable as multisample storage, and
a half float's 11-bit mantissa cannot hold a 16-bit signed-normalized channel:
KHR-GL33.texture_swizzle's blue channel came back several units of 32767 away from
the value the reference computes, well outside its one-unit tolerance.

GL_EXT_render_snorm makes the signed-normalized formats colour-renderable on ES, so
widen to GL_RGBA16_SNORM instead wherever it and EXT_texture_norm16 are both
present, and only fall back to the half float otherwise. Threaded through as its own
normalize option so the capability probe and the runtime pick the same format, the
way every other driver-dependent substitution here is decided.
2026-08-02 04:24:23 -04:00
BZLZHH a687873d32 [Fix] (DirectGLES): probe format capabilities on the target ES stores them on
1D, 1D-array and rectangle textures are emulated on ES 2D and 2D-array targets, but
the capability probe kept asking the driver about the desktop-only target itself.
glTexImage2D(GL_TEXTURE_1D, ...) is not something an ES driver has ever accepted, so
those rows of the cache stayed empty - and an empty row reads as "nothing is known",
not as "the format needs help", so no fallback format was ever selected for them.

GL_DEPTH_COMPONENT32 on a 1D texture therefore went to the driver unchanged instead
of as GL_DEPTH_COMPONENT24, and the texture ended up with no storage
(KHR-GL33.texture_swizzle format_idx_65 on both 1D targets read the wrong value for
every pixel).

Probe the ES target the texture will actually live on, while still recording the
capabilities against the target the frontend asked for.
2026-08-02 04:18:20 -04:00
BZLZHH 43a43c1180 [Fix] (DirectGLES, MG_Util): raw framebuffer writes while GL_FRAMEBUFFER_SRGB is off
GLES core always encodes a fragment written into an sRGB colour attachment, and
offers no switch to stop it. Desktop GL has one, GL_FRAMEBUFFER_SRGB, and it starts
out disabled - so a GL application that never touches it expects its writes to land
raw. The frontend models exactly that (the capability reads as disabled and
DirectVulkan attaches the UNORM twin to honour it), but DirectGLES was passing the
draw straight to a driver that encodes anyway.

The value therefore came back one conversion short of the reference wherever it was
written and then read again: rendering into an sRGB texture and fetching it in a
shader decodes once but had encoded twice, which is how
KHR-GL32.texture_size_promotion read 0.0142 for GL_SRGB8_ALPHA8 where 0.00111 was
expected.

Detect GL_EXT_sRGB_write_control and sync GL_FRAMEBUFFER_SRGB from the frontend
capability alongside the other enables, starting from the driver's enabled state so
the first sync always pushes the disable down.
2026-08-02 04:10:13 -04:00
BZLZHH 65dbfa6f26 [Fix] (DirectGLES, MG_Util): widen three-channel formats for multisample textures
GLES has no colour-renderable three-channel format beyond RGB8, so
glTexStorage2DMultisample rejects GL_RGB16 (and the SNORM variants) with
GL_INVALID_ENUM and the texture is left with no storage at all - every draw into it
then hit GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT and every read came back zero.

The existing fallback machinery could not help: it picks one replacement format per
requested format, from the driver's capabilities, and never re-checks that
replacement against the target it is going to be used with. GL_RGB16's fallback is
GL_RGB32F, which is a perfectly legal ES texture format and a perfectly illegal
multisample storage format, and with EXT_texture_norm16 present no fallback was
selected at all.

Add PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget, applied only to
multisample targets, mapping GL_RGB16 to GL_RGBA32F and the three-channel SNORM
formats to GL_RGBA16F. Widening the channel count is safe precisely there and
nowhere else: a multisample texture can never be uploaded to, only rendered into, so
no transfer path has to expand three-channel client data, and the alpha a draw
writes for a three-channel source is already the 1.0 the frontend format implies.

The capability probe recomputes its fallback per target for the same reason, so the
probed format and the format the texture is actually created with stay in agreement.
2026-08-02 04:10:01 -04:00
BZLZHH 6de38c666c [Feat] (DirectGLES): support rectangle textures where the emulation is exact
ES has no rectangle target and no rectangle sampler, so DirectGLES declared
GL_TEXTURE_RECTANGLE unsupported outright: the texture was never synced or bound,
and SPIRV-Cross refused the shader ("Rectangle textures are not supported on
OpenGL ES") which left the whole program unlinkable.

A rectangle texture is a single-level, clamped 2D texture whose only real
difference is that its lookups take non-normalized coordinates. Where every use
takes *integer* texel coordinates - texelFetch, textureSize - that difference
does not exist at all, and the two are the same thing. So:

- A new SPIR-V pass rewrites Dim::Rect image types to Dim::2D before
  transpiling, and restates the rectangle capabilities as Shader. It declines
  any module containing a normalized-coordinate lookup rather than emitting
  something subtly wrong; SPIRV-Cross then rejects that module exactly as
  before, so nothing that used to work changes and nothing new renders wrongly.
- The target maps to GL_TEXTURE_2D for storage, uploads and binding, alongside
  the existing 1D and 1D-array emulation.

Fixes KHR-GL31.texture_size_promotion.functional outright, which takes GL31 to
100% conformance. GL32/GL33 advance past their rectangle cases to a separate
GL_RGB16 multisample issue. No regressions across texture_swizzle, shaders30,
texture_lod_*, framebuffer_blit, packed_depth_stencil, transform_feedback,
clip_distance or draw_buffers; DirectVulkan re-verified unaffected.
2026-08-02 01:56:22 -04:00
BZLZHH 9dff24f3e1 [Fix] (DirectGLES): never leave a stale program bound when the new one is broken
When a shader stage fails to transpile or compile, SyncToBackend logs it and
carries on, so the program is linked without that stage - or does not link at
all. Use() then issued glUseProgram for it, which is an INVALID_OPERATION for an
unlinked program and, crucially, leaves the *previous* program current. The draw
went ahead and rendered with an entirely unrelated shader.

That is how KHR-GL3x.texture_size_promotion's GL_TEXTURE_RECTANGLE cases
produced 1.0 for a red channel: SPIRV-Cross refuses sampler2DRect for ESSL
("Rectangle textures are not supported on OpenGL ES"), so every rectangle
program was broken, and the draws kept running the previous case's 1D-array
alpha shader - whose alpha is 1.0. Wrong pixels from a shader the app never
bound are far worse to debug than a blank result.

The program now records whether the last sync produced something usable, and
Use() binds 0 rather than the broken program, making the draw a visible no-op.
The redundancy cache tracks whatever was actually bound, so it stays correct
across the switch.

Does not fix the rectangle cases themselves - those need a SPIR-V pass lowering
Dim::Rect to Dim::2D before SPIRV-Cross runs (plus the coordinate divide for
non-texelFetch lookups), alongside mapping the target to GL_TEXTURE_2D.
2026-08-02 01:40:53 -04:00
BZLZHH 5a400e0297 Merge branch 'dev' of github.com:MobileGL-Dev/MobileGL into dev 2026-08-02 12:21:25 +08:00
BZLZHH b6a2bf08d4 [Fix] (DirectGLES): resolve an aliased texture unit by the sampler's type
Desktop GL_TEXTURE_1D/1D_ARRAY are emulated on ES GL_TEXTURE_2D/2D_ARRAY, so one
native binding serves two of a unit's frontend slots. An earlier fix settled the
real-versus-default case; two REAL textures can collide just as easily, and there
the slot iteration order decided it. KHR-GL3x.texture_size_promotion keeps its 1D
source texture and its 2D destination texture bound to the same unit, so the
shader sampled the render target it was drawing into instead of the source.

GL resolves this from the shader's sampler type, so ask the program: the
frontend's uniform reflection still carries the original GLSL type, which maps
straight back to the target the lookup means. Only consulted when a collision
actually happens, so an ordinary unit costs nothing, and the first binding
placed stands when the program gives no answer rather than being overwritten by
whichever slot happens to come last.

Also adds the read-colour clamp that goes with it: GL clamps a glReadPixels from
a fixed-point colour buffer to [0,1] (GL_CLAMP_READ_COLOR defaults to
GL_FIXED_ONLY), which ES has no equivalent for at all - a GL_R16_SNORM target
holding -0.125 read back unclamped. Applied to the wide rows before they are
repacked, for float, half, short and byte reads alike, and deliberately NOT for
glGetTexImage, which reaches the same helper through a scratch framebuffer but
is not subject to read-colour clamping.

texture_size_promotion now clears every 1D case (it stops at the first failure
and has moved on to GL_TEXTURE_RECTANGLE, which DirectGLES does not emulate at
all yet), and KHR-GL33.texture_swizzle's GL_DEPTH_COMPONENT32 1D cases pass.
DirectVulkan re-verified unchanged.
2026-08-01 16:21:28 -04:00
BZLZHH 6b2a2b5e00 [Fix] (MG_Util): emulate GL_DEPTH_COMPONENT32 with the 24-bit sized format
GL_DEPTH_COMPONENT32 has no ES equivalent. The previous commit routed it to
GL_DEPTH_COMPONENT32F, which gives the attachment storage but changes the
encoding: the transfer type has to become GL_FLOAT for ES to accept the store,
and the upload path hands over the caller's fixed-point GL_UNSIGNED_INT bytes
unchanged, so the texels came out as garbage.

GL_DEPTH_COMPONENT24 is the nearest sized ES format that keeps the same
fixed-point encoding, so GL_UNSIGNED_INT still describes the data and no
conversion is needed. Fixes KHR-GL33.texture_swizzle's GL_DEPTH_COMPONENT32
cases on the 2D and 2D-array targets; framebuffer_blit's GL_DEPTH_COMPONENT32
config still passes, since the depth values it compares are exactly
representable in 24 bits.

(The 1D and 1D-array targets still fail, but for the separate desktop-1D-on-ES
emulation reason that also holds back texture_size_promotion.)
2026-08-01 14:59:19 -04:00
BZLZHH 512c857f18 [Fix] (DirectGLES): emulate a format-converting multisample resolve blit
ES rejects any blit out of a multisample read framebuffer whose format differs
from the draw framebuffer's. Desktop GL only requires identical formats when
BOTH framebuffers are multisampled - a multisample resolve into a single-sample
target is allowed to convert on the way out, and KHR-GL3x.framebuffer_blit
resolves an R8 multisample texture straight into the RGBA8 default framebuffer.
The forwarded blit failed with GL_INVALID_OPERATION, and since the driver's
error never reaches the frontend error queue the caller saw a successful call
that had written nothing.

Retried in two steps when the first blit fails and the read framebuffer really
is the multisampled one: resolve into a scratch renderbuffer of the source's own
format, then run the caller's blit from there - single-sample on both sides,
which is exactly where ES does allow the conversion. The scratch buffer is
cached and grown on demand, keyed on the source format and dropped with its ES
context. Only reached on the failure path, so an ordinary blit is untouched.

KHR-GL3{0,1,2,3}.framebuffer_blit is now 3/3 on all four versions; DirectVulkan
(lavapipe) re-verified at 3/3 as well.
2026-08-01 14:37:58 -04:00
BZLZHH 9f3cac6691 [Fix] (DirectGLES): report distinct depth/stencil framebuffers as unsupported
GL only requires framebuffers whose depth and stencil attachments refer to the
same image; anything else may be answered GL_FRAMEBUFFER_UNSUPPORTED, and both
backends' real targets do exactly that - DirectVulkan cannot form two separate
attachments at all, and the ES drivers behind DirectGLES return UNSUPPORTED for
a separate depth renderbuffer plus stencil renderbuffer.

The frontend already knew how to detect the configuration, but only consulted it
for DirectVulkan. On DirectGLES it answered GL_FRAMEBUFFER_COMPLETE for a
framebuffer the driver had rejected, so every clear and draw against it was
silently dropped and the results read back as zeros - which is what
KHR-GL3x.packed_depth_stencil.verify_mixed_attachments saw. (That test
explicitly tolerates GL_FRAMEBUFFER_UNSUPPORTED; what it cannot survive is being
told the framebuffer works.)

Turned into a backend capability rather than a backend-type check, probed once
at init from a scratch framebuffer the same way the format-capability cache is,
so a driver that does support the configuration keeps using it. Defaults to
supported, leaving any backend that does not set it on the permissive path.

Fixes KHR-GL3{2,3}.packed_depth_stencil.verify_mixed_attachments for both
formats; DirectVulkan re-verified unchanged at 23/25 pass + 2 not-supported.
2026-08-01 14:05:28 -04:00
BZLZHH 4c7332d5e6 [Fix] (DirectGLES): broadcast legacy gl_FragColor to every draw buffer
Legacy GLSL's gl_FragColor goes to every enabled draw buffer (GL 4.6 15.2.3),
but ShaderSourceProcessor lowers it to a single mg_FragColor output, which only
ever reaches draw buffer 0. Everything past the first attachment kept its
pre-draw contents.

Replicated across the enabled draw buffers with copies at the end of main.
Gated on the count so the ordinary single-target shader is byte-for-byte what it
was: the pass is a no-op below two draw buffers, and the count comes from the
frontend draw framebuffer at program-sync time (not from the backend framebuffer
sync, which only runs later in PrepareForDraw - a program compiled against a
stale count would not be relinked until the draw after the one that needed it).
It joins the snorm/unorm clamp masks as framebuffer state the shader is compiled
against, with the same relink-on-change check.

Also advertises GL_ARB_explicit_attrib_location and GL_ARB_texture_multisample,
which DirectGLES implements for every version it advertises but only listed for
DirectVulkan. Both are core from GL 3.2/3.3 on, so an app targeting 3.0/3.1
reaches them only through the extension string - without the former the CTS
picks an entirely different draw_buffers shader, and without the latter
KHR-GL31.texture_size_promotion.functional crashed outright.

KHR-GL3{0,1,2,3}.draw_buffers.draw_buffers_1 now passes on all four versions,
and texture_size_promotion.functional on GL31 downgrades from a crash to a
(still open) comparison failure.
2026-08-01 13:59:11 -04:00
BZLZHH 1c5f6c0986 [Chore] (tools/cts): pick a run config the suite does not contradict
Two harness settings were producing failures that say nothing about the backend:

- dEQP's FboRenderContext picks the first entry of its own depth/stencil format
  list, GL_DEPTH32F_STENCIL8, when the config leaves the bit counts DONT_CARE.
  framebuffer_blit meanwhile hardcodes GL_DEPTH24_STENCIL8 for its own buffers
  as soon as it detects an FBO surface, and then blits depth between the two -
  which the spec forbids for mismatched formats, so a conformant driver has no
  choice but to fail it. Default to --deqp-gl-config-name=rgba8888d24s8 so the
  wrapper framebuffer and the test agree.

- --deqp-watchdog aborts the whole process when one case exceeds a hardcoded 30
  seconds (framework/common/tcuApp.hpp). That is not a hang on a CPU rasterizer:
  several texture_swizzle cases take ~17s each standalone and cross the limit
  once the process is warm, which came back as ten spurious Timeouts. dEQP's own
  default is off, and --chunk-timeout is what actually rescues a genuinely
  wedged case, so default it off too and leave it selectable.
2026-08-01 13:48:58 -04:00
BZLZHH 8b75628dec [Fix] (DirectGLES): depth/stencil clear value and readback gaps
Three separate holes, all of them silent, that KHR-GL3x.framebuffer_blit walks
straight into because it clears and reads back depth and stencil directly:

- glClearStencil was frontend-only. The value was recorded in render state and
  never synced, so the real driver kept its default of 0 and every
  glClear(GL_STENCIL_BUFFER_BIT) wrote zeros. glClearColor and glClearDepthf
  were already synced right next to it.

- Stencil readback assumed GL_STENCIL_INDEX works. It is not part of core ES
  (it needs GL_NV_read_stencil) and a driver without it rejects the read
  outright, which left the caller's buffer untouched. Where the attachment is a
  combined depth-stencil buffer the packed GL_DEPTH_STENCIL read carries the
  same bytes in its low octet, so that is now the fallback; the widening to
  GL_UNSIGNED_SHORT/INT moved into the same helper, since even a byte-for-byte
  read needs it.

- Depth readback always went through GL_UNSIGNED_INT. A floating-point depth
  attachment (GL_DEPTH_COMPONENT32F, GL_DEPTH32F_STENCIL8 - the latter is what
  dEQP's own fbo-surface-type wrapper framebuffer picks) rejects that with
  GL_INVALID_OPERATION and only reads back as GL_FLOAT. Try both.

And one format gap behind the same test: GL_DEPTH_COMPONENT32 has no ES
equivalent and was being normalized to the *unsized* GL_DEPTH_COMPONENT base
format, which is not a legal glTexStorage/glRenderbufferStorage internal format
there - the attachment ended up with no storage and the framebuffer read back as
incomplete. GL_DEPTH_COMPONENT32F is the sized ES format that keeps the
requested 32-bit depth footprint; the transfer type follows it to GL_FLOAT.

Takes KHR-GL3x.framebuffer_blit from 0/3 to 2/3 (the remaining
multisampled_to_singlesampled_blit_color_config_test is a separate
single-channel MSAA resolve issue). Note that scissor_blit additionally needs
the suite to run with a depth/stencil config the test agrees with
(--deqp-gl-config-name=rgba8888d24s8): under FBO surfaces the test hardcodes
GL_DEPTH24_STENCIL8 for its own buffers while dEQP's wrapper framebuffer
defaults to GL_DEPTH32F_STENCIL8, and blitting depth between mismatched formats
is a spec error that any conformant driver has to report.
2026-08-01 13:24:30 -04:00
BZLZHH 8269a1786f [Feat] (DirectGLES): emulate GL_TEXTURE_LOD_BIAS in the transpiled ESSL
ES has no per-texture or per-sampler LOD bias at all - GL_TEXTURE_LOD_BIAS is
desktop only, and Vulkan spells it VkSamplerCreateInfo::mipLodBias, which is why
DirectVulkan already honours it. DirectGLES stored the value in sampler state
and then dropped it, so every lookup sampled at the unbiased level of detail.

The bias now reaches the shader as a uniform: a new SPIRV-Cross post-pass
declares one `uniform highp float mg_lodBias_<sampler>;` per mip-capable sampler
and folds it into the level of detail of every lookup that has somewhere to put
it - appended as the bias argument, added to an existing bias, or added to an
explicit textureLod level (Vulkan applies mipLodBias to explicit-LOD fetches too,
and the CTS reference expects the same). texelFetch/textureGather have no bias
by definition, textureGrad offers no argument to fold one into, and the
array-shadow lookups have no bias overload in GLSL at all, so all of those are
left alone. Draws push the bound texture's (or the bound sampler object's, which
overrides it as in GL) value into the uniform, and only when it changed - a
shader whose samplers all have a zero bias issues no extra call at all.

Fixes KHR-GL3{0,2,3}.texture_lod_bias.texture_lod_bias_all.
2026-08-01 13:24:12 -04:00
BZLZHH 3019c68945 [Fix] (MG_Util): glBindBufferBase must not freeze the buffer's size
BindBufferBase_State stored Range1D(0, bufferObject->GetSize()) as the binding
point's range, so the range reflected whatever size the buffer happened to have
at bind time. Binding an empty buffer and giving it storage afterwards is
ordinary application code - glGenBuffers / glBindBufferBase / glBufferData is
exactly the order KHR-GL3{0,2,3}.clip_distance.coverage uses - and the binding
then stayed frozen at [0, 0).

Every backend consumer reads GetRange() as the range the binding actually
covers, so the stale window meant the capture buffer was bound with
glBindBufferRange(..., 0, 0) instead of glBindBufferBase, transform feedback
captured nothing, and the test read back its pre-draw zeros. The same stale
range also under-counted the CPU-side transform feedback capacity accounting.

GL resolves a whole-buffer binding against the object's size at every use;
only glBindBufferRange pins a fixed window, and the binding point already
tracked which of the two it was for the glGetIntegeri_v START/SIZE queries.
GetRange() now resolves the non-explicit case dynamically.

Fixes KHR-GL3{0,2,3}.clip_distance.coverage on Espryt; transform_feedback stays
21/21 on all four versions, and DirectVulkan (lavapipe) re-verified unaffected.
2026-08-01 12:30:46 -04:00
BZLZHH 800142c104 [Fix] (DirectGLES): stop the default texture clobbering an aliased real binding
Desktop GL_TEXTURE_1D/1D_ARRAY have no ES equivalent and are emulated on
GL_TEXTURE_2D/2D_ARRAY, so one native binding serves two frontend slots of the
same texture unit. BindCurrentTextures walked the slots in enum order and let
the last one win, which is wrong as soon as one of an aliased pair holds a real
texture and the other holds the unit's default (name 0) object: the default
would be bound over the real texture and the shader sampled an empty texture,
which GL resolves to opaque black.

The default is only skipped while it has never been given an image, so this
needed nothing more than some earlier test in the same glcts process defining
one on texture name 0 - after which every later case that sampled a 1D texture
returned black. That is the mechanism behind a whole family of failures that
only reproduced when another case ran first: texture_lod_basic.lod_selection,
packed_pixels.varied_rectangle.rgba4_format_bgra, shaders.arrays.{return,
unnamed_parameter}.float_vertex and clip_distance.functional all pass in the
full-suite ordering now.

Resolved with a second pass, mirroring the intent the unbind half of the
function already had ("a default alias must not clear a real binding"): real
textures are placed first, then defaults fill only the native targets nothing
else claimed.
2026-08-01 12:07:23 -04:00
BZLZHH e9382f5329 [Fix] (DirectGLES): exact transform feedback primitive queries
Two leftovers from the capture passthrough, both only observable with a
geometry shader in the pipeline:

- GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN / GL_PRIMITIVES_GENERATED fell back
  to the frontend's CPU accounting, which counts the primitives the draw call
  assembles and so cannot see a geometry stage's amplification. Both are core ES
  query targets (GL_PRIMITIVES_GENERATED from 3.2 on, gated accordingly so an
  older driver doesn't get a stray GL_INVALID_ENUM), so they now go straight to
  the driver's own counters. Generalized the occlusion-query handle's isOcclusion
  flag into the glBeginQuery target it already had to remember for glEndQuery,
  which is what tells the result read to use the core 32-bit getter.

- FixupGsStripCaptureOrder rewrites captured strip triangles from Vulkan's
  (i, i+1, i+2) order into GL's (i+1, i, i+2). A driver-side capture already
  emits GL order, so the rewrite corrupted it - KHR-GL33.transform_feedback
  .geometry read back the odd triangle rotated one vertex. Skipped when the
  backend owns the capture span.

KHR-GL3{0,1,2,3}.transform_feedback is now 21/21 on Espryt; DirectVulkan
(lavapipe) re-verified at 21/21 for the shared frontend change.
2026-08-01 11:57:18 -04:00
BZLZHH df7d1edeca [Feat] (DirectGLES): implement transform feedback capture
Transform feedback was frontend-only on DirectGLES: glBeginTransformFeedback
just flipped MobileGL's own capture state and the real ES driver was never told
to capture anything, so every capture buffer read back as whatever it held
before the draw (zeros for a fresh glBufferData(NULL)). DirectVulkan drives its
capture from its own draw recording, so the shared GLFunctionsTable had no
entries for the span at all.

Capture now runs on the real driver:

- The backend program declares the capture set with glTransformFeedbackVaryings
  before it links. SPIRV-Cross keeps user output names verbatim in the
  transpiled ESSL, so the frontend's requested names carry over unchanged.
- New GLFunctionsTable Begin/EndTransformFeedback entries hand the span
  boundaries to the backend (null for DirectVulkan, which is unaffected).
- The driver-side begin is deferred to the first draw of the span: ES needs the
  capturing program current and the capture buffers bound, and both only become
  true once PrepareForDraw has run. A span that never draws never touches the
  driver, which is what the GL semantics amount to anyway.
- The end mirrors the captured ranges back into the frontend buffer shadows -
  the GPU wrote them behind the frontend's back, so MapBuffer/GetBufferSubData
  would otherwise still return the pre-draw bytes.

Takes KHR-GL32.transform_feedback from 13/21 to 19/21; the two remaining
failures are the geometry-amplified primitive queries, which still go through
the frontend's CPU accounting.
2026-08-01 11:51:19 -04:00
BZLZHH 8025745fa3 [Chore] (Version): bump version to 26.8 for android plugin 2026-08-01 23:46:58 +08:00
BZLZHH 4a533a215a [Chore] (MG_Backend): fix compiling error. 2026-08-01 23:21:35 +08:00
BZLZHH 0a5d7ceb6c [Feat] (DirectGLES): implement GL_ANY_SAMPLES_PASSED occlusion queries
DirectGLES never registered BeginOcclusionQuery/EndOcclusionQuery, so
the frontend rejected the occlusion query targets entirely; the CTS
tests that use them (e.g. packed_depth_stencil.verify_partial/mixed_
attachments) left a stray GL_INVALID_ENUM that a later, unrelated
glGetError() check would report as its own failure
("Uploading buffer data failed" at gluDrawUtil.cpp:363).

Occlusion queries are core ES3 (glGenQueries/glBeginQuery(GL_ANY_
SAMPLES_PASSED, ...)/glEndQuery/glGetQueryObjectuiv), unlike the timer
queries which need GL_EXT_disjoint_timer_query, so they're wired up
unconditionally (independent of MOBILEGL_DISABLE_TIMERQUERY) using the
same handle-based GetQueryResult64/DeleteBackendQuery plumbing already
shared with timer queries. GetQueryResult64 now reads the 0/1 result
through the core 32-bit glGetQueryObjectuiv getter for occlusion
handles instead of the timer-only 64-bit GL_EXT_disjoint_timer_query
getter, since a driver can fully support core occlusion queries while
lacking that extension entirely.
2026-08-01 11:20:38 -04:00
BZLZHH ac81185968 [Fix] (DirectGLES): implement GL_DEPTH_STENCIL readback for ReadPixels/GetTexImage
Neither ReadPixels nor GetTexImage recognized format=GL_DEPTH_STENCIL
(type GL_UNSIGNED_INT_24_8 / GL_FLOAT_32_UNSIGNED_INT_24_8_REV): it
matched none of the native-passthrough gates nor the color-channel
conversion mapping, so both silently no-op'd (logging a compiled-out
MGLOG_E) and left the caller's buffer untouched. Real GLES/GL drivers
already implement this readback natively, so widen the native-pair
gates to include it.

GetTexImage additionally always attached the source texture to its
scratch FBO as GL_COLOR_ATTACHMENT0, which a depth-stencil texture
cannot be (framebuffer-incomplete) - route it through the existing
EnsureDepthAttachment2D(..., withStencil=true) path instead and skip
the color-only glReadBuffer call for that format.

Fixes KHR-GL3{2,3}.packed_depth_stencil.verify_read_pixels,
verify_get_tex_image, and verify_copy_tex_image (which depends on
GetTexImage internally) for both depth24_stencil8 and
depth32f_stencil8.
2026-08-01 11:14:33 -04:00
BZLZHH 9e861f3f7a [Test] (tools/cts): extend the fbo-harness waiver to the Espryt renderer
Same test-methodology artifact as Magma (dEQP's fbo-surface-type
wrapper FBO being mistaken for the true default framebuffer by
ApiCoverageTestCase's ReadBuffer coverage sub-test) - the waiver's
renderer_list only matched "Magma*", so KHR-GL3{0,1,2}.api.coverage
still reported Fail under the DirectGLES (Espryt) backend. Add
"Espryt*" to the same waiver entry.
2026-08-01 10:48:11 -04:00
BZLZHH da6f75dbd1 [Fix] (DirectGLES): cap advertised GL_MAX_SAMPLE_MASK_WORDS to 1
MobileGL's sample-mask state is a single 32-bit word (RenderState::
SampleMaskValue) and SampleMaski_State() hard-rejects any maskNumber
other than 0. DirectGLES forwarded the real underlying driver's
GL_MAX_SAMPLE_MASK_WORDS unmodified (NVIDIA's GLES driver reports 2),
so dEQP's per-test-case gluStateReset - which always calls
glSampleMaski up to that reported word count - hit GL_INVALID_VALUE on
word 1 after every single case and aborted the whole glcts process.
Each restart only got through one more case before repeating, which
run_cts_local.py recorded as a wall of per-case crashes (63 in
packed_pixels.rectangle alone) and tripped its "many empty chunks"
abort heuristic partway through the GL32 suite. 1 is the spec-required
minimum and is what MobileGL actually implements, so cap to it instead
of forwarding the raw driver limit.
2026-08-01 09:59:26 -04:00
BZLZHH 951da362f7 [Fix] (submodules): point include/FastSTL at the fork's already-pushed fix
Our local-only commit f8567f6 for the erase(iterator) bug was never
pushed to MobileGL-Dev/FastSTL and broke CI's submodule checkout
("not our ref"). The fork's own main branch already carries an
equivalent fix (022211c, same root cause) plus a perf improvement on
erase(key) (34f55f9), so switch to that instead of pushing a redundant
duplicate that would diverge from it.
2026-08-01 09:25:01 -04:00
BZLZHH 4c929b9b3f Merge branch 'dev' of github.com:MobileGL-Dev/MobileGL into dev 2026-08-01 21:03:14 +08:00
BZLZHH 8d5072543a [Test] (tools/cts): waive api.coverage's fbo-surface-type wrapper-FBO artifact
KHR-GL3{0,1,2,3}.api.coverage's ReadBuffer coverage sub-test captures
GL_READ_BUFFER while dEQP's own fbo-surface-type wrapper FBO is bound
(a real, non-zero-named FBO, not framebuffer 0), then later deletes an
unrelated FBO of its own. Per the GL spec, deleting a bound FBO
implicitly rebinds framebuffer target 0 - the true default framebuffer
this time, not the wrapper - and restoring the captured
GL_COLOR_ATTACHMENTn value against it correctly raises GL_INVALID_ENUM
(only FRONT/BACK-style tokens are valid there). This is a spec-correct
response to a --deqp-surface-type=fbo-only test-methodology artifact,
not a MobileGL conformance defect, and cannot occur on a real
window/pbuffer-backed run where framebuffer 0 is genuinely bound
throughout. Add a waiver (dEQP's own mechanism for exactly this kind of
known non-defect) instead of weakening the (correct) validation, and
wire --waiver-file through run_cts_local.py.
2026-08-01 08:53:35 -04:00
BZLZHH 5de2b9e3e9 [Chore] (Version): bump version to 26.8 2026-08-01 20:35:31 +08:00
BZLZHH 9192d156d1 [Fix] (DirectVulkan): ReadPixels materializes pending clears before resolving the blit binding
ResolveColorBlitBinding cached a RenderbufferResource*/TextureResource*
(trackedLayout) before the pending-clear materialization step ran. For an
attachment that had never been part of any render pass yet (e.g. a
GL_NONE draw buffer slot read back via an explicit glReadBuffer), the
materialize call was the first thing to touch its resource, and creating
that entry in the UnorderedMap (FastSTL, open-addressing) can rehash and
invalidate every previously-taken pointer into the map - including the
one just cached. The read then saw a stale VK_IMAGE_LAYOUT_UNDEFINED and
silently bailed (via a compiled-out MGLOG_E in release builds), leaving
the client buffer untouched. Reordering so the clear is materialized
first, then the binding resolved, guarantees the pointer reflects the
final resource state. Fixes KHR-GL3{0,1,2,3}.draw_buffers.draw_buffers_1.
2026-08-01 08:09:49 -04:00
BZLZHH 27cdfbc0ca [Fix] (DirectVulkan): advertise GL_ARB_explicit_attrib_location unconditionally
layout(location=N) out qualifiers are fully supported (glslang parses them,
SPIR-V expresses them natively), but the extension string was never
advertised. KHR-GL3{0,1,2}.draw_buffers.draw_buffers_1 builds its MRT
fragment shader with per-attachment layout(location=i) outputs only when
GL_ARB_explicit_attrib_location is reported or the context is >=3.3; below
that it fell back to a single non-indexed `out vec4`, which only ever
targets location 0, leaving every draw buffer past slot 0 unwritten.
2026-08-01 08:09:35 -04:00
BZLZHH 4567c3b468 [Fix] (DirectVulkan): advertise GL_ARB_texture_multisample unconditionally
GL_ARB_texture_multisample was implemented (glTexImage2D/3DMultisample,
GL_TEXTURE_2D_MULTISAMPLE) but never listed in BuildAdvertisedExtensions.
dEQP's GL 3.1 context loader only binds non-core-until-3.2 entry points
when the extension string is present, so glTexImage2DMultisample stayed
a null function pointer and KHR-GL31.texture_size_promotion.functional
crashed on the null call. GL 3.2+ contexts treat it as core and were
unaffected.
2026-08-01 08:09:20 -04:00
BZLZHH d18c6a1bae [Fix] (DirectVulkan): Xlib surface fallback for ICDs without VK_EXT_headless_surface
Real drivers (NVIDIA proprietary Linux) don't implement VK_EXT_headless_surface,
which the pbuffer path required unconditionally, hard-aborting at CreateInstance.
CreateInstance now detects instance-extension support and requests
VK_KHR_xlib_surface instead when headless is unavailable; CreateSurface creates
an unmapped Xlib window purely to obtain a VkSurfaceKHR, then proceeds through
the existing swapchain path unchanged. Shutdown destroys the window it owns.
Lavapipe and other headless-capable ICDs are unaffected.
2026-08-01 06:38:30 -04:00
BZLZHH dd745d7547 [Fix] (MG_State, MG_Impl): GL-order capture for geometry triangle strips
Vulkan transform feedback captures odd strip triangles as (i, i+2, i+1)
while GL table 10.1 decomposes them as (i+1, i, i+2). When the capture
stage is a triangle-strip geometry shader whose EmitVertex/EndPrimitive
sequence is statically knowable (no emission under control flow), link
time extracts the per-invocation strip lengths from the glslang AST, and
EndTransformFeedback rotates each odd triangle's captured vertex records
into GL order in place (bounded by the binding ranges' whole-triangle
capacity; raw input primitives tracked per Begin/End).
KHR-GL33.transform_feedback.geometry passes - the family is 21/21.
2026-08-01 03:17:30 -04:00
BZLZHH 4407be89cd [Fix] (MG_Impl): count multisample texture attachments in GL_SAMPLE_BUFFERS
The draw-framebuffer sample resolver only looked at renderbuffer
attachments, so framebuffers with multisample texture attachments
reported GL_SAMPLE_BUFFERS == 0 and callers took single-sampled paths
(the CTS blit helpers read multisampled attachments based on it).
2026-08-01 02:50:29 -04:00
BZLZHH 0c1a433af6 [Fix] (DirectVulkan): multisample resolve blits; per-buffer blit skip; RGBA-widened renderbuffers
Color blits from a multisampled source now use vkCmdResolveImage (both
blit resolvers carry the image sample count); a buffer named in the blit
mask but absent from either framebuffer skips just that buffer instead
of cancelling the whole blit (GL 4.6 18.3.1); and three-channel color
renderbuffers widen to their RGBA twin exactly like textures, so
renderbuffer<->texture blits of the same GL format see one VkFormat.
framebuffer_blit.multisampled_to_singlesampled_blit_color_config_test
passes - the whole framebuffer_blit family is green.
2026-08-01 02:46:16 -04:00
BZLZHH a2f3efe22c [Feat] (DirectVulkan): combined and scissored blits; cross-format depth-stencil blits
BlitFramebuffer now serves any GL_COLOR/DEPTH/STENCIL mask combination:
the depth/stencil aspects run as per-aspect image copies before the color
path, renderbuffer attachments materialize their pending clears like
texture ones, and the scissor test clips blit writes (destination rect
intersected, source shrunk proportionally). Depth copies between images
of different depth formats (a D24S8 renderbuffer into a
DEPTH_COMPONENT24 texture riding the D32_SFLOAT fallback) round-trip
through the host with a per-texel re-encode; stencil aspects pass
through raw since every packed format encodes S8. scissor_blit and
packed_depth_stencil.blit.* now pass.
2026-08-01 02:19:07 -04:00
BZLZHH b9a15aed61 [Feat] (DirectVulkan, MG_Impl): GPU transform feedback primitive queries
The TF primitive queries now ride VK_QUERY_TYPE_TRANSFORM_FEEDBACK_STREAM_EXT
pools when the device reports transformFeedbackQueries: each captured draw
is wrapped in a slot (shared between both GL targets when active
together), and results sum the (written, needed) pairs -
GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN from the first,
GL_PRIMITIVES_GENERATED from the second. This is exact through geometry
shaders, so KHR-GL33.transform_feedback.query_geometry_* pass; the CPU
accounting delta remains the fallback for backends without the feature.
2026-08-01 02:02:35 -04:00
BZLZHH f748a06632 [Fix] (3rdparty): glslang evaluates defined() expanded from macros
Pulls the glslang change that downgrades the 'defined in macro
expansion' diagnostic to a portability warning with normal evaluation.
KHR-GL33.shaders.preprocessor.conditional_inclusion.basic_2_* pass; the
whole preprocessor family (482 cases, including every negative
invalid_defined_* case) stays green.
2026-08-01 01:54:48 -04:00
BZLZHH 4532cae175 [Feat] (MG_Impl, MG_Util): STENCIL_INDEX8 renderbuffers; report distinct D/S renderbuffers unsupported
GL_STENCIL_INDEX8 becomes a first-class internal format (VK_FORMAT_S8_UINT
backing, metrics, classifiers, converters), so glRenderbufferStorage
accepts it instead of leaving GL_INVALID_ENUM behind. Framebuffer
completeness now also mirrors the renderer's gate for renderbuffers:
distinct depth/stencil renderbuffer attachments (or a renderbuffer
paired with a texture) report GL_FRAMEBUFFER_UNSUPPORTED - the spec only
requires the same-image case - instead of passing completeness and then
failing at draw/clear (verify_mixed_attachments.* now passes).
2026-08-01 01:50:34 -04:00
BZLZHH 107b56d603 [Feat] (DirectVulkan, MG_Impl): occlusion queries via Vulkan query pools
GL_SAMPLES_PASSED / GL_ANY_SAMPLES_PASSED(_CONSERVATIVE) now work: every
app draw between Begin/EndQuery is wrapped in a slot of a host-reset
occlusion query pool (precise counts when occlusionQueryPrecise is
granted), and the result flush ends any active render pass before
submitting, waits, sums the slots and recycles them. ANY_* targets
report the boolean form; GL_QUERY_COUNTER_BITS and GL_CURRENT_QUERY
answer for the occlusion targets, and deleting an active query releases
its slot. Draw-time depth/stencil state also honors attachment absence:
a framebuffer without a depth (stencil) attachment behaves as if that
test always passes, even when a packed depth-stencil image is attached
through only one half (verify_partial_attachments.*).
2026-08-01 01:50:34 -04:00
BZLZHH 22b749dd37 [Fix] (MG_Util, DirectVulkan): canonical depth shadows with upload conversion
Depth textures previously raw-copied whatever the client handed over
into the Vulkan image, so any client format other than the image's exact
texel layout uploaded garbage (float DEPTH_COMPONENT data read as
16-bit words, GL_TEXTURE_1D/2D alike).

The shadow now has a defined canonical layout - unorm16 for
DEPTH_COMPONENT16, a full-scale unorm32 word for the 24/32-bit fixed
depths, float for DEPTH_COMPONENT32F - produced by the pixel-store
unpack converter (new DepthComponent channel mapping + UNorm32
component). GL_DEPTH_COMPONENT client data may also fill packed
depth-stencil internals (stencil half zero). The Vulkan uploader
converts shadow words to the image texel layout per aspect, and
X8_D24_UNORM falls back to D32_SFLOAT where optimal tiling lacks
support (lavapipe). texture_size_promotion.functional and
packed_depth_stencil.verify_copy_tex_image.* now pass.
2026-08-01 01:15:58 -04:00
BZLZHH f0c0211767 [Fix] (DirectVulkan): raw sRGB attachment writes while FRAMEBUFFER_SRGB is off
GL renders into sRGB color attachments RAW when GL_FRAMEBUFFER_SRGB is
disabled (the core-profile default), but Vulkan sRGB attachments always
encode on write - one decode went missing whenever a rendered-into sRGB
texture was sampled again (multisampled sRGB targets in
texture_size_promotion and texture_swizzle idx27/28 ms cases).

Attachment views (textures and renderbuffers) now reinterpret sRGB
images through their UNORM twin while the capability is off, switching
back when enabled: images get MUTABLE_FORMAT, the attachment-view cache
keys the view format, renderbuffers carry a second view, and the render
pass hash includes the capability state. Sampled views keep decoding.
The VkTextureManager.cpp half of this rides with the next commit.
2026-08-01 01:15:57 -04:00
BZLZHH 9ebbb76df1 [Fix] (DirectVulkan): clamp fixed-point ReadPixels to [0,1]
glReadPixels final conversion honors GL_CLAMP_READ_COLOR (default
GL_FIXED_ONLY): fixed-point normalized color buffers clamp to [0,1] on
read - visible for SNORM attachments, whose negative values previously
leaked through (texture_size_promotion SNORM cases). True float formats
stay unclamped unless the mode is GL_TRUE; GetTexImage is unaffected.
2026-08-01 01:15:57 -04:00
BZLZHH 95547ab9ce [Fix] (MG_Util): map GL adjacency primitives to their Vulkan topologies
GL_LINES_ADJACENCY / GL_LINE_STRIP_ADJACENCY / GL_TRIANGLES_ADJACENCY /
GL_TRIANGLE_STRIP_ADJACENCY fell through to the TRIANGLE_LIST default,
so adjacency draws assembled garbage. They now map to the matching
*_WITH_ADJACENCY topologies (adjacency vertices are discarded by Vulkan
when no geometry shader is active, matching GL semantics);
KHR-GL33.primitive_restart.restart_mode passes.
2026-08-01 00:30:01 -04:00
BZLZHH 8eaf2d0069 [Fix] (MG_Impl): create sampler objects at Gen; guard combined-format CopyTexImage
glGenSamplers creates the sampler objects themselves (unlike texture and
buffer names), so glIsSampler must answer GL_TRUE before any bind - the
names now get their state vectors at Gen time (KHR-GL33.api.coverage).

glCopyTexImage2D with a combined DEPTH_STENCIL internalformat now
requires both halves in the read framebuffer and reports
GL_INVALID_OPERATION when only the depth or only the stencil attachment
point is populated (packed_depth_stencil.validate_errors.*).
2026-08-01 00:27:05 -04:00
BZLZHH 54a8609c64 [Feat] (DirectVulkan): depth-stencil GetTexImage
The depth-stencil ReadPixels core (per-aspect copies + CPU repack) is
now shared, and glGetTexImage serves GL_DEPTH_COMPONENT /
GL_DEPTH_STENCIL / GL_STENCIL_INDEX queries of depth textures with it
instead of rejecting every non-color aspect
(packed_depth_stencil.verify_get_tex_image.* now passes).
2026-07-31 18:45:52 -04:00
BZLZHH 1fb0eb0737 [Fix] (MG_Util): GL_FLOAT_32_UNSIGNED_INT_24_8_REV is 8 bytes per pixel
The packed-type size table listed the D32F+S8 client format at 4 bytes,
so every GL_DEPTH32F_STENCIL8 upload copied only half its client data -
the top half of such textures stayed zero (packed_depth_stencil
verify_read_pixels/clear_buffer.depth32f_stencil8 now pass).
2026-07-31 18:43:30 -04:00
BZLZHH 282dd69230 [Feat] (DirectVulkan, MG_Impl): depth-stencil ReadPixels and combined-attachment queries
glReadPixels now serves GL_DEPTH_COMPONENT, GL_DEPTH_STENCIL and
GL_STENCIL_INDEX from the read framebuffer's depth/stencil attachment:
per-aspect vkCmdCopyImageToBuffer copies (4-byte-aligned stencil region)
with CPU repacking into GL_FLOAT / GL_UNSIGNED_SHORT / GL_UNSIGNED_INT /
GL_UNSIGNED_INT_24_8 / GL_FLOAT_32_UNSIGNED_INT_24_8_REV /
GL_UNSIGNED_BYTE layouts, honoring pack state and pixel-pack buffers.

GL_DEPTH_STENCIL_ATTACHMENT parameter queries follow the spec's combined
rules: differing depth/stencil attachment images (or a lone half) fail
with GL_INVALID_OPERATION, as does GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE
on the combined name. packed_depth_stencil.verify_parameters.* and
verify_read_pixels.depth24_stencil8 now pass.
2026-07-31 18:41:08 -04:00
BZLZHH e6ebe7078d [Fix] (MG_Util, MG_State): reject reserved GLSL identifiers glslang accepts
glslang parses "packed" and "row_major" as plain identifiers outside a
layout(...) list and accepts the reserved image*Shadow names outright.
A comment/preprocessor-aware pre-scan in the compile path now fails such
shaders with a proper info log, while layout(packed)/layout(row_major)
qualifier lists stay legal (uniform_block family still passes).
KHR-GL31/32/33.CommonBugs.CommonBug_ReservedNames now pass.
2026-07-31 18:27:11 -04:00
BZLZHH 30a91023f6 [Chore] (tools/cts): pin local runner surface size to 256x256
Without an explicit size dEQP's FboRenderContext sizes the wrapper FBO
to GL_MAX_RENDERBUFFER_SIZE (16384^2 here) and size-derived test
allocations explode - the multisampled depth blit config test alone
needs a 4 GiB depth texture on such a surface.
2026-07-31 18:21:28 -04:00
BZLZHH 47dd8cdc05 [Feat] (MG_Impl): answer format-derived framebuffer attachment queries
glGetFramebufferAttachmentParameteriv (and the DSA variant) now answer
GL_FRAMEBUFFER_ATTACHMENT_RED/GREEN/BLUE/ALPHA/DEPTH/STENCIL_SIZE,
COMPONENT_TYPE and COLOR_ENCODING from the attached image's internal
format, and accept the default-framebuffer attachment names (GL_DEPTH,
GL_STENCIL, GL_FRONT/GL_BACK variants). Querying them with no image
attached reports GL_INVALID_OPERATION per spec instead of
GL_INVALID_ENUM.
2026-07-31 18:21:28 -04:00
BZLZHH 8b36a15fb3 [Fix] (DirectVulkan): repair VK_VERIFY varargs and soften image-creation OOM
VK_VERIFY appended the caller's context format string to the base format
while the context ARGUMENTS expanded before the base arguments, so any
failing VK_VERIFY with context args formatted every conversion from the
wrong slot - the %s for VkResultToString dereferenced an integer arg and
crashed inside the logger. The context line is now its own log call
(XXHASH_VERIFY had the same defect).

vmaCreateImage failure in SyncTextureResource is now a soft failure like
the unsupported-sample-count path: a driver may pass the
vkGetPhysicalDeviceImageFormatProperties pre-check yet still refuse
creation (a 4-sample 16K depth texture on lavapipe is 4 GiB), and a GL
implementation must not abort on that.
2026-07-31 18:21:28 -04:00
BZLZHH 07fa84fb8d [Fix] (MG_State, MG_Impl): defer deletion of the program in use
glDeleteProgram on the current program now only flags it: the name (and
every glGetProgram* query) stays valid until the program stops being
current, at which point UseProgram frees the slot and releases orphaned
attached shaders. Previously the name died immediately, so a second
glDeleteProgram - as issued by common CTS utility teardown - recorded
GL_INVALID_VALUE that poisoned the next test iteration's build
(KHR-GL33.clip_distance.functional now passes its build phase).

glIsProgram/glIsShader piggyback on the same rule: a flagged name is
still a program/shader while it stays GL-visible, which resolves the
long-standing FIXMEs there.
2026-07-31 18:21:14 -04:00
BZLZHH a03817b4ee [Fix] (FastSTL): bump submodule for erase(iterator) out-of-bounds fix
Pulls the FastSTL fix for erase() iterator advancement: erase loops
(pending-clear GC, render-pass eviction, frame-transient drains) no
longer skip elements or walk past the bucket array. Root cause of the
order-dependent CTS batch segfaults (texture_lod_bias_all,
clip_distance.functional after ReadPixels, batch-order aborts).
2026-07-31 18:21:05 -04:00
BZLZHH 641bc0cdd9 [Feat] (MG_Impl): transform feedback primitive queries
glBeginQuery/glEndQuery now accept GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN
and GL_PRIMITIVES_GENERATED. The result comes from CPU-side accounting:
every captured draw adds the primitives it assembles, clamped by the
capture buffers' remaining capacity in whole primitives (a full buffer
stops recording, which is exactly what PRIMITIVES_WRITTEN reports), with
the captured-vertex cursor resetting on glBeginTransformFeedback.

Draws without a geometry stage write exactly what they assemble, so this
is precise for them (KHR-GL33.transform_feedback.query_vertex_* now pass);
geometry amplification is not modelled yet and the query_geometry_*
variants still fail.
2026-07-31 17:40:42 -04:00
BZLZHH c069890ac7 [Feat] (DirectVulkan): GPU transform feedback capture via VK_EXT_transform_feedback
Second stage of GL 3.0 transform feedback: captured draws now write real
data.

- Device setup enables the VK_EXT_transform_feedback feature when present
  and loads the bind/begin/end entry points.
- Captured draws compile an XfbCapture program variant whose last
  vertex-processing stage gets XfbBuffer/XfbStride/Offset decorations from
  the program's resolved varyings (a new spirv-opt pass). A captured
  gl_Position is mirrored into a dedicated output written before every
  OpReturn - or before every OpEmitVertex in a geometry stage - ahead of
  the position fixup, so the captured value is the shader's own pre-remap
  position.
- DrawArrays/DrawElements wrap the draw in Begin/EndTransformFeedbackEXT;
  a small counter buffer resumes the append position across draws within
  one glBeginTransformFeedback (fresh Begin starts at the bound offsets).
- Capture targets are promoted to persistently-mapped host-coherent GPU
  storage (persistent-map storage now also carries the transform feedback
  usage), so MapBuffer/GetBufferSubData read the captured bytes after the
  fence wait glEndTransformFeedback now performs.
- Draw-mode/feedback-mode validation defers to the geometry shader's
  output primitive when one is present, and glGetBooleanv reports
  GL_TRANSFORM_FEEDBACK_ACTIVE/PAUSED so dEQP's per-case state reset can
  unwind an active capture.

KHR-GL33: transform_feedback capture_vertex_*/capture_geometry_*/
discard_*/draw_xfb and clip_distance.coverage now pass; queries
(PRIMITIVES_WRITTEN) and gl_ClipDistance capture remain.
2026-07-31 17:31:41 -04:00
BZLZHH 48dd1c5956 [Feat] (MG_State, MG_Impl): transform feedback state, validation and reflection
First stage of GL 3.0 transform feedback: glTransformFeedbackVaryings /
glGetTransformFeedbackVarying / glBeginTransformFeedback /
glEndTransformFeedback were unimplemented stubs. This adds

- per-program capture state: requested varyings apply on the next link and
  resolve against the last vertex-processing stage's linker objects (with
  gl_Position/gl_PointSize handled as builtins), failing the link on
  unknown or duplicate names or exceeded interleaved/separate limits, with
  offsets and strides computed per GL rules;
- context Begin/End state with the GL 3.3 error semantics: invalid
  primitive modes, redundant Begin/End, missing program or capture-buffer
  bindings, primitive-mode compatibility at draw time, and the
  while-active prohibitions on rebinding capture buffers, switching
  programs, and relinking the captured program;
- GetProgramiv TRANSFORM_FEEDBACK_* queries and a 4-slot bound on indexed
  GL_TRANSFORM_FEEDBACK_BUFFER binding points.

KHR-GL33.transform_feedback api_errors/linking_errors/get_xfb_varying now
pass; GPU-side capture is the remaining stage.
2026-07-31 17:08:37 -04:00
BZLZHH a389477f78 [Fix] (MG_Impl): report INVALID_OPERATION for shader names in program APIs
Program entry points answered GL_INVALID_VALUE whenever the name did not
resolve to a program, including names that exist but belong to a shader
object. Programs and shaders share one name space, so the spec (and
KHR-GL33.get_uniform_tests.get_uniform) requires GL_INVALID_OPERATION for
the shader-name case and GL_INVALID_VALUE only for names GL never handed
out, matching the interface-query helper's existing behavior.
2026-07-31 16:47:45 -04:00
BZLZHH 94a8f1e3f3 [Fix] (ShaderTranspiler): keep declared modern GLSL versions strict
Normalization rewrote every desktop core #version below 400 to 330 (and
400+ to 460), and a failed parse was retried at 460. Together these erased
the declared version's rules: KHR-GL33 negative-compile cases (reserved
names, parenthesized layout-qualifier values in a declared-420 shader,
GLSL 4.5 mix() overloads at 330, precise in struct members) all compiled.

Explicitly declared core versions >= 330 now keep their number, and the
460 retry only fires for sources whose directive carries the normalizer's
own legacy marker - i.e. shaders that declared 110-150 (or nothing), which
is the shader-pack compatibility case the retry exists for. Replaces the
narrower arrays-of-arrays special case.
2026-07-31 16:43:27 -04:00
BZLZHH 45d506545e [Fix] (MG_Util, DirectVulkan): tolerate storage-less attachments in component-size queries
GetComponentSizesForInternalFormat asserted on TextureInternalFormat::Unknown,
which framebuffer-parameter queries legitimately reach for attachments that
have no storage yet (KHR-GL33.packed_depth_stencil.validate_errors.initial_state
aborted there). Answer with all-zero sizes and keep a warning for genuinely
unhandled formats. Also include the image dimensions in the texture
vmaCreateImage failure report.
2026-07-31 16:35:03 -04:00
BZLZHH d9d63c9496 [Fix] (MG_Impl): answer ARB_transform_feedback3 limit queries
The GL CTS queries GL_MAX_TRANSFORM_FEEDBACK_BUFFERS and
GL_MAX_VERTEX_STREAMS before checking whether the extension is advertised
and requires no GL error (desktop drivers all accept these enums). Answer
with the separate-attrib capacity and a single vertex stream; the
transform_feedback3 tests then report NotSupported instead of failing on
GL_INVALID_ENUM.
2026-07-31 16:25:46 -04:00
BZLZHH 92140405c1 [Feat] (DirectVulkan): emulate GL_LINE_LOOP with closed indexed line strips
Vulkan has no LINE_LOOP topology and the frontend used to reject the mode
with GL_INVALID_OPERATION, which is itself non-conformant (several KHR-GL33
transform_feedback tests draw line loops and expect no error). DrawArrays,
DrawElements and DrawElementsBaseVertex now rewrite the draw into an
indexed GL_LINE_STRIP whose synthesized uint32 index list revisits the
first vertex, delivered through the client-memory index path (a new
forceClientMemory flag keeps a bound element-array buffer from hijacking
the synthesized pointer). Entry points without the rewrite degrade to an
open line strip instead of a triangle list.
2026-07-31 16:25:46 -04:00
BZLZHH b9ecfef0b6 [Fix] (DirectVulkan): handle renderbuffer attachments in color blit clears
BlitFramebuffer's color path asserted that the read framebuffer's source
attachment is a texture; a renderbuffer source (packed_depth_stencil.blit
color checks) aborted the process. Materialize pending clears through the
renderbuffer path for both source and destination, as ReadPixels already
does.
2026-07-31 16:15:50 -04:00
BZLZHH fba26ea169 [Fix] (DirectVulkan): round renderbuffer MSAA requests to supported counts
glRenderbufferStorageMultisample accepts any sample count up to MAX_SAMPLES
(including non-powers-of-two like 3) and promises at-least allocation, but
the renderbuffer path required an exact Vulkan sample-count match and failed
on devices like llvmpipe that expose 1x/4x only. Round the request up to a
power of two and then to the nearest count the device supports for the
format, cached per format so per-draw resolution does not re-query the
physical device.

Un-crashes KHR-GL33.packed_depth_stencil.blit.* (2x/3x MSAA renderbuffers).
2026-07-31 16:15:50 -04:00
BZLZHH 93a3b55907 [Feat] (DirectVulkan): upload combined depth-stencil texture data
UploadDirtyMipLevels used to skip D24S8/D32FS8 textures outright, leaving
glTexImage-supplied depth-stencil data unuploaded (KHR-GL33
texture_repeat_mode depth24_stencil8 and texture_swizzle depth-stencil
cases all sampled zeros). De-interleave the shadow's GL wire format into a
depth plane (X8_D24 word / float) and a stencil byte plane and record one
copy per aspect, with cross-conversion when the device backs the texture
with the other depth-stencil format.

Depth32FStencil8's shadow byte size also claimed 16 bytes/texel while the
stored wire format (GL_FLOAT_32_UNSIGNED_INT_24_8_REV) is 8; that mismatch
truncated every upload of it.

Also route a multisample-texture sample-count request through the device's
supported counts (round up, GL promises at-least semantics).
2026-07-31 16:15:50 -04:00
BZLZHH d1487bedf0 [Fix] (DirectVulkan): address array layers in mip upload copies
UploadDirtyMipLevels encoded a texture's GL depth into VkBufferImageCopy
imageExtent.depth with layerCount = 1. For array textures the layers live in
the image's arrayLayers, and extent.depth > 1 is invalid for 2D images - in
practice every layer past the first never received its data.

Route the third dimension into layerCount for 1D/2D/cube array images and
keep imageExtent.depth for genuine 3D images.

Fixes the KHR-GL33.pixelstoragemodes.teximage3d.* failures (110 cases) on
lavapipe.
2026-07-31 16:01:41 -04:00
BZLZHH 1bb736c57e [Fix] (ShaderTranspiler): keep arrays-of-arrays illegal below GLSL 430
The legacy-shader retry that retargets a failed parse to #version 460 also
re-legalized multidimensional arrays, which every desktop driver rejects
below 430 and KHR-GL33.shaders.arrays.invalid.* requires to fail. Skip the
retry when the original failure is glslang's arrays-of-arrays error; other
legacy rescues (e.g. layout(binding=...)) keep working.

KHR-GL33.shaders.arrays.invalid.multidimensional_array* now report the
required compile failure (4 cases).
2026-07-31 15:51:40 -04:00
BZLZHH eb76686c1e [Fix] (DirectVulkan): stop replaying consumed renderbuffer clears mid-pass
A cached RenderPassEntry bakes renderbuffer clear payloads inline into its
pendingClearAttachments, and that list outlives the clear's consumption at
pass begin (loadOp CLEAR). Every subsequent draw that reused the entry while
its pass was still active replayed the stale clear through
vkCmdClearAttachments, wiping the color and depth of everything drawn so far
in the pass.

Texture-keyed clears already re-checked the clear manager before clearing;
do the same for inline renderbuffer payloads: only clear while the
renderbuffer clear is still actually pending, and take the live payload so a
newer glClear's values win.

On lavapipe this takes KHR-GL33.shaders.fragdepth.* from 0/18 to 18/18; the
same defect hit any renderbuffer-FBO case with several draws per pass.
2026-07-31 15:51:40 -04:00
BZLZHH 1a04fb8c0c [Fix] (DirectVulkan): support client-memory indices in DrawElements
With no GL_ELEMENT_ARRAY_BUFFER bound, the IndexBufferView byte offset is a
raw client pointer (desktop drivers accept client-memory indices and the GL
CTS relies on this even in core contexts). UploadAndBindIndexBuffer used to
assert-crash the process there; it now snapshots the client index data into
a transient per-frame slice and binds that, matching how client-memory
vertex attributes are already streamed.

Fixes the process abort in KHR-GL33.transform_feedback.capture_* and every
other mustpass case that draws with client-side index arrays.
2026-07-31 15:10:01 -04:00
BZLZHH 306790ee7c [Feat] (tools/cts): add crash-resuming local-host glcts runner
Local counterpart of run_cts.py for desktop Linux runs: re-invokes glcts
with the not-yet-measured cases after a crash, quarantines timed-out cases
with the dEQP watchdog enabled, and records crashed/hung/unrun lists so a
partial run cannot read as a complete one.
2026-07-31 14:52:38 -04:00
BZLZHH 170ccda3e7 [Feat] (tools/cts): port dEQP MobileGL platform to desktop Linux
Guard the AImageReader window path behind __ANDROID__ and add a
mobilegl-desktop DEQP target so glcts can run against libMobileGL.so on a
Linux host via pbuffer surfaces (VK_EXT_headless_surface).
2026-07-31 14:50:18 -04:00
BZLZHH e86a9bbec5 [Chore] (MG_Backend): restore target GL version to 3.3 2026-07-31 16:45:19 +08:00
swung0x48 c5569e71b3 [Feat] (tools/cts): automate Windows WGL conformance runs 2026-07-30 23:00:13 -04:00
swung0x48 7e8c32a063 [Feat] (MG_Backend, MG_Impl): expose experimental GL 4.6 CTS limits 2026-07-30 23:00:13 -04:00
swung0x48 77bd03d962 [Chore]: remove unnecessary doc 2026-07-30 21:56:17 -04:00
swung0x48 37111ae992 [Perf] (DirectVulkan): snapshot-gated consecutive-draw fast path skips SetupDraw re-resolution 2026-07-30 09:40:54 -04:00
swung0x48 6b0c2a15ab [Perf] (DirectVulkan): reuse unchanged global-UBO slices and skip identical descriptor binds 2026-07-30 08:18:21 -04:00
swung0x48 e9ffd99313 [Perf] (DirectVulkan): bake the attribute location mask and memoize the explicit-LOD eligibility probe 2026-07-30 08:18:20 -04:00
swung0x48 7c01ddea0c [Perf] (DirectVulkan): drop per-draw weak-ptr locks, re-resolves and rebuilt masks from the sampled-texture and vertex paths 2026-07-30 08:01:07 -04:00
swung0x48 2d4d6e9cfb [Perf] (DirectVulkan): skip pending-clear probes through a lock-free empty check 2026-07-30 07:02:03 -04:00
swung0x48 76b8957b99 [Perf] (DirectVulkan): memoize sampled-texture resources across draws 2026-07-30 07:02:02 -04:00
swung0x48 ec685b9fa7 [Perf] (DirectVulkan): memoize resolved vertex-input state on the VAO and dedupe vertex/index binds 2026-07-30 07:02:01 -04:00
swung0x48 a12068df52 [Perf] (DirectVulkan): reuse pipelines across per-chunk buffers and skip redundant pipeline binds 2026-07-30 05:01:46 -04:00
swung0x48 0b344792cc [Fix] (DirectVulkan): stop fence-waiting out-of-band texture uploads; reclaim transients asynchronously 2026-07-30 05:01:46 -04:00
swung0x48 9fa32bdad0 [Fix] (DirectVulkan): declare only the used colour attachment span per subpass
- every render pass declared colorAttachmentCount=8 (the full GL draw-buffer
  slot span) with trailing VK_ATTACHMENT_UNUSED references, and Adreno
  configures its per-pixel render-backend/export path from the DECLARED
  count - so every fragment of every pass paid an 8-render-target export
  cost; this was the bulk of the 1.5x per-pixel gap against
  MobileGlues+ANGLE on the same Qualcomm driver (their subpasses declare
  exactly the used span)
- measured on Adreno 650 / MC 26.2 / 1440x3044: total GPU frame time
  11.9 -> 7.5 ms (-37%, now below ANGLE's 7.87 ms), the single-quad
  swapchain blit pass alone 1.26 -> 0.40 ms, steady in-world FPS 82.8 -> 123
  under the standard cooled-start protocol, matching the
  MobileGlues+ANGLE+system-Vulkan benchmark of 123.8
- trailing UNUSED references are popped before the subpass is built (the
  entry's colorAttachmentCount and every pipeline's colour-blend span follow
  it); interior GL_NONE holes keep their slots so fragment-output locations
  still line up
- the pipeline-side fragmentOutputMask check downgrades from assert to a
  debug log: an output at a location past the trimmed span is discarded,
  which is GL's defined behaviour for a draw buffer set to GL_NONE
2026-07-30 02:45:39 -04:00
swung0x48 a4980f2b56 [Fix] (DirectVulkan): skip redundant per-draw dynamic-state commands
- viewport, scissor, blend constants, depth bias, line width and the six
  stencil parameters were re-emitted unconditionally for EVERY draw (~1500
  vkCmdSet* per frame in MC 26.2, where ANGLE emits a handful), costing CPU
  record time and GPU command-processor work for values that almost never
  change between draws
- a recording-scoped shadow now drops any vkCmdSet* whose values match what
  the command buffer already holds; valid because every PipelineFactory
  pipeline declares the same eight dynamic states, so set values persist
  across those binds
- the shadow resets at every command-buffer (re)begin (dynamic state does
  not survive the boundary) and after binding the blit or depth-mipmap
  pipelines, whose narrower dynamic sets make the untouched states undefined
  and whose raw viewport/scissor writes bypass the shadow
2026-07-30 02:45:07 -04:00
swung0x48 8ca20e28ca [Fix] (DirectVulkan): size texture backings by their defined mip level count
- every non-MSAA texture was allocated with a full mip chain regardless of
  how many levels the GL texture actually defines, so MC's 3044x1440 main
  colour and depth render targets each carried 12 levels where ANGLE
  allocates one; a level-0-only texture now gets a single-level backing and
  upgrades to the full chain exactly once when a second level is first
  defined, through the existing preserve-copy recreation path
- saves a third of the memory of every mip-less texture and keeps
  single-level render targets off the multi-mip image layout entirely, which
  also removes the surface the Adreno 650 implicit-LOD overread workaround
  (ForceExplicitLod0SamplePass) exists to defend
- measured perf-neutral on Adreno 650 / MC 26.2 (the driver keeps full UBWC
  on multi-mip render targets), so this is a memory/robustness fix, not a
  speed one
2026-07-30 02:43:32 -04:00
swung0x48 c353a2055f [Feat] (DirectVulkan): pre-pass command stream for reorderable out-of-pass work
- a draw whose sampled texture needs out-of-pass work (deferred clear
  materialization or a sampled-layout transition) used to end the active
  render pass - a full-target store+reload on a tiler - even when the only
  ordering the work needs is 'before this draw'; MC 26.2 clears an overlay
  texture every frame and samples it mid-pass, splitting the main scene pass
  once per frame for nothing
- every frame slot now carries a second primary command buffer, submitted
  strictly AHEAD of the frame command buffer in the same vkQueueSubmit; when
  the open recording has not referenced the image yet (tracked via a
  recording-generation stamp on the texture resource, advanced on every
  frame-command-buffer begin and stamped at every recorded reference:
  attachments at BeginRenderPass/attachment-write, sampled reads per draw,
  layout transitions), the clear/transition is recorded there and the active
  pass stays open - ANGLE's outside-render-pass command stream, restricted
  to the provably reorderable case
- mid-frame flushes and readback submits close and carry the pre stream with
  the frame buffer (it must never be submitted later than the recording it
  was paired with), retiring both under the same submit index; dropped
  recordings (present suspension, swapchain recreation) abandon it
- MaterializePendingClearForTexture's no-active-render-pass assert now
  applies only to the frame command buffer, since the pre stream records
  while a pass is open on the frame buffer by design
2026-07-30 02:43:13 -04:00
swung0x48 421c20984e [Fix] (DirectVulkan): stop loading and carrying dead default-framebuffer content
- EGL swap semantics make the presented colour buffer's content undefined at
  its next acquire (EGL_BUFFER_DESTROYED, the implementation default) and
  every ancillary depth/stencil buffer's content undefined after ANY swap,
  yet the default-FBO render pass reloaded both with LOAD_OP_LOAD every
  frame; SwapchainObject now tracks per-image content validity (defined when
  a pass stores into the attachment, invalidated at present) and the
  render-pass manager turns an undefined attachment's tile load into
  LOAD_OP_DONT_CARE with initialLayout=UNDEFINED, keyed into both hashes so
  the cached LOAD variants cannot be hit by mistake
- the default framebuffer's depth attachment is now attached ON DEMAND: a
  draw with depth test and stencil test both disabled (GL: a disabled test
  neither reads nor writes its buffer), and no pending depth/stencil clear,
  resolves to a depth-less pass flavour, dropping the D24S8 tile load AND
  store outright - MC 26.2 renders its GUI into its own FBO and only ever
  blits colour to the default framebuffer, so its swapchain pass carried a
  full-screen depth round-trip for nothing
- the flavour only escalates: an active depth-full pass absorbs depth-less
  draws unchanged, while a depth-using draw against a depth-less pass
  resolves to an incompatible entry and splits, its depth loading DONT_CARE
  (the content was undefined all along); the depth-less flavour is folded
  into ComputeHash and the per-draw fast-path memo so the two flavours can
  never alias
2026-07-30 02:40:47 -04:00
140 changed files with 18802 additions and 1320 deletions
+10 -3
View File
@@ -201,6 +201,11 @@ fetch_file_from_mirror() {
return 1
}
# Files no mirror could serve, even after retrying every mirror. Only these fall
# back to Git LFS, so a mirror that served the rest of the case still spares
# GitHub the bandwidth for those files.
mirror_failures=()
fetch_from_mirror() {
mkdir -p "${fixture_dir}"
for file in "${files[@]}"; do
@@ -219,17 +224,19 @@ fetch_from_mirror() {
echo "Mirror did not serve ${name}; trying the next mirror" >&2
done
if [ "${fetched}" -ne 1 ]; then
return 1
mirror_failures+=("${file}")
fi
done
[ "${#mirror_failures[@]}" -eq 0 ]
}
if fetch_from_mirror; then
echo "Fetched trace fixture files for ${case_name} from mirror: ${include}"
else
echo "All mirrors failed for ${case_name}; falling back to Git LFS: ${include}"
fallback_include="$(IFS=,; echo "${mirror_failures[*]}")"
echo "All mirrors failed for ${#mirror_failures[@]} of ${#files[@]} file(s) of ${case_name}; falling back to Git LFS: ${fallback_include}"
git lfs install --local
git lfs pull --include="${include}" --exclude=""
git lfs pull --include="${fallback_include}" --exclude=""
fi
for file in "${files[@]}"; do
+2
View File
@@ -25,3 +25,5 @@ MobileGL/MG*/cmake-build*
/android-plugin/app/src/trace/jniLibs
/android-plugin/local.properties
tools/trace_replay/work/
__pycache__/
*.py[cod]
+2
View File
@@ -191,6 +191,7 @@ set(SOURCE_FILES
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecoratePositionInvariantPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.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/StripNoPerspectivePass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateNoPerspectivePass.cpp
@@ -217,6 +218,7 @@ set(SOURCE_FILES
MobileGL/MG_Impl/GLImpl/Framebuffer/Validators.cpp
MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp
MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp
MobileGL/MG_Impl/GLImpl/Program/GL_ProgramPipeline.cpp
MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp
MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp
MobileGL/MG_Impl/GLImpl/Texture/ProxyTexture.cpp
+1 -1
View File
@@ -14,7 +14,7 @@ namespace MobileGL::MG_Config {
inline const String ProjectName = "MobileGL";
inline const String CoreName = "MobileGL Core";
inline const String CoreVendor = "MobileGL-Dev (BZLZHH, Swung0x48, Tungsten)";
inline const Version CoreVersion = {26, 7, 0, "-dev", VersionType::Development};
inline const Version CoreVersion = {26, 8, 0, "-dev", VersionType::Development};
inline const VersionStringFormatAttrib DefaultVersionStringFormatAttrib = {2, 2, 0, true, true};
inline const Uint64 CacheVersion = 0;
+58
View File
@@ -145,6 +145,10 @@ namespace MobileGL {
GLenum buffer, GLint drawbuffer, const GLfloat* value);
void (*ClearNamedFramebufferfi)(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
void (*ClearNamedFramebufferiv)(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
GLenum buffer, GLint drawbuffer, const GLint* value);
void (*ClearNamedFramebufferuiv)(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
GLenum buffer, GLint drawbuffer, const GLuint* value);
void (*BlitFramebuffer)(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0,
GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter);
void (*BlitNamedFramebuffer)(const SharedPtr<MG_State::GLState::FramebufferObject>& readFramebuffer,
@@ -220,6 +224,31 @@ namespace MobileGL {
// and leave the query readable later.
Bool (*GetQueryResult64)(BackendQueryHandle query, Bool wait, Uint64* outNanoseconds);
void (*DeleteBackendQuery)(BackendQueryHandle query);
// GL_SAMPLES_PASSED occlusion queries (optional; null = unsupported,
// the frontend then rejects the target). Results/deletion flow through
// GetQueryResult64 / DeleteBackendQuery like timer queries.
BackendQueryHandle (*BeginOcclusionQuery)();
void (*EndOcclusionQuery)(BackendQueryHandle query);
// Transform feedback primitive queries backed by real GPU query pools
// (optional; null = frontend falls back to CPU accounting).
BackendQueryHandle (*BeginXfbPrimitivesQuery)(Bool generated);
void (*EndXfbPrimitivesQuery)(BackendQueryHandle query);
// Transform feedback capture spans, for backends whose own GL/ES driver
// performs the capture (DirectGLES). Both optional; null means the backend
// drives capture from its draw recording instead (DirectVulkan). End is
// called while the frontend capture state is still active, so the backend
// can still see the capture program and buffer bindings.
// GL_PATCH_VERTICES; ES 3.2 spells it the same way.
void (*PatchParameteri)(GLenum pname, GLint value);
void (*BeginTransformFeedback)(GLenum primitiveMode);
void (*EndTransformFeedback)();
// ARB_transform_feedback2. A backend that leaves these null keeps the single
// implicit capture span the frontend has always modelled; the frontend state
// (paused flag, per-object bindings) is tracked either way.
void (*PauseTransformFeedback)();
void (*ResumeTransformFeedback)();
void (*BindTransformFeedback)(GLuint name);
void (*DeleteTransformFeedback)(GLuint name);
Int64 (*GetGpuTimestampNs)(); // glGetInteger64v(GL_TIMESTAMP); 0 if unsupported
};
struct GlobalBackendFunctionsTable {
@@ -272,6 +301,13 @@ namespace MobileGL {
Int MaxIntegerSamples = 1;
Int MaxSamples = 1;
Int MaxSampleMaskWords = 1;
// Tessellation limits; defaults are the GL 4.0 core minimums.
Int MaxPatchVertices = 32;
Int MaxTessGenLevel = 64;
// GL_MIN/MAX_PROGRAM_TEXTURE_GATHER_OFFSET. Defaults are the GL 4.0 core
// minimums, which every ES 3.1 driver also guarantees.
Int MinProgramTextureGatherOffset = -8;
Int MaxProgramTextureGatherOffset = 7;
Int MaxTextureImageUnits = 32;
Int MaxVertexTextureImageUnits = 32;
Int MaxComputeTextureImageUnits = 32;
@@ -283,6 +319,8 @@ namespace MobileGL {
Int MaxComputeWorkGroupInvocations = 128;
Int MaxShaderStorageBufferBindings = 8;
Int MaxTextureBufferSize = 65536;
// GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT; 1 means the offset is unconstrained.
Int TextureBufferOffsetAlignment = 1;
Int MaxUniformBufferBindings = 24;
Int MaxUniformBlockSize = 16384;
Int MaxImageUnits = 8;
@@ -300,7 +338,27 @@ namespace MobileGL {
Float ViewportBoundsRangeMin = 0.0f;
Float ViewportBoundsRangeMax = 0.0f;
Int ViewportSubpixelBits = 0;
// GL 4.x fragment-interpolation offset limits. These defaults are the
// core minimums and are replaced by live GLES/Vulkan device limits.
Float MinFragmentInterpolationOffset = -0.5f;
// For four fractional bits the greatest required legal offset is
// 0.5 - 2^-4 = 0.4375 (GL 4.6 table 23.70).
Float MaxFragmentInterpolationOffset = 0.4375f;
Int FragmentInterpolationOffsetBits = 4;
Bool SupportsWideLines = false;
// Whether a framebuffer whose depth and stencil attachments are distinct
// images can be rendered to. GL only requires support when both refer to the
// same image and lets an implementation answer GL_FRAMEBUFFER_UNSUPPORTED
// otherwise, which is what DirectVulkan (one combined attachment) and the
// real ES drivers behind DirectGLES both do. Defaults to true so a backend
// that never sets it keeps the permissive behaviour.
Bool SupportsDistinctDepthStencilAttachments = true;
// Whether attaching a single layer of a 3D or array texture to a framebuffer actually
// renders to that layer. DirectGLES hands the layer straight to
// glFramebufferTextureLayer, so it does; DirectVulkan maps a GL layer onto a Vulkan
// array layer with no notion of a 3D depth slice, so it does not yet. Defaults to false
// so a backend that never sets it gets the conservative answer.
Bool SupportsPerLayerFramebufferAttachment = false;
SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024;
Uint32 SubgroupSize = 0;
Uint32 SubgroupSupportedStages = 0;
@@ -20,6 +20,7 @@
#include <MG_Util/Texture/TextureFormatProcessor.h>
#include <Config.h>
#include <algorithm>
#include <cmath>
#include <format>
namespace MobileGL::MG_Backend::DirectGLES {
@@ -31,8 +32,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
void ClearGLErrors(const MG_External::GLESFunctionsTable& gl) {
if (!gl.glGetError) return;
while (gl.glGetError() != GL_NO_ERROR) {
}
while (gl.glGetError() != GL_NO_ERROR) {}
}
Bool CheckNoGLError(const MG_External::GLESFunctionsTable& gl) {
@@ -76,8 +76,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
Bool IsGLESProbeMultisampleTarget(TextureTarget target) {
return target == TextureTarget::Texture2DMultisample ||
target == TextureTarget::Texture2DMultisampleArray;
return target == TextureTarget::Texture2DMultisample || target == TextureTarget::Texture2DMultisampleArray;
}
GLenum GetFramebufferAttachment(TextureInternalFormat format) {
@@ -114,8 +113,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
GLenum normalizedInternalFormat = glFormat;
GLenum imageFormat = GL_RGBA;
GLenum imageType = GL_UNSIGNED_BYTE;
MG_Util::TextureFormatProcessor::NormalizePixelFormat(
glFormat, PixelFormatNormalizeOptionBit::None, &normalizedInternalFormat, &imageFormat, &imageType);
MG_Util::TextureFormatProcessor::NormalizePixelFormat(glFormat, PixelFormatNormalizeOptionBit::None,
&normalizedInternalFormat, &imageFormat, &imageType);
return imageFormat != GL_RED_INTEGER && imageFormat != GL_RG_INTEGER && imageFormat != GL_RGB_INTEGER &&
imageFormat != GL_RGBA_INTEGER && !MG_Util::IsDepthFormatInternalFormat(format) &&
!MG_Util::IsStencilFormatInternalFormat(format);
@@ -153,9 +152,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
GLESProbeFormatInfo BuildNativeProbeFormatInfo(GLenum requestedInternalFormat) {
GLESProbeFormatInfo info;
info.InternalFormat = requestedInternalFormat;
MG_Util::TextureFormatProcessor::NormalizePixelFormat(
requestedInternalFormat, PixelFormatNormalizeOptionBit::None, nullptr, &info.ImageFormat,
&info.ImageType);
MG_Util::TextureFormatProcessor::NormalizePixelFormat(requestedInternalFormat,
PixelFormatNormalizeOptionBit::None, nullptr,
&info.ImageFormat, &info.ImageType);
return info;
}
@@ -209,6 +208,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (options & PixelFormatNormalizeOptionBit::NoDepthComponent32) {
reasons.push_back("GL_DEPTH_COMPONENT32 native probe failed on OpenGL ES");
}
if (options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget) {
reasons.push_back("no three-channel multisample storage format on OpenGL ES");
}
if (options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget) {
reasons.push_back("EXT_render_snorm not supported");
}
String reason;
for (SizeT i = 0; i < reasons.size(); ++i) {
@@ -226,20 +231,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
return MG_Util::ConvertGLEnumToString(internalFormat);
}
void LogGLESFormatCaveat(TextureInternalFormat logicalFormat,
SizeT targetIndex,
void LogGLESFormatCaveat(TextureInternalFormat logicalFormat, SizeT targetIndex,
const GLESProbeFormatInfo& fallbackInfo) {
MGLOG_D("Caveat: %s %s not fully supported. Reason: %s. Fallback: %s",
GetFormatCapabilityTargetName(targetIndex).c_str(),
MG_Util::ConvertTextureInternalFormatToString(logicalFormat).c_str(),
fallbackInfo.Reason.c_str(),
MG_Util::ConvertTextureInternalFormatToString(logicalFormat).c_str(), fallbackInfo.Reason.c_str(),
ConvertFallbackInternalFormatToString(fallbackInfo.InternalFormat).c_str());
}
Bool BuildFallbackProbeFormatInfo(GLenum requestedInternalFormat,
Flags<PixelFormatNormalizeOptionBit> options,
Bool forced,
GLESProbeFormatInfo& outInfo) {
Bool BuildFallbackProbeFormatInfo(GLenum requestedInternalFormat, Flags<PixelFormatNormalizeOptionBit> options,
Bool forced, GLESProbeFormatInfo& outInfo) {
const Flags<PixelFormatNormalizeOptionBit> applicableOptions =
MG_Util::TextureFormatProcessor::GetApplicablePixelFormatNormalizeOptions(requestedInternalFormat,
options);
@@ -254,8 +255,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
return outInfo.InternalFormat != GL_UNKNOWN_MGL;
}
FormatCapabilityFlags BuildTextureCapsFromProbe(TextureInternalFormat logicalFormat,
TextureTarget target,
FormatCapabilityFlags BuildTextureCapsFromProbe(TextureInternalFormat logicalFormat, TextureTarget target,
Bool renderable) {
FormatCapabilityFlags caps = GetTextureFeatureCaps(logicalFormat, target);
if (renderable) {
@@ -269,16 +269,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
return caps;
}
void AddFullFormatCaps(FormatCapabilityCache& cache,
SizeT targetIndex,
SizeT formatIndex,
void AddFullFormatCaps(FormatCapabilityCache& cache, SizeT targetIndex, SizeT formatIndex,
FormatCapabilityFlags caps) {
cache.FullCaps[targetIndex][formatIndex] |= caps;
}
Bool AddCaveatFormatCaps(FormatCapabilityCache& cache,
SizeT targetIndex,
SizeT formatIndex,
Bool AddCaveatFormatCaps(FormatCapabilityCache& cache, SizeT targetIndex, SizeT formatIndex,
FormatCapabilityFlags caps) {
Bool added = false;
for (FormatCapability capability : kReportedFormatCapabilities) {
@@ -292,8 +288,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
Int GetGLESFormatMaxSamples(const MG_External::GLESCapabilities& capabilities,
TextureInternalFormat logicalFormat,
GLenum imageFormat) {
TextureInternalFormat logicalFormat, GLenum imageFormat) {
const Bool isDepth = MG_Util::IsDepthFormatInternalFormat(logicalFormat);
const Bool isStencil = MG_Util::IsStencilFormatInternalFormat(logicalFormat);
const Bool isInteger = imageFormat == GL_RED_INTEGER || imageFormat == GL_RG_INTEGER ||
@@ -307,10 +302,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
return capabilities.MaxColorTextureSamples;
}
Bool ProbeFramebufferCompletenessForTexture(const MG_External::GLESFunctionsTable& gl,
TextureTarget target,
GLuint texture,
TextureInternalFormat format) {
Bool ProbeFramebufferCompletenessForTexture(const MG_External::GLESFunctionsTable& gl, TextureTarget target,
GLuint texture, TextureInternalFormat format) {
GLuint framebuffer = 0;
GLint prevFramebuffer = 0;
if (!gl.glGenFramebuffers || !gl.glBindFramebuffer || !gl.glCheckFramebufferStatus ||
@@ -356,9 +349,44 @@ namespace MobileGL::MG_Backend::DirectGLES {
return complete;
}
Bool ProbeFramebufferCompletenessForRenderbuffer(const MG_External::GLESFunctionsTable& gl,
GLuint renderbuffer,
TextureInternalFormat format) {
// Whether the driver renders to a framebuffer whose depth and stencil come from
// two different renderbuffers. GL only requires support when both attachments are
// the same image, and ES drivers commonly answer GL_FRAMEBUFFER_UNSUPPORTED here;
// reporting COMPLETE from the frontend and then rendering into a framebuffer the
// driver refuses leaves the results silently empty.
Bool ProbeDistinctDepthStencilAttachments(const MG_External::GLESFunctionsTable& gl) {
if (!gl.glGenFramebuffers || !gl.glBindFramebuffer || !gl.glFramebufferRenderbuffer ||
!gl.glCheckFramebufferStatus || !gl.glDeleteFramebuffers || !gl.glGenRenderbuffers ||
!gl.glBindRenderbuffer || !gl.glRenderbufferStorage || !gl.glDeleteRenderbuffers) {
return true;
}
GLint prevFramebuffer = 0, prevRenderbuffer = 0;
gl.glGetIntegerv(GL_FRAMEBUFFER_BINDING, &prevFramebuffer);
gl.glGetIntegerv(GL_RENDERBUFFER_BINDING, &prevRenderbuffer);
GLuint framebuffer = 0;
GLuint renderbuffers[2] = {0, 0};
gl.glGenFramebuffers(1, &framebuffer);
gl.glGenRenderbuffers(2, renderbuffers);
gl.glBindRenderbuffer(GL_RENDERBUFFER, renderbuffers[0]);
gl.glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, 4, 4);
gl.glBindRenderbuffer(GL_RENDERBUFFER, renderbuffers[1]);
gl.glRenderbufferStorage(GL_RENDERBUFFER, GL_STENCIL_INDEX8, 4, 4);
gl.glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
gl.glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, renderbuffers[0]);
gl.glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER, renderbuffers[1]);
const Bool supported = gl.glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE;
gl.glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(prevFramebuffer));
gl.glBindRenderbuffer(GL_RENDERBUFFER, static_cast<GLuint>(prevRenderbuffer));
gl.glDeleteFramebuffers(1, &framebuffer);
gl.glDeleteRenderbuffers(2, renderbuffers);
return supported;
}
Bool ProbeFramebufferCompletenessForRenderbuffer(const MG_External::GLESFunctionsTable& gl, GLuint renderbuffer,
TextureInternalFormat format) {
GLuint framebuffer = 0;
GLint prevFramebuffer = 0;
if (!gl.glGenFramebuffers || !gl.glBindFramebuffer || !gl.glFramebufferRenderbuffer ||
@@ -425,16 +453,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
break;
case TextureTarget::Texture3D:
gl.glTexImage3D(glTarget, 0, static_cast<GLint>(internalFormat), 2, 2, 2, 0, imageFormat,
imageType, nullptr);
gl.glTexImage3D(glTarget, 0, static_cast<GLint>(internalFormat), 2, 2, 2, 0, imageFormat, imageType,
nullptr);
break;
case TextureTarget::Texture2DArray:
gl.glTexImage3D(glTarget, 0, static_cast<GLint>(internalFormat), 2, 2, 1, 0, imageFormat,
imageType, nullptr);
gl.glTexImage3D(glTarget, 0, static_cast<GLint>(internalFormat), 2, 2, 1, 0, imageFormat, imageType,
nullptr);
break;
case TextureTarget::TextureCubeMapArray:
gl.glTexImage3D(glTarget, 0, static_cast<GLint>(internalFormat), 2, 2, 6, 0, imageFormat,
imageType, nullptr);
gl.glTexImage3D(glTarget, 0, static_cast<GLint>(internalFormat), 2, 2, 6, 0, imageFormat, imageType,
nullptr);
break;
default:
break;
@@ -455,11 +483,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
return created;
}
Bool ProbeRenderbuffer(const MG_External::GLESFunctionsTable& gl,
GLenum internalFormat,
TextureInternalFormat logicalFormat,
Bool multisample,
Int samples) {
Bool ProbeRenderbuffer(const MG_External::GLESFunctionsTable& gl, GLenum internalFormat,
TextureInternalFormat logicalFormat, Bool multisample, Int samples) {
if (!gl.glGenRenderbuffers || !gl.glBindRenderbuffer || !gl.glDeleteRenderbuffers) {
return false;
}
@@ -481,17 +506,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
gl.glRenderbufferStorage(GL_RENDERBUFFER, internalFormat, 1, 1);
}
const Bool created = CheckNoGLError(gl);
const Bool complete = created && ProbeFramebufferCompletenessForRenderbuffer(gl, renderbuffer, logicalFormat);
const Bool complete =
created && ProbeFramebufferCompletenessForRenderbuffer(gl, renderbuffer, logicalFormat);
gl.glBindRenderbuffer(GL_RENDERBUFFER, static_cast<GLuint>(prevRenderbuffer));
gl.glDeleteRenderbuffers(1, &renderbuffer);
ClearGLErrors(gl);
return complete;
}
Vector<Int> ProbeRenderbufferSampleCounts(const MG_External::GLESFunctionsTable& gl,
GLenum internalFormat,
TextureInternalFormat logicalFormat,
Int maxSamples) {
Vector<Int> ProbeRenderbufferSampleCounts(const MG_External::GLESFunctionsTable& gl, GLenum internalFormat,
TextureInternalFormat logicalFormat, Int maxSamples) {
Vector<Int> sampleCounts;
for (Int samples = std::max(maxSamples, 1); samples > 1; samples >>= 1) {
if (ProbeRenderbuffer(gl, internalFormat, logicalFormat, true, samples)) {
@@ -519,20 +543,50 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
const GLESProbeFormatInfo nativeInfo = BuildNativeProbeFormatInfo(requestedInternalFormat);
GLESProbeFormatInfo fallbackInfo;
const Bool hasForcedFallback =
BuildFallbackProbeFormatInfo(requestedInternalFormat, forcedOptions, true, fallbackInfo);
if (!hasForcedFallback) {
BuildFallbackProbeFormatInfo(requestedInternalFormat, driverOptions, false, fallbackInfo);
GLESProbeFormatInfo outerFallbackInfo;
const Bool outerHasForcedFallback =
BuildFallbackProbeFormatInfo(requestedInternalFormat, forcedOptions, true, outerFallbackInfo);
if (!outerHasForcedFallback) {
BuildFallbackProbeFormatInfo(requestedInternalFormat, driverOptions, false, outerFallbackInfo);
}
for (SizeT targetIndex = 0; targetIndex < kFormatCapabilityTextureTargetCount; ++targetIndex) {
const auto target = static_cast<TextureTarget>(targetIndex);
// A multisample texture can only ever be rendered into, so its storage format
// has to stay colour-renderable; the ordinary fallback for a three-channel
// format is a three-channel one, which ES accepts as a texture but rejects as
// multisample storage. Recompute the fallback per target so those formats get
// widened here and nowhere else.
Flags<PixelFormatNormalizeOptionBit> targetOptions;
if (IsGLESProbeMultisampleTarget(target)) {
targetOptions |= PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget;
if (!capabilities.SupportsRenderSnorm || !capabilities.SupportsNorm16Texture) {
targetOptions |= PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget;
}
}
GLESProbeFormatInfo fallbackInfo = outerFallbackInfo;
Bool hasForcedFallback = outerHasForcedFallback;
if (targetOptions) {
hasForcedFallback = BuildFallbackProbeFormatInfo(
requestedInternalFormat, forcedOptions | targetOptions, true, fallbackInfo);
if (!hasForcedFallback) {
BuildFallbackProbeFormatInfo(requestedInternalFormat, driverOptions | targetOptions, false,
fallbackInfo);
}
}
// 1D, 1D-array and rectangle textures live on an ES target (see
// TextureImpl::MapToBackendTextureTarget), so they have to be probed there too -
// probing the desktop-only target itself always failed, which left those slots
// of the cache empty and stopped any fallback format from being selected for
// them (a GL_DEPTH_COMPONENT32 1D texture then got no storage at all).
const TextureTarget probeTarget = TextureImpl::MapToBackendTextureTarget(target);
Bool shouldProbeFallback = hasForcedFallback;
if (!hasForcedFallback) {
Bool nativeRenderable = false;
const Bool nativeCreated =
ProbeTexture(gl, target, nativeInfo.InternalFormat, nativeInfo.ImageFormat,
ProbeTexture(gl, probeTarget, nativeInfo.InternalFormat, nativeInfo.ImageFormat,
nativeInfo.ImageType, logicalFormat, &nativeRenderable);
if (nativeCreated) {
AddFullFormatCaps(cache, targetIndex, formatIndex,
@@ -547,12 +601,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (shouldProbeFallback && fallbackInfo.InternalFormat != GL_UNKNOWN_MGL) {
Bool fallbackRenderable = false;
const Bool fallbackCreated =
ProbeTexture(gl, target, fallbackInfo.InternalFormat, fallbackInfo.ImageFormat,
ProbeTexture(gl, probeTarget, fallbackInfo.InternalFormat, fallbackInfo.ImageFormat,
fallbackInfo.ImageType, logicalFormat, &fallbackRenderable);
if (fallbackCreated) {
if (AddCaveatFormatCaps(cache, targetIndex, formatIndex,
BuildTextureCapsFromProbe(logicalFormat, target,
fallbackRenderable))) {
if (AddCaveatFormatCaps(
cache, targetIndex, formatIndex,
BuildTextureCapsFromProbe(logicalFormat, target, fallbackRenderable))) {
LogGLESFormatCaveat(logicalFormat, targetIndex, fallbackInfo);
}
if (IsGLESProbeMultisampleTarget(target)) {
@@ -563,8 +617,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
const SizeT renderbufferTargetIndex = GetRenderbufferFormatCapabilityTargetIndex();
Bool shouldProbeFallbackRenderbuffer = hasForcedFallback;
if (!hasForcedFallback) {
Bool shouldProbeFallbackRenderbuffer = outerHasForcedFallback;
if (!outerHasForcedFallback) {
const Bool nativeRenderbufferComplete =
ProbeRenderbuffer(gl, nativeInfo.InternalFormat, logicalFormat, false, 1);
if (nativeRenderbufferComplete) {
@@ -578,16 +632,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
shouldProbeFallbackRenderbuffer = true;
}
}
if (shouldProbeFallbackRenderbuffer && fallbackInfo.InternalFormat != GL_UNKNOWN_MGL &&
ProbeRenderbuffer(gl, fallbackInfo.InternalFormat, logicalFormat, false, 1)) {
if (shouldProbeFallbackRenderbuffer && outerFallbackInfo.InternalFormat != GL_UNKNOWN_MGL &&
ProbeRenderbuffer(gl, outerFallbackInfo.InternalFormat, logicalFormat, false, 1)) {
if (AddCaveatFormatCaps(cache, renderbufferTargetIndex, formatIndex,
GetRenderbufferFeatureCaps(logicalFormat))) {
LogGLESFormatCaveat(logicalFormat, renderbufferTargetIndex, fallbackInfo);
LogGLESFormatCaveat(logicalFormat, renderbufferTargetIndex, outerFallbackInfo);
}
const Int maxSamples =
GetGLESFormatMaxSamples(capabilities, logicalFormat, fallbackInfo.ImageFormat);
GetGLESFormatMaxSamples(capabilities, logicalFormat, outerFallbackInfo.ImageFormat);
cache.SampleCounts[renderbufferTargetIndex][formatIndex] =
ProbeRenderbufferSampleCounts(gl, fallbackInfo.InternalFormat, logicalFormat, maxSamples);
ProbeRenderbufferSampleCounts(gl, outerFallbackInfo.InternalFormat, logicalFormat, maxSamples);
}
}
}
@@ -603,7 +657,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
.ExtraVendor = Nullopt, // Extra vendor
.RendererGLInfo =
{
.TargetGLVersion = {3, 3, 0}, // Target OpenGL Version
.TargetGLVersion = {4, 0, 0}, // GL target version
.TargetGLSLVersion = {4, 6, 0}, // Target Shading Language Version
// Baseline advertisement (no timer queries / anisotropy yet); reconciled
// once the ES capabilities exist, see UpdateAdvertisedCapabilityExtensions.
@@ -634,8 +688,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
} // namespace
void PopulateFormatCapabilities(const MG_External::GLESFunctionsTable& gl,
const MG_External::GLESCapabilities& capabilities,
FormatCapabilityCache& cache) {
const MG_External::GLESCapabilities& capabilities, FormatCapabilityCache& cache) {
PopulateFormatCapabilitiesImpl(gl, capabilities, cache);
}
@@ -699,10 +752,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
return false;
}
if ((handle.Backend != WindowBackend::Android &&
handle.Backend != WindowBackend::X11 &&
handle.Backend != WindowBackend::MetalLayer &&
handle.Backend != WindowBackend::Win32) ||
if ((handle.Backend != WindowBackend::Android && handle.Backend != WindowBackend::X11 &&
handle.Backend != WindowBackend::MetalLayer && handle.Backend != WindowBackend::Win32) ||
!handle.Handle) {
MGLOG_E("DirectGLES backend only supports Android, X11, CAMetalLayer, and Win32 native windows");
return false;
@@ -821,17 +872,25 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported) {
Vector<GLExtension> extensions = {V_OpenGL30, V_OpenGL31, V_OpenGL32,
V_OpenGL33, E_GL_ARB_draw_buffers_blend, E_GL_ARB_compute_shader,
E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object,
E_GL_EXT_framebuffer_object, E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage,
E_GL_ARB_texture_storage, E_GL_ARB_texture_storage_multisample,
E_GL_ARB_clear_texture, E_GL_ARB_direct_state_access,
E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters,
E_GL_ARB_shader_draw_parameters, 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};
Vector<GLExtension> extensions = {
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, E_GL_ARB_draw_buffers_blend,
E_GL_ARB_compute_shader, E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_EXT_framebuffer_object,
E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage, E_GL_ARB_texture_storage,
E_GL_ARB_texture_storage_multisample, E_GL_ARB_clear_texture, E_GL_ARB_direct_state_access,
E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters, E_GL_ARB_shader_draw_parameters,
E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind, E_GL_ARB_shading_language_420pack,
E_GL_ARB_vertex_attrib_binding,
// Both are core from GL 3.2/3.3 on and implemented here for
// every advertised version, but an app targeting 3.0/3.1
// only reaches them through the extension string - the CTS
// picks a whole different shader for draw_buffers without
// explicit_attrib_location. DirectVulkan advertises both.
E_GL_ARB_explicit_attrib_location, E_GL_ARB_texture_multisample, E_GL_ARB_shader_image_size,
// 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};
// Only advertised when the device driver actually has usable timer queries
// (GL_EXT_disjoint_timer_query plus its entry points) and the
// MOBILEGL_DISABLE_TIMERQUERY escape hatch is off.
@@ -905,6 +964,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
funcsTable.GL.ClearBufferuiv = ClearBufferuiv;
funcsTable.GL.ClearBufferiv = ClearBufferiv;
funcsTable.GL.ClearNamedFramebufferfv = ClearNamedFramebufferfv;
funcsTable.GL.ClearNamedFramebufferiv = ClearNamedFramebufferiv;
funcsTable.GL.ClearNamedFramebufferuiv = ClearNamedFramebufferuiv;
funcsTable.GL.ClearNamedFramebufferfi = ClearNamedFramebufferfi;
funcsTable.GL.BlitFramebuffer = BlitFramebuffer;
funcsTable.GL.BlitNamedFramebuffer = BlitNamedFramebuffer;
@@ -934,11 +995,30 @@ namespace MobileGL::MG_Backend::DirectGLES {
funcsTable.GL.BeginTimeElapsedQuery = BeginTimeElapsedQuery;
funcsTable.GL.EndTimeElapsedQuery = EndTimeElapsedQuery;
funcsTable.GL.QueryCounterTimestamp = QueryCounterTimestamp;
funcsTable.GL.IsQueryResultAvailable = IsQueryResultAvailable;
funcsTable.GL.GetQueryResult64 = GetQueryResult64;
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
funcsTable.GL.GetGpuTimestampNs = GetGpuTimestampNs;
}
// Occlusion queries are core ES3 (independent of MOBILEGL_DISABLE_TIMERQUERY)
// and share the handle-based result/delete entries, which must exist even
// when the timer-query group above is disabled.
funcsTable.GL.BeginOcclusionQuery = BeginOcclusionQuery;
funcsTable.GL.EndOcclusionQuery = EndOcclusionQuery;
// Real driver primitive counters: the frontend's CPU accounting cannot see a
// geometry shader's amplification.
funcsTable.GL.BeginXfbPrimitivesQuery = BeginXfbPrimitivesQuery;
funcsTable.GL.EndXfbPrimitivesQuery = EndXfbPrimitivesQuery;
funcsTable.GL.IsQueryResultAvailable = IsQueryResultAvailable;
funcsTable.GL.GetQueryResult64 = GetQueryResult64;
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
// Transform feedback is captured by the real ES driver rather than
// reconstructed from the draw recording, so the frontend has to hand the
// span boundaries over.
funcsTable.GL.PatchParameteri = DirectGLES::PatchParameteri;
funcsTable.GL.BeginTransformFeedback = XfbImpl::BeginTransformFeedback;
funcsTable.GL.EndTransformFeedback = XfbImpl::EndTransformFeedback;
funcsTable.GL.PauseTransformFeedback = XfbImpl::PauseTransformFeedback;
funcsTable.GL.ResumeTransformFeedback = XfbImpl::ResumeTransformFeedback;
funcsTable.GL.BindTransformFeedback = XfbImpl::BindTransformFeedback;
funcsTable.GL.DeleteTransformFeedback = XfbImpl::DeleteTransformFeedback;
funcsTableInitialized = true;
}
return funcsTable;
@@ -948,8 +1028,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
return m_dynamicParameters;
}
void BackendObject_DirectGLES::ApplyGLESCapabilitiesForTesting(
const MG_External::GLESCapabilities& capabilities) {
void BackendObject_DirectGLES::ApplyGLESCapabilitiesForTesting(const MG_External::GLESCapabilities& capabilities) {
m_GLESCapabilities = capabilities;
UpdateDynamicBackendParameters();
}
@@ -979,6 +1058,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
m_dynamicParameters.MaxIntegerSamples = m_GLESCapabilities.MaxIntegerSamples;
m_dynamicParameters.MaxSamples = m_GLESCapabilities.MaxSamples;
m_dynamicParameters.MaxSampleMaskWords = m_GLESCapabilities.MaxSampleMaskWords;
m_dynamicParameters.MaxPatchVertices = m_GLESCapabilities.MaxPatchVertices;
m_dynamicParameters.MaxTessGenLevel = m_GLESCapabilities.MaxTessGenLevel;
m_dynamicParameters.MinProgramTextureGatherOffset = m_GLESCapabilities.MinProgramTextureGatherOffset;
m_dynamicParameters.MaxProgramTextureGatherOffset = m_GLESCapabilities.MaxProgramTextureGatherOffset;
// Clamp the advertised sampler limits the same way the DirectVulkan backend does: per-stage
// GL_MAX_TEXTURE_IMAGE_UNITS must never exceed host-side fixed arrays sized off it (e.g.
// Minecraft's 128-entry Blaze3D GlStateManager.TEXTURES[], iterated by Iris), and the combined
@@ -1006,10 +1089,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
m_dynamicParameters.MaxComputeWorkGroupInvocations = m_GLESCapabilities.MaxComputeWorkGroupInvocations;
m_dynamicParameters.MaxShaderStorageBufferBindings = m_GLESCapabilities.MaxShaderStorageBufferBindings;
m_dynamicParameters.MaxTextureBufferSize = m_GLESCapabilities.MaxTextureBufferSize;
m_dynamicParameters.TextureBufferOffsetAlignment = m_GLESCapabilities.TextureBufferOffsetAlignment;
m_dynamicParameters.MaxUniformBufferBindings = m_GLESCapabilities.MaxUniformBufferBindings;
m_dynamicParameters.MaxUniformBlockSize = m_GLESCapabilities.MaxUniformBlockSize;
const Int maxSupportedTextureUnits =
static_cast<Int>(MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS);
const Int maxSupportedTextureUnits = static_cast<Int>(MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS);
m_dynamicParameters.MaxImageUnits =
std::max(std::min(m_GLESCapabilities.MaxImageUnits, maxSupportedTextureUnits), 0);
m_dynamicParameters.MaxCombinedImageUniforms = std::max(m_GLESCapabilities.MaxCombinedImageUniforms, 0);
@@ -1017,14 +1100,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
return std::min({std::max(stageLimit, 0), m_dynamicParameters.MaxImageUnits,
m_dynamicParameters.MaxCombinedImageUniforms});
};
m_dynamicParameters.MaxVertexImageUniforms =
clampStageImageUniforms(m_GLESCapabilities.MaxVertexImageUniforms);
m_dynamicParameters.MaxVertexImageUniforms = clampStageImageUniforms(m_GLESCapabilities.MaxVertexImageUniforms);
m_dynamicParameters.MaxGeometryImageUniforms =
clampStageImageUniforms(m_GLESCapabilities.MaxGeometryImageUniforms);
m_dynamicParameters.MaxFragmentImageUniforms =
clampStageImageUniforms(m_GLESCapabilities.MaxFragmentImageUniforms);
m_dynamicParameters.MaxComputeImageUniforms =
clampStageImageUniforms(m_GLESCapabilities.MaxComputeImageUniforms);
m_dynamicParameters.SupportsDistinctDepthStencilAttachments =
ProbeDistinctDepthStencilAttachments(DirectGLES::g_GLESFuncs);
// SyncAttachmentObject routes a layered upload target to glFramebufferTextureLayer with the
// attachment's layer passed through, so this backend really does render to the layer it was
// given - provided the driver resolved the entry point at all.
m_dynamicParameters.SupportsPerLayerFramebufferAttachment =
DirectGLES::g_GLESFuncs.glFramebufferTextureLayer != nullptr;
m_dynamicParameters.MaxDrawBuffers = m_GLESCapabilities.MaxDrawBuffers;
m_dynamicParameters.MaxColorAttachments = m_GLESCapabilities.MaxColorAttachments;
m_dynamicParameters.MaxClipDistances = m_GLESCapabilities.MaxClipDistances;
@@ -1034,13 +1123,29 @@ namespace MobileGL::MG_Backend::DirectGLES {
m_dynamicParameters.ViewportBoundsRangeMin = m_GLESCapabilities.ViewportBoundsRangeMin;
m_dynamicParameters.ViewportBoundsRangeMax = m_GLESCapabilities.ViewportBoundsRangeMax;
m_dynamicParameters.ViewportSubpixelBits = m_GLESCapabilities.ViewportSubpixelBits;
m_dynamicParameters.MinFragmentInterpolationOffset =
std::isfinite(m_GLESCapabilities.MinFragmentInterpolationOffset) &&
m_GLESCapabilities.MinFragmentInterpolationOffset <= -0.5f
? m_GLESCapabilities.MinFragmentInterpolationOffset
: -0.5f;
m_dynamicParameters.MaxFragmentInterpolationOffset = 0.4375f;
m_dynamicParameters.FragmentInterpolationOffsetBits = 4;
if (m_GLESCapabilities.FragmentInterpolationOffsetBits >= 4 &&
std::isfinite(m_GLESCapabilities.MaxFragmentInterpolationOffset)) {
const Float requiredMaxOffset =
0.5f - std::ldexp(1.0f, -m_GLESCapabilities.FragmentInterpolationOffsetBits);
if (m_GLESCapabilities.MaxFragmentInterpolationOffset >= requiredMaxOffset) {
m_dynamicParameters.MaxFragmentInterpolationOffset = m_GLESCapabilities.MaxFragmentInterpolationOffset;
m_dynamicParameters.FragmentInterpolationOffsetBits =
m_GLESCapabilities.FragmentInterpolationOffsetBits;
}
}
m_dynamicParameters.SupportsWideLines =
m_GLESCapabilities.AliasedLineWidthRangeMax > 1.0f || m_GLESCapabilities.SmoothLineWidthRangeMax > 1.0f;
const auto containsAny = [](const String& haystack, std::initializer_list<const char*> needles) {
return std::any_of(needles.begin(), needles.end(), [&](const char* needle) {
return haystack.find(needle) != String::npos;
});
return std::any_of(needles.begin(), needles.end(),
[&](const char* needle) { return haystack.find(needle) != String::npos; });
};
const String vendorAndRenderer =
m_GLESCapabilities.GLESVendorString + " " + m_GLESCapabilities.GLESRendererString;
File diff suppressed because it is too large Load Diff
@@ -59,6 +59,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
GLenum buffer, GLint drawbuffer, const GLfloat* value);
void ClearNamedFramebufferfi(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
void ClearNamedFramebufferiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
GLenum buffer, GLint drawbuffer, const GLint* value);
void ClearNamedFramebufferuiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
GLenum buffer, GLint drawbuffer, const GLuint* value);
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1,
GLint dstY1, GLbitfield mask, GLenum filter);
void BlitNamedFramebuffer(const SharedPtr<MG_State::GLState::FramebufferObject>& readFramebuffer,
@@ -130,6 +134,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
BackendQueryHandle BeginTimeElapsedQuery();
void EndTimeElapsedQuery(BackendQueryHandle query);
BackendQueryHandle QueryCounterTimestamp();
// GL_ANY_SAMPLES_PASSED(_CONSERVATIVE) occlusion queries: core ES3, independent of
// GL_EXT_disjoint_timer_query and of MOBILEGL_DISABLE_TIMERQUERY. Results/deletion
// flow through GetQueryResult64/DeleteBackendQuery like the timer queries above.
BackendQueryHandle BeginOcclusionQuery();
void EndOcclusionQuery(BackendQueryHandle query);
// GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN / GL_PRIMITIVES_GENERATED, also core ES
// (GL_PRIMITIVES_GENERATED from ES 3.2 on). Null when the target is unavailable, in
// which case the frontend falls back to counting primitives from the draw calls.
BackendQueryHandle BeginXfbPrimitivesQuery(Bool generated);
void EndXfbPrimitivesQuery(BackendQueryHandle query);
Bool IsQueryResultAvailable(BackendQueryHandle query);
// Returns true when a final value landed in *outNanoseconds (a zero for
// null or stale-generation handles IS final: the frontend may cache it
@@ -154,6 +168,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
void SetGLESCapabilities(const MG_External::GLESCapabilities& capabilities);
void DestroyEGLContext();
// Transform feedback capture spans, performed by the real ES driver. The
// capture set is declared on the backend program at link time; the driver-side
// begin is deferred to the first draw of the span (ES needs the capturing
// program current and the capture buffers bound), and the end also mirrors the
// captured bytes back into the frontend buffer shadows.
void PatchParameteri(GLenum pname, GLint value);
namespace XfbImpl {
Bool AreTransformFeedbacksSupported();
void BeginTransformFeedback(GLenum primitiveMode);
void EndTransformFeedback();
void PauseTransformFeedback();
void ResumeTransformFeedback();
void BindTransformFeedback(GLuint name);
void DeleteTransformFeedback(GLuint name);
void OnBackendContextDestroyed();
} // namespace XfbImpl
extern MG_External::EGLFunctionsTable g_EGLFuncs;
extern MG_External::GLESFunctionsTable g_GLESFuncs;
extern MG_External::GLESCapabilities g_GLESCapabilities;
+150 -7
View File
@@ -630,6 +630,32 @@ namespace MobileGL::MG_Backend::DirectGLES {
resource->syncedChangeSerial = bufferObject.GetChangeSerial();
}
// A shader wrote this buffer through a storage/atomic-counter binding, so the ES
// driver's copy is ahead of the frontend shadow. Pull the whole thing back so
// MapBuffer/GetBufferSubData/CopyBufferSubData see the real results.
void Ops_ReadbackFromGpu(BufferObject& bufferObject) {
auto* resource = ResourceOf(bufferObject);
if (!resource || resource->id == 0 || !resource->storageInitialized) return;
if (resource->persistentMapped) return; // shadow already IS the GPU storage
if (!CanTouchGLNow() || resource->contextGeneration != g_bufferContextGeneration) return;
if (!g_GLESFuncs.glMapBufferRange || !g_GLESFuncs.glUnmapBuffer) return;
const SizeT size = std::min<SizeT>(bufferObject.GetSize(), resource->storageSize);
if (size == 0) return;
BindBufferId(TempBufferTarget, resource->id);
void* mapped = g_GLESFuncs.glMapBufferRange(TempBufferTarget, 0, static_cast<GLsizeiptr>(size),
GL_MAP_READ_BIT);
if (mapped == nullptr) {
MGLOG_E("Ops_ReadbackFromGpu: glMapBufferRange(read) failed for buffer %u", resource->id);
return;
}
bufferObject.WritebackFromBackend({mapped, size}, 0);
g_GLESFuncs.glUnmapBuffer(TempBufferTarget);
// The shadow now matches the backend byte for byte; without this the next
// draw would see a newer change serial and re-upload the readback over it.
resource->syncedChangeSerial = bufferObject.GetChangeSerial();
}
void Ops_OnDestroy(SharedPtr<BackendBufferResource>&& resource) {
if (!resource) return;
auto* glesResource = static_cast<GLESBufferResource*>(resource.get());
@@ -662,6 +688,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
.FlushMappedRange = Ops_FlushMappedRange,
.OnDestroy = Ops_OnDestroy,
.AcquirePersistentMap = Ops_AcquirePersistentMap,
.ReadbackFromGpu = Ops_ReadbackFromGpu,
};
} // namespace
@@ -2205,7 +2232,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
"ID: %u, buffer ID: %u, buffer size: %zu, format: %s",
m_backendTextureId, backendId, buffer->GetSize(),
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(
[file = __FILE__, line = __LINE__, func = __func__, glInternalFormat, backendId](GLenum err) {
MGLOG_D("%s(%s:%d) glTexBuffer(format=%s, buffer=%u) ES error: %s",
@@ -2405,7 +2449,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
MGLOG_D("%s(%s:%d) ES error %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str());
});
const auto& swizzleParams = stateTextureObject->GetAllSwizzleParams();
// A three-channel format widened to four for a multisample target (see
// NormalizePixelFormat) gains an alpha channel the frontend format does not have, and
// whatever the draw that filled it wrote there is not what GL would report: a format
// without alpha reads back as 1.0. Answer the ALPHA swizzle source with ONE so the
// promotion stays invisible, composed with the swizzle the application asked for.
Vec4<TextureSwizzleParam> swizzleParams = stateTextureObject->GetAllSwizzleParams();
if (TextureImpl::BackendTextureFormatAddsAlpha(stateTextureObject->GetFormat(), targetInternal)) {
for (SizeT channel = 0; channel < 4; ++channel) {
if (swizzleParams[channel] == TextureSwizzleParam::Alpha) {
swizzleParams[channel] = TextureSwizzleParam::One;
}
}
}
if (swizzleParams != m_cacheSwizzleParams) {
#define SYNC_TEX_SWIZZLE_PARAM_IF_CHANGED(func, glEnum) \
if (m_cacheSwizzleParams.func != swizzleParams.func) { \
@@ -2423,7 +2479,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
});
}
if (!isMultisampleTarget && m_cacheBorderColor != stateTextureObject->GetBorderColor()) {
// GL_TEXTURE_BORDER_COLOR needs ES 3.2 or EXT/OES_texture_border_clamp; on a driver
// without it every such call is INVALID_ENUM, so the parameter is simply not synced.
if (!isMultisampleTarget && g_GLESCapabilities.SupportsTextureBorderClamp &&
m_cacheBorderColor != stateTextureObject->GetBorderColor()) {
const auto& borderColor = stateTextureObject->GetBorderColor();
GLfloat borderColorArray[4] = {borderColor.x(), borderColor.y(), borderColor.z(), borderColor.w()};
g_GLESFuncs.glTexParameterfv(target, GL_TEXTURE_BORDER_COLOR, borderColorArray);
@@ -2679,6 +2738,31 @@ namespace MobileGL::MG_Backend::DirectGLES {
return false;
}
Bool IsFixedPointFallbackReadAttachment() {
const auto& readFBO =
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();
if (!readFBO) {
return false;
}
const auto readBuffer = readFBO->GetReadBuffer();
if (readBuffer < FramebufferAttachmentType::Color0 || readBuffer > FramebufferAttachmentType::Color31) {
return false;
}
// Any signed-normalized attachment, not just the ones currently substituted:
// ES has no GL_CLAMP_READ_COLOR at all, so even a natively stored SNORM buffer
// hands back the negative half that desktop GL clamps away.
const auto& attachmentObject = readFBO->GetAttachment(readBuffer);
if (attachmentObject.IsTexture()) {
const auto& textureObject = attachmentObject.GetTexture();
return textureObject && IsSnormFormat(textureObject->GetFormat());
}
if (attachmentObject.IsRenderbuffer()) {
const auto& renderbufferObject = attachmentObject.GetRenderbuffer();
return renderbufferObject && IsSnormFormat(renderbufferObject->GetInternalFormat());
}
return false;
}
void BackendFramebufferObject::SyncReadBufferToBackend(
const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject) {
if (!stateFBOObject) {
@@ -3181,6 +3265,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
namespace PrgramImpl {
Uint32 g_snormFallbackClampOutputMask = 0;
Uint g_fragColorBroadcastCount = 1;
Uint32 g_unormFallbackClampOutputMask = 0;
Uint g_lastUsedBackendProgramId = 0;
StateBackendObjectRegistry<MG_State::GLState::ProgramObject, BackendProgramObjectImpl> g_backendProgramObjects;
@@ -3232,8 +3317,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
MGLOG_D("Syncing program to backend. State program ID: %u, Backend ID: %u",
stateProgramObject->GetExternalIndex(), m_backendProgramId);
m_backendProgramUsable = true;
m_snormFallbackClampOutputMask = g_snormFallbackClampOutputMask;
m_unormFallbackClampOutputMask = g_unormFallbackClampOutputMask;
m_fragColorBroadcastCount = g_fragColorBroadcastCount;
// Detach all existing shaders
GLint attachedCount = 0;
@@ -3316,6 +3403,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
effectiveSpirv = &noperspectiveSpirv;
}
// ES has no rectangle sampler, and SPIRV-Cross refuses the whole module rather
// than approximating one. The shared pass turns the type into the 2D one and
// divides the coordinate of every normalized-coordinate lookup by the texture
// size, which is the whole of the difference between the two.
Vector<unsigned int> rectLoweredSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::LowerRectImages(*effectiveSpirv, rectLoweredSpirv) &&
!rectLoweredSpirv.empty()) {
effectiveSpirv = &rectLoweredSpirv;
}
MG_Util::ShaderTranspiler::SpvcSession spvcSession(*effectiveSpirv,
MG_Util::ShaderTranspiler::SessionUsageBit::Transpile);
@@ -3338,6 +3435,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
r.log += spvcSession.GetLastErrorString();
r.errc = -5;
MGLOG_E("%s", r.log.c_str());
m_backendProgramUsable = false;
continue;
}
@@ -3347,6 +3445,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
source = RemoveLayoutBinding(source);
source = ProcessOutColorLocations(source);
source = ForceFlatIntegerVaryings(source, glShaderType);
source = BroadcastLegacyFragColor(std::move(source), glShaderType, m_fragColorBroadcastCount);
source = EmulateTextureLodBias(source);
source = EmulateBaseInstanceInVertexShader(std::move(source), glShaderType);
source = PromoteDrawParameterGlobalsToUniforms(std::move(source), glShaderType);
source = ForceSupporterOutput(source);
@@ -3377,6 +3477,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
Vector<GLchar> log(logLength);
g_GLESFuncs.glGetShaderInfoLog(backendShaderId, logLength, nullptr, log.data());
MGLOG_E("Shader compilation failed for backend ID %u: %s", backendShaderId, log.data());
m_backendProgramUsable = false;
continue;
}
@@ -3386,12 +3487,33 @@ namespace MobileGL::MG_Backend::DirectGLES {
MGLOG_D("Processed shader source length: %zu", source.length());
}
// Transform feedback capture runs on the real driver (see XfbImpl in
// DirectGLES.cpp), so the capture set has to be declared on the backend
// program before it links. SPIRV-Cross keeps user output names verbatim in
// the transpiled ESSL (`out vec4 result_0;` stays `result_0`), so the
// frontend's requested names carry over unchanged.
if (stateProgramObject->GetTransformFeedbackVaryingCount() > 0 &&
g_GLESFuncs.glTransformFeedbackVaryings != nullptr) {
const auto& xfbVaryings = stateProgramObject->GetTransformFeedbackVaryings();
Vector<const GLchar*> xfbNames;
xfbNames.reserve(xfbVaryings.size());
for (const auto& xfbVarying : xfbVaryings) {
xfbNames.push_back(xfbVarying.name.c_str());
}
MGLOG_D("Declaring %zu transform feedback varyings on program %u", xfbNames.size(),
m_backendProgramId);
g_GLESFuncs.glTransformFeedbackVaryings(m_backendProgramId, static_cast<GLsizei>(xfbNames.size()),
xfbNames.data(),
stateProgramObject->GetTransformFeedbackBufferMode());
}
// Link program
MGLOG_D("Linking program %u", m_backendProgramId);
g_GLESFuncs.glLinkProgram(m_backendProgramId);
GLint linkStatus;
g_GLESFuncs.glGetProgramiv(m_backendProgramId, GL_LINK_STATUS, &linkStatus);
m_backendProgramUsable = m_backendProgramUsable && linkStatus == GL_TRUE;
if (linkStatus != GL_TRUE) {
GLint logLength;
g_GLESFuncs.glGetProgramiv(m_backendProgramId, GL_INFO_LOG_LENGTH, &logLength);
@@ -3504,6 +3626,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
binding.backendLocation = backendLoc;
binding.uniformType = uniformType;
binding.lastAssignedUnit = -1;
// Present only for the samplers EmulateTextureLodBias actually rewrote; the
// pass names it after the sampler, which SPIRV-Cross preserves verbatim.
binding.lodBiasLocation =
g_GLESFuncs.glGetUniformLocation(m_backendProgramId, (String(LOD_BIAS_UNIFORM_PREFIX) + name).c_str());
binding.lastAssignedLodBias = 0.0f;
m_samplerUniformBindings.push_back(binding);
}
}
@@ -3512,12 +3639,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (g_lastUsedBackendProgramId == m_backendProgramId) {
// glUseProgram on a program that did not link is an INVALID_OPERATION and
// leaves the *previous* program current, so the draw would silently render
// with an unrelated shader (KHR-GL3x.texture_size_promotion read another
// test case's alpha that way once a sampler2DRect stage failed to
// transpile). Bind nothing instead: the draw is then a visible no-op.
const Uint programToBind = m_backendProgramUsable ? m_backendProgramId : 0;
if (g_lastUsedBackendProgramId == programToBind) {
return;
}
MGLOG_D("Using program %u", m_backendProgramId);
g_GLESFuncs.glUseProgram(m_backendProgramId);
g_lastUsedBackendProgramId = m_backendProgramId;
MGLOG_D("Using program %u", programToBind);
g_GLESFuncs.glUseProgram(programToBind);
g_lastUsedBackendProgramId = programToBind;
}
void BackendProgramObjectImpl::SetBaseInstance(Uint32 baseInstance) const {
@@ -3625,6 +3758,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
m_cacheSamplerParameters.maxAnisotropy = samplerParams.maxAnisotropy;
}
if (m_cacheSamplerParameters.borderColor != samplerParams.borderColor) {
// Same gate as the texture-side border colour above.
if (g_GLESCapabilities.SupportsTextureBorderClamp && g_GLESFuncs.glSamplerParameterfv) {
const GLfloat borderColorArray[4] = {
samplerParams.borderColor.x(), samplerParams.borderColor.y(),
samplerParams.borderColor.z(), samplerParams.borderColor.w()};
g_GLESFuncs.glSamplerParameterfv(m_backendSamplerId, GL_TEXTURE_BORDER_COLOR, borderColorArray);
}
m_cacheSamplerParameters.borderColor = samplerParams.borderColor;
}
#undef SYNC_SAMPLER_PARAM_IF_CHANGED
m_isInitialized = true;
}
+37 -7
View File
@@ -270,18 +270,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
namespace TextureImpl {
inline Bool IsSupportedTextureTarget(TextureTarget target) {
// Rectangle textures need non-normalized sampling ES cannot express; everything else is
// either native or emulated (1D -> 2D with height 1, 1D array -> 2D array, see
// MapToBackendTextureTarget). SPIRV-Cross already emits the matching ESSL samplers and
// coordinate padding for 1D/1D-array shaders.
return target != TextureTarget::TextureRectangle;
// Every desktop-only target is stored on an ES one; see MapToBackendTextureTarget.
(void)target;
return true;
}
// ES has no 1D targets: 1D textures are stored as 2D (height 1) and 1D arrays as 2D arrays
// (height 1, layers in depth). Must match SPIRV-Cross's ES 1D-as-2D shader emulation.
// ES has none of the desktop-only targets: 1D textures are stored as 2D (height 1), 1D
// 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.
// Must match the shader-side emulation: SPIRV-Cross handles 1D/1D-array itself, and
// ShaderCompiler::LowerRectImages rewrites rectangle images (declining any module
// whose lookups are not integer-coordinate, which SPIRV-Cross then still rejects).
inline TextureTarget MapToBackendTextureTarget(TextureTarget target) {
switch (target) {
case TextureTarget::Texture1D:
case TextureTarget::TextureRectangle:
return TextureTarget::Texture2D;
case TextureTarget::Texture1DArray:
return TextureTarget::Texture2DArray;
@@ -297,6 +300,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
inline GLenum ConvertTextureUploadTargetToBackendGLEnum(TextureUploadTarget uploadTarget) {
switch (uploadTarget) {
case TextureUploadTarget::Texture1D:
case TextureUploadTarget::TextureRectangle:
return GL_TEXTURE_2D;
case TextureUploadTarget::Texture1DArray:
return GL_TEXTURE_2D_ARRAY;
@@ -438,6 +442,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
extern StateBackendObjectRegistry<MG_State::GLState::FramebufferObject, BackendFramebufferObject>
g_backendFramebufferObjects;
// True when the read buffer names a fixed-point (norm/snorm) attachment that the
// backend actually stores in a floating-point format. GL clamps a read from a
// fixed-point colour buffer to [0,1] (GL_CLAMP_READ_COLOR defaults to
// GL_FIXED_ONLY); the substituted float storage would not, so the readback path
// has to apply the clamp itself.
Bool IsFixedPointFallbackReadAttachment();
extern Array<Uint16, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fboBindVersions;
// Tracks the bound FBO's object version (bumped on any attachment/drawbuffer change)
// per target: re-attaching textures or changing draw buffers on an already-bound FBO
@@ -576,6 +587,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
Int backendLocation = -1;
GLenum uniformType = 0;
Int lastAssignedUnit = -1;
// Location of this sampler's emulated GL_TEXTURE_LOD_BIAS uniform
// (PrgramImpl::EmulateTextureLodBias), -1 when the shader has none.
// lastAssignedLodBias mirrors the value the program currently holds,
// so an unbiased shader issues no per-draw glUniform1f at all.
Int lodBiasLocation = -1;
Float lastAssignedLodBias = 0.0f;
};
BackendProgramObjectImpl();
@@ -587,9 +604,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
void SetDrawID(Uint32 drawId) const;
Int GetIndirectParamsBinding() const { return m_indirectParamsBinding; }
Uint GetBackendProgramId() const { return m_backendProgramId; }
// False when the last SyncToBackend could not produce a usable program (a
// shader failed to transpile or compile, or the link itself failed). Use()
// must not leave the previously bound program current in that case.
Bool IsBackendProgramUsable() const { return m_backendProgramUsable; }
Uint GetBackendGlobalUBOId() const { return m_backendGlobalUBOId; }
Uint32 GetSnormFallbackClampOutputMask() const { return m_snormFallbackClampOutputMask; }
Uint32 GetUnormFallbackClampOutputMask() const { return m_unormFallbackClampOutputMask; }
Uint GetFragColorBroadcastCount() const { return m_fragColorBroadcastCount; }
Bool HasGlobalUboBlock() const { return m_globalUboBackendBlockIndex >= 0; }
const Vector<Int>& GetUniformBlockBackendIndices() const { return m_uniformBlockBackendIndices; }
@@ -616,7 +638,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
Int m_indirectParamsBinding = -1;
Uint32 m_snormFallbackClampOutputMask = 0;
Uint32 m_unormFallbackClampOutputMask = 0;
// Draw buffers a legacy gl_FragColor write has to reach (see
// PrgramImpl::BroadcastLegacyFragColor); 1 keeps the plain single-output shader.
Uint m_fragColorBroadcastCount = 1;
Bool m_isInitialized = false;
Bool m_backendProgramUsable = false;
Int m_globalUboBackendBlockIndex = -1;
Int m_globalUboBackendBlockSize = 0;
@@ -629,6 +655,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
extern Uint32 g_snormFallbackClampOutputMask;
extern Uint32 g_unormFallbackClampOutputMask;
// Draw buffers the current draw framebuffer enables. Like the clamp masks above it
// is framebuffer state that the shader has to be compiled against, so a program
// whose snapshot no longer matches is relinked.
extern Uint g_fragColorBroadcastCount;
// Backend id of the last glUseProgram issued through this backend; lets Use()
// skip redundant rebinds. Reset to 0 wherever glUseProgram(0) is issued or the
// ES context is recreated.
+261 -6
View File
@@ -22,6 +22,9 @@
#include <MG_Util/Math/SmallFloat.h>
#include <cmath>
#include <cctype>
#include <cstring>
#include <regex>
namespace MobileGL::MG_Backend::DirectGLES {
namespace {
@@ -45,15 +48,40 @@ namespace MobileGL::MG_Backend::DirectGLES {
return options;
}
Flags<PixelFormatNormalizeOptionBit> GetRuntimeFallbackNormalizeOptions(GLenum requestedInternalFormat) {
Flags<PixelFormatNormalizeOptionBit>
GetRuntimeFallbackNormalizeOptions(GLenum requestedInternalFormat,
Flags<PixelFormatNormalizeOptionBit> extraOptions) {
using namespace MG_Util::TextureFormatProcessor;
const Flags<PixelFormatNormalizeOptionBit> forcedOptions =
GetApplicablePixelFormatNormalizeOptions(requestedInternalFormat, GetForcedPixelFormatNormalizeOptions());
const Flags<PixelFormatNormalizeOptionBit> forcedOptions = GetApplicablePixelFormatNormalizeOptions(
requestedInternalFormat, GetForcedPixelFormatNormalizeOptions() | extraOptions);
if (forcedOptions) {
return forcedOptions;
}
return GetApplicablePixelFormatNormalizeOptions(requestedInternalFormat,
GetDriverPixelFormatNormalizeOptions());
return GetApplicablePixelFormatNormalizeOptions(
requestedInternalFormat, GetDriverPixelFormatNormalizeOptions() | extraOptions);
}
// Multisample textures can only ever be rendered into, never uploaded to, so a fallback
// format for them has to stay colour-renderable - a three-channel float fallback is a legal
// ES texture format but not a legal multisample storage format. Widening to four channels
// is safe here precisely because there is no transfer path that would have to expand
// three-channel client data, and the alpha the draw writes for a three-channel source is
// already the 1.0 the frontend format implies.
Bool TargetRequiresRenderableFormat(SizeT targetIndex) {
return targetIndex == static_cast<SizeT>(TextureTarget::Texture2DMultisample) ||
targetIndex == static_cast<SizeT>(TextureTarget::Texture2DMultisampleArray);
}
Flags<PixelFormatNormalizeOptionBit> GetRenderTargetNormalizeOptions(SizeT targetIndex) {
Flags<PixelFormatNormalizeOptionBit> options;
if (!TargetRequiresRenderableFormat(targetIndex)) {
return options;
}
options |= PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget;
if (!g_GLESCapabilities.SupportsRenderSnorm || !g_GLESCapabilities.SupportsNorm16Texture) {
options |= PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget;
}
return options;
}
Bool HasCachedFormatCapability(TextureInternalFormat internalFormat,
@@ -113,7 +141,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
const GLenum requestedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(internalFormat);
Flags<PixelFormatNormalizeOptionBit> options;
if (!pActiveBackendObject || ShouldUseCaveatFormat(internalFormat, targetIndex)) {
options = GetRuntimeFallbackNormalizeOptions(requestedInternalFormat);
options = GetRuntimeFallbackNormalizeOptions(requestedInternalFormat,
GetRenderTargetNormalizeOptions(targetIndex));
}
NormalizePixelFormat(requestedInternalFormat, options, outInternalFormat, outFormat, outType);
}
@@ -148,6 +177,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
Bool ShouldUseCaveatRenderbufferFormat(TextureInternalFormat internalFormat) {
return ShouldUseCaveatFormat(internalFormat, GetRenderbufferFormatCapabilityTargetIndex());
}
Bool BackendTextureFormatAddsAlpha(TextureInternalFormat internalFormat, TextureTarget target) {
const SizeT targetIndex =
target == TextureTarget::Unknown ? kFormatCapabilityTargetCount : GetFormatCapabilityTargetIndex(target);
if (!TargetRequiresRenderableFormat(targetIndex)) {
return false;
}
if (pActiveBackendObject && !ShouldUseCaveatFormat(internalFormat, targetIndex)) {
return false;
}
const GLenum requestedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(internalFormat);
const Flags<PixelFormatNormalizeOptionBit> options =
GetRuntimeFallbackNormalizeOptions(requestedInternalFormat,
GetRenderTargetNormalizeOptions(targetIndex));
return static_cast<Bool>(options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget);
}
} // namespace TextureImpl
namespace PrgramImpl {
String ProcessOutColorLocations(const String& glslCode) {
@@ -276,6 +321,55 @@ namespace MobileGL::MG_Backend::DirectGLES {
return glslCode;
}
String BroadcastLegacyFragColor(String glslCode, GLenum shaderType, Uint drawBufferCount) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
// The name is the marker: ShaderSourceProcessor only emits it when the source
// wrote gl_FragColor, and such a shader can have no other output.
static const char* const kLoweredName = "mg_FragColor";
if (shaderType != GL_FRAGMENT_SHADER || drawBufferCount <= 1) {
return glslCode;
}
static const std::regex declRegex(
R"(layout\s*\(\s*location\s*=\s*0\s*\)\s*out\s+((?:lowp|mediump|highp)\s+)?vec4\s+mg_FragColor\s*;)");
std::smatch declMatch;
if (!std::regex_search(glslCode, declMatch, declRegex)) {
return glslCode;
}
const String precision = declMatch[1].matched ? declMatch[1].str() : String();
String replicaDecls;
String replicaCopies;
for (Uint location = 1; location < drawBufferCount; ++location) {
const String name = String(kLoweredName) + "_" + std::to_string(location);
replicaDecls += "\nlayout(location = " + std::to_string(location) + ") out " + precision + "vec4 " +
name + ";";
replicaCopies += "\n " + name + " = " + kLoweredName + ";";
}
static const std::regex mainRegex(R"(void\s+main\s*\([^)]*\)\s*\{)");
std::smatch mainMatch;
if (!std::regex_search(glslCode, mainMatch, mainRegex)) {
return glslCode;
}
SizeT bracePos = static_cast<SizeT>(mainMatch.position(0) + mainMatch.length(0) - 1);
Int depth = 0;
for (SizeT pos = bracePos; pos < glslCode.size(); ++pos) {
if (glslCode[pos] == '{') {
++depth;
} else if (glslCode[pos] == '}') {
--depth;
if (depth == 0) {
glslCode.insert(pos, replicaCopies + "\n");
break;
}
}
}
glslCode.insert(static_cast<SizeT>(declMatch.position(0)) + declMatch[0].str().size(), replicaDecls);
return glslCode;
}
String ForceFlatIntegerVaryings(const String& glslCode, GLenum shaderType) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
@@ -342,6 +436,167 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
return result;
}
namespace {
// How a lookup carries its level of detail, and how many arguments it takes
// before the optional bias.
struct LodLookupForm {
const char* name;
Int requiredArgs; // arguments before the optional bias (implicit form)
Int explicitLodArg; // index of the explicit LOD argument, -1 for implicit
};
// texelFetch* is deliberately absent: an integer fetch names its level directly
// and takes no LOD bias. textureGather has no bias either. textureGrad* derives
// the LOD from gradients and offers no argument to fold a bias into, so it is
// left alone rather than rewritten incorrectly.
constexpr LodLookupForm LOD_LOOKUP_FORMS[] = {
{"textureProjLodOffset", 0, 2}, {"textureProjOffset", 4, -1}, {"textureProjLod", 0, 2},
{"textureLodOffset", 0, 2}, {"textureOffset", 3, -1}, {"textureProj", 2, -1},
{"textureLod", 0, 2}, {"texture", 2, -1},
};
// Sampler types with no mip chain, or whose GLSL lookups have no bias overload
// at all (the array-shadow forms), so nothing can or should be folded in.
Bool IsBiasableSamplerType(const String& samplerType) {
if (samplerType.find("MS") != String::npos) return false; // multisample
if (samplerType.find("Buffer") != String::npos) return false; // texture buffer
if (samplerType.find("Rect") != String::npos) return false; // rectangle: no mips
if (samplerType == "sampler2DArrayShadow") return false;
if (samplerType == "samplerCubeArrayShadow") return false;
return true;
}
Bool IsIdentifierChar(char c) { return std::isalnum(static_cast<unsigned char>(c)) || c == '_'; }
// Byte offsets of the top-level argument separators and of the closing paren,
// starting from the '(' at openParen. Empty when the parentheses do not balance.
Vector<SizeT> SplitCallArguments(const String& code, SizeT openParen) {
Vector<SizeT> marks;
Int depth = 0;
for (SizeT i = openParen; i < code.size(); ++i) {
const char c = code[i];
if (c == '(' || c == '[') {
++depth;
} else if (c == ']') {
--depth;
} else if (c == ')') {
--depth;
if (depth == 0) {
marks.push_back(i);
return marks;
}
} else if (c == ',' && depth == 1) {
marks.push_back(i);
}
}
return {};
}
} // namespace
String EmulateTextureLodBias(const String& glslCode) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (glslCode.find("sampler") == String::npos || glslCode.find("texture") == String::npos) {
return glslCode;
}
// Collect the mip-capable sampler uniforms this shader declares.
static const std::regex samplerDeclRegex(
R"(uniform\s+(?:(?:highp|mediump|lowp)\s+)?([iu]?sampler[A-Za-z0-9]*)\s+([A-Za-z_][A-Za-z0-9_]*)\s*;)");
UnorderedMap<String, String> samplerNames; // name -> bias uniform name
for (std::sregex_iterator it(glslCode.begin(), glslCode.end(), samplerDeclRegex), end; it != end; ++it) {
const String samplerType = (*it)[1].str();
if (!IsBiasableSamplerType(samplerType)) continue;
const String name = (*it)[2].str();
samplerNames.emplace(name, String(LOD_BIAS_UNIFORM_PREFIX) + name);
}
if (samplerNames.empty()) {
return glslCode;
}
// Rewrite the lookups. Right-to-left so earlier offsets stay valid, and only for
// samplers named directly as the first argument (SPIRV-Cross never produces an
// expression there for ES output, which has no separate sampler objects).
String result = glslCode;
Vector<String> usedSamplers;
for (SizeT scan = result.size(); scan-- > 0;) {
if (result[scan] != 't') continue;
if (scan > 0 && IsIdentifierChar(result[scan - 1])) continue;
const LodLookupForm* form = nullptr;
SizeT openParen = 0;
for (const auto& candidate : LOD_LOOKUP_FORMS) {
const SizeT nameLength = std::strlen(candidate.name);
if (result.compare(scan, nameLength, candidate.name) != 0) continue;
SizeT after = result.find_first_not_of(" \t", scan + nameLength);
if (after == String::npos || result[after] != '(') continue;
form = &candidate;
openParen = after;
break;
}
if (form == nullptr) continue;
const Vector<SizeT> marks = SplitCallArguments(result, openParen);
if (marks.empty()) continue;
const SizeT argCount = marks.size();
const SizeT closeParen = marks.back();
// First argument must be one of our samplers.
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);
const auto samplerIt = samplerNames.find(samplerName);
if (samplerIt == samplerNames.end()) continue;
const String& biasName = samplerIt->second;
if (form->explicitLodArg >= 0) {
// Explicit LOD: the bias adds to it, as Vulkan does for
// OpImageSampleExplicitLod and as the CTS reference expects.
const SizeT lodIndex = static_cast<SizeT>(form->explicitLodArg);
if (argCount <= lodIndex) continue;
const SizeT lodStart = marks[lodIndex - 1] + 1;
const SizeT lodEnd = marks[lodIndex];
result.insert(lodEnd, String(") + ") + biasName + ")");
result.insert(lodStart, "((");
} else {
const SizeT required = static_cast<SizeT>(form->requiredArgs);
if (argCount == required) {
result.insert(closeParen, String(", ") + biasName);
} else if (argCount == required + 1) {
const SizeT biasStart = marks[argCount - 2] + 1;
result.insert(closeParen, String(") + ") + biasName + ")");
result.insert(biasStart, "((");
} else {
continue;
}
}
usedSamplers.push_back(samplerName);
}
if (usedSamplers.empty()) {
return glslCode;
}
// Declare the bias uniforms that were actually referenced, right after the
// sampler declaration line they belong to.
for (const auto& samplerName : usedSamplers) {
const String& biasName = samplerNames[samplerName];
if (result.find(String("float ") + biasName + ";") != String::npos) continue;
const std::regex declRegex(
R"(uniform\s+(?:(?:highp|mediump|lowp)\s+)?[iu]?sampler[A-Za-z0-9]*\s+)" + samplerName + R"(\s*;)");
std::smatch match;
if (!std::regex_search(result, match, declRegex)) continue;
const SizeT declEnd = static_cast<SizeT>(match.position(0)) + match[0].str().size();
result.insert(declEnd, String("\nuniform highp float ") + biasName + ";");
}
return result;
}
} // namespace PrgramImpl
namespace Utils {
+24
View File
@@ -40,6 +40,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
void GenerateRenderbufferFormatInfo(TextureInternalFormat internalFormat, GLenum* outInternalFormat,
GLenum* outFormat, GLenum* outType);
Bool ShouldUseCaveatTextureFormat(TextureInternalFormat internalFormat, TextureTarget target);
// True when the format the texture is actually created with has an alpha channel the
// frontend format does not (the three-channel multisample widening). GL reads such a
// channel back as 1.0, so any swizzle source of ALPHA has to be answered with ONE.
Bool BackendTextureFormatAddsAlpha(TextureInternalFormat internalFormat, TextureTarget target);
Bool ShouldUseCaveatRenderbufferFormat(TextureInternalFormat internalFormat);
} // namespace TextureImpl
@@ -104,7 +109,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
String ClampNormFallbackOutputs(String glslCode, GLenum shaderType, Uint32 snormOutputMask,
Uint32 unormOutputMask);
String ForceFlatIntegerVaryings(const String& glslCode, GLenum shaderType);
// Legacy GLSL's gl_FragColor is broadcast to every enabled draw buffer (GL 4.6
// 15.2.3), but ShaderSourceProcessor lowers it to the single output mg_FragColor,
// which only ever reaches draw buffer 0. Replicates it across `drawBufferCount`
// outputs and copies the value into them at the end of main. A no-op for
// drawBufferCount <= 1, i.e. for everything but a framebuffer that actually
// enables several draw buffers, so the ordinary single-target shader is untouched.
String BroadcastLegacyFragColor(String glslCode, GLenum shaderType, Uint drawBufferCount);
String RemoveLayoutBinding(const String& glslCode);
// Prefix of the per-sampler float uniform that carries GL_TEXTURE_LOD_BIAS into
// the shader (see EmulateTextureLodBias); the suffix is the sampler's own name.
constexpr const char* LOD_BIAS_UNIFORM_PREFIX = "mg_lodBias_";
// ES has no per-texture/sampler LOD bias at all (GL_TEXTURE_LOD_BIAS is desktop
// only; Vulkan spells it VkSamplerCreateInfo::mipLodBias), so it has to reach the
// shader as a uniform and be folded into every lookup's level of detail. Declares
// one `uniform highp float mg_lodBias_<sampler>;` per mip-capable sampler and adds
// it to the bias / explicit-LOD argument of every lookup that takes one. Draws push
// the bound texture's (or sampler object's) value into it; a shader whose samplers
// all have a zero bias is therefore unaffected. Returns the source unchanged when
// there is nothing to rewrite.
String EmulateTextureLodBias(const String& glslCode);
} // namespace PrgramImpl
namespace Utils {
@@ -18,6 +18,7 @@
#include "MG_Util/Texture/TextureFormatProcessor.h"
#include <Config.h>
#include <cmath>
#include <cstdlib>
#include <cstring>
@@ -40,13 +41,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool IsLayeredTarget(TextureTarget target) {
return target == TextureTarget::Texture3D || target == TextureTarget::Texture1DArray ||
target == TextureTarget::Texture2DArray || target == TextureTarget::TextureCubeMap ||
target == TextureTarget::TextureCubeMapArray ||
target == TextureTarget::Texture2DMultisampleArray;
target == TextureTarget::TextureCubeMapArray || target == TextureTarget::Texture2DMultisampleArray;
}
Bool IsMultisampleTarget(TextureTarget target) {
return target == TextureTarget::Texture2DMultisample ||
target == TextureTarget::Texture2DMultisampleArray;
return target == TextureTarget::Texture2DMultisample || target == TextureTarget::Texture2DMultisampleArray;
}
Bool IsTextureBufferTarget(TextureTarget target) {
@@ -58,8 +57,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
GLenum normalizedInternalFormat = glFormat;
GLenum imageFormat = GL_RGBA;
GLenum imageType = GL_UNSIGNED_BYTE;
MG_Util::TextureFormatProcessor::NormalizePixelFormat(
glFormat, PixelFormatNormalizeOptionBit::None, &normalizedInternalFormat, &imageFormat, &imageType);
MG_Util::TextureFormatProcessor::NormalizePixelFormat(glFormat, PixelFormatNormalizeOptionBit::None,
&normalizedInternalFormat, &imageFormat, &imageType);
return imageFormat == GL_RED_INTEGER || imageFormat == GL_RG_INTEGER || imageFormat == GL_RGB_INTEGER ||
imageFormat == GL_RGBA_INTEGER;
}
@@ -80,8 +79,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return caps;
}
FormatCapabilityFlags BuildVulkanCaps(TextureInternalFormat logicalFormat,
TextureTarget target,
FormatCapabilityFlags BuildVulkanCaps(TextureInternalFormat logicalFormat, TextureTarget target,
VkFormatFeatureFlags features) {
FormatCapabilityFlags caps;
const Bool isDepth = MG_Util::IsDepthFormatInternalFormat(logicalFormat);
@@ -100,8 +98,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const Bool sampled = (features & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) != 0;
const Bool linearFilter = (features & VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT) != 0;
const Bool colorRenderable = (features & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) != 0;
const Bool depthStencilRenderable =
(features & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT) != 0;
const Bool depthStencilRenderable = (features & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT) != 0;
const Bool renderable = (isDepth || isStencil) ? depthStencilRenderable : colorRenderable;
if (sampled || renderable) {
@@ -198,21 +195,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool HasNewCaveatFormatCaps(FormatCapabilityFlags nativeCaps, FormatCapabilityFlags fallbackCaps) {
for (FormatCapability capability : kReportedFormatCapabilities) {
if (HasFormatCapability(fallbackCaps, capability) &&
!HasFormatCapability(nativeCaps, capability)) {
if (HasFormatCapability(fallbackCaps, capability) && !HasFormatCapability(nativeCaps, capability)) {
return true;
}
}
return false;
}
void LogVulkanFormatCaveat(TextureInternalFormat logicalFormat,
SizeT targetIndex,
void LogVulkanFormatCaveat(TextureInternalFormat logicalFormat, SizeT targetIndex,
TextureInternalFormat fallbackFormat) {
MGLOG_D("Caveat: %s %s not fully supported. Reason: native Vulkan format is not fully supported. Fallback: %s",
GetFormatCapabilityTargetName(targetIndex).c_str(),
MG_Util::ConvertTextureInternalFormatToString(logicalFormat).c_str(),
MG_Util::ConvertTextureInternalFormatToString(fallbackFormat).c_str());
MGLOG_D(
"Caveat: %s %s not fully supported. Reason: native Vulkan format is not fully supported. Fallback: %s",
GetFormatCapabilityTargetName(targetIndex).c_str(),
MG_Util::ConvertTextureInternalFormatToString(logicalFormat).c_str(),
MG_Util::ConvertTextureInternalFormatToString(fallbackFormat).c_str());
}
Vector<Int> BuildSampleCounts(Int maxSamples) {
@@ -256,15 +252,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
for (SizeT targetIndex = 0; targetIndex < kFormatCapabilityTextureTargetCount; ++targetIndex) {
const auto target = static_cast<TextureTarget>(targetIndex);
const VkFormatFeatureFlags nativeFeatures =
IsTextureBufferTarget(target) ? nativeProperties.bufferFeatures
: nativeProperties.optimalTilingFeatures;
const VkFormatFeatureFlags nativeFeatures = IsTextureBufferTarget(target)
? nativeProperties.bufferFeatures
: nativeProperties.optimalTilingFeatures;
FormatCapabilityFlags nativeCaps = BuildVulkanCaps(logicalFormat, target, nativeFeatures);
cache.FullCaps[targetIndex][formatIndex] |= nativeCaps;
const VkFormatFeatureFlags fallbackFeatures =
IsTextureBufferTarget(target) ? fallbackProperties.bufferFeatures
: fallbackProperties.optimalTilingFeatures;
const VkFormatFeatureFlags fallbackFeatures = IsTextureBufferTarget(target)
? fallbackProperties.bufferFeatures
: fallbackProperties.optimalTilingFeatures;
FormatCapabilityFlags fallbackCaps = BuildVulkanCaps(logicalFormat, target, fallbackFeatures);
if (fallbackFormat != VK_FORMAT_UNDEFINED && fallbackFormat != nativeFormat) {
cache.CaveatCaps[targetIndex][formatIndex] |= fallbackCaps;
@@ -299,9 +295,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
cache.FullCaps[renderbufferTargetIndex][formatIndex] |= renderbufferCaps;
if (fallbackFormat != VK_FORMAT_UNDEFINED && fallbackFormat != nativeFormat) {
FormatCapabilityFlags fallbackRenderbufferCaps =
BuildVulkanCaps(logicalFormat, TextureTarget::Texture2D,
fallbackProperties.optimalTilingFeatures);
FormatCapabilityFlags fallbackRenderbufferCaps = BuildVulkanCaps(
logicalFormat, TextureTarget::Texture2D, fallbackProperties.optimalTilingFeatures);
fallbackRenderbufferCaps &= FormatCapability::Creatable;
if ((fallbackProperties.optimalTilingFeatures &
(VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT | VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT)) !=
@@ -310,8 +305,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
fallbackRenderbufferCaps |= FormatCapability::MultisampleRenderbuffer;
}
cache.CaveatCaps[renderbufferTargetIndex][formatIndex] |= fallbackRenderbufferCaps;
if (fallbackLogicalFormat &&
HasNewCaveatFormatCaps(renderbufferCaps, fallbackRenderbufferCaps)) {
if (fallbackLogicalFormat && HasNewCaveatFormatCaps(renderbufferCaps, fallbackRenderbufferCaps)) {
LogVulkanFormatCaveat(logicalFormat, renderbufferTargetIndex, *fallbackLogicalFormat);
}
}
@@ -328,14 +322,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void PopulateFormatCapabilities(VkPhysicalDevice physicalDevice,
PFN_vkGetPhysicalDeviceFormatProperties getFormatProperties,
const MG_External::VulkanCapabilities& capabilities,
FormatCapabilityCache& cache) {
const MG_External::VulkanCapabilities& capabilities, FormatCapabilityCache& cache) {
PopulateFormatCapabilitiesImpl(physicalDevice, getFormatProperties, capabilities, cache);
}
BackendObject_DirectVulkan::~BackendObject_DirectVulkan() = default;
BackendObject_DirectVulkan::BackendObject_DirectVulkan(): m_rendererInfo{GetRendererIdentity()} {}
BackendObject_DirectVulkan::BackendObject_DirectVulkan() : m_rendererInfo{GetRendererIdentity()} {}
Bool BackendObject_DirectVulkan::InitWindowSurface() {
if (!m_windowHandle.Handle) {
@@ -409,10 +402,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MGLOG_E("DirectVulkan backend not initialized");
return false;
}
if (!handle.Handle || (handle.Backend != WindowBackend::Android &&
handle.Backend != WindowBackend::X11 &&
handle.Backend != WindowBackend::MetalLayer &&
handle.Backend != WindowBackend::Win32)) {
if (!handle.Handle || (handle.Backend != WindowBackend::Android && handle.Backend != WindowBackend::X11 &&
handle.Backend != WindowBackend::MetalLayer && handle.Backend != WindowBackend::Win32)) {
MGLOG_E("DirectVulkan backend only supports Android, X11, CAMetalLayer, and Win32 native windows");
return false;
}
@@ -503,32 +494,32 @@ namespace MobileGL::MG_Backend::DirectVulkan {
.RendererName = "Magma",
.BackendName = "Direct (Vulkan)",
.ExtraVendor = Nullopt,
.RendererGLInfo =
{
.TargetGLVersion = {3, 3, 0},
.TargetGLSLVersion = {4, 6, 0},
// Baseline advertisement (no shader subgroup, no timer queries); a
// live backend reconciles its copy in UpdateAdvertisedExtensions.
.Extensions = BuildAdvertisedExtensions(false, false, false),
.IsCompatibilityProfile = false
},
.RendererGLInfo = {.TargetGLVersion = {4, 0, 0},
.TargetGLSLVersion = {4, 6, 0},
// Baseline advertisement (no shader subgroup, no timer queries); a
// live backend reconciles its copy in UpdateAdvertisedExtensions.
.Extensions = BuildAdvertisedExtensions(false, false, false),
.IsCompatibilityProfile = false},
.StaticBackendCapability = {.AllowVSOnlyPrograms = false}};
return rendererInfo;
}
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
Bool anisotropicFilteringSupported) {
Vector<GLExtension> extensions = {V_OpenGL30, V_OpenGL31, V_OpenGL32,
V_OpenGL33, E_GL_ARB_draw_buffers_blend, E_GL_ARB_compute_shader,
E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object,
E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters,
E_GL_EXT_framebuffer_object, E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage,
E_GL_ARB_texture_storage, E_GL_ARB_texture_storage_multisample,
E_GL_ARB_clear_texture, E_GL_ARB_direct_state_access,
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_vertex_attrib_binding, E_GL_ARB_shader_image_size};
Vector<GLExtension> extensions = {
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, E_GL_ARB_draw_buffers_blend,
E_GL_ARB_compute_shader, E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_ARB_multi_draw_indirect,
E_GL_ARB_indirect_parameters, E_GL_EXT_framebuffer_object, E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage,
E_GL_ARB_texture_storage, E_GL_ARB_texture_storage_multisample, E_GL_ARB_texture_multisample,
E_GL_ARB_clear_texture, E_GL_ARB_direct_state_access, 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_vertex_attrib_binding, E_GL_ARB_shader_image_size,
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) {
extensions.push_back(E_GL_KHR_shader_subgroup);
}
@@ -591,6 +582,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
funcsTable.GL.ClearBufferiv = ClearBufferiv;
funcsTable.GL.ClearNamedFramebufferfv = ClearNamedFramebufferfv;
funcsTable.GL.ClearNamedFramebufferfi = ClearNamedFramebufferfi;
funcsTable.GL.ClearNamedFramebufferiv = ClearNamedFramebufferiv;
funcsTable.GL.ClearNamedFramebufferuiv = ClearNamedFramebufferuiv;
funcsTable.GL.BlitFramebuffer = BlitFramebuffer;
funcsTable.GL.BlitNamedFramebuffer = BlitNamedFramebuffer;
funcsTable.GL.CopyTexImage2D = CopyTexImage2D;
@@ -634,6 +627,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
funcsTable.GL.GetGpuTimestampNs = GetGpuTimestampNs;
}
// Occlusion queries share the handle-based result/delete entries, which must
// exist even when timer queries are disabled.
funcsTable.GL.BeginOcclusionQuery = BeginOcclusionQuery;
funcsTable.GL.EndOcclusionQuery = EndOcclusionQuery;
funcsTable.GL.BeginXfbPrimitivesQuery = BeginXfbPrimitivesQuery;
funcsTable.GL.EndXfbPrimitivesQuery = EndXfbPrimitivesQuery;
funcsTable.GL.IsQueryResultAvailable = IsQueryResultAvailable;
funcsTable.GL.GetQueryResult64 = GetQueryResult64;
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
funcsTableInitialized = true;
}
return funcsTable;
@@ -714,7 +716,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// rather than a maximum the sampler manager will never apply.
m_dynamicParameters.MaxTextureMaxAnisotropy =
(pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported()) ? m_vulkanCaps.MaxSamplerAnisotropy
: 1.0f;
: 1.0f;
m_dynamicParameters.SmoothLineWidthRangeMin = m_vulkanCaps.SmoothLineWidthRangeMin;
m_dynamicParameters.SmoothLineWidthRangeMax = m_vulkanCaps.SmoothLineWidthRangeMax;
m_dynamicParameters.SmoothLineWidthGranularity = m_vulkanCaps.SmoothLineWidthGranularity;
@@ -735,8 +737,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_dynamicParameters.MaxIntegerSamples = m_vulkanCaps.MaxIntegerSamples;
m_dynamicParameters.MaxSamples = m_vulkanCaps.MaxSamples;
m_dynamicParameters.MaxSampleMaskWords = m_vulkanCaps.MaxSampleMaskWords;
const Int maxSupportedTextureUnits =
static_cast<Int>(MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS);
const Int maxSupportedTextureUnits = static_cast<Int>(MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS);
// GL_MAX_TEXTURE_IMAGE_UNITS is a *per-stage* sampler limit. Adreno/Qualcomm report a huge
// maxPerStageDescriptorSampledImages (descriptor-indexing scale), so clamping it only to our
// combined array capacity (192) still advertises 192 per stage. Host code treats this value as
@@ -746,8 +747,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// limits while keeping the combined limit at our texture-unit array capacity.
constexpr Int maxPerStageTextureUnits =
static_cast<Int>(MG_State::GLState::TextureState::MAX_PER_STAGE_TEXTURE_IMAGE_UNITS);
m_dynamicParameters.MaxTextureImageUnits =
std::min(m_vulkanCaps.MaxTextureImageUnits, maxPerStageTextureUnits);
m_dynamicParameters.MaxTextureImageUnits = std::min(m_vulkanCaps.MaxTextureImageUnits, maxPerStageTextureUnits);
m_dynamicParameters.MaxVertexTextureImageUnits =
std::min(m_vulkanCaps.MaxVertexTextureImageUnits, maxPerStageTextureUnits);
m_dynamicParameters.MaxComputeTextureImageUnits =
@@ -756,19 +756,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
std::min(m_vulkanCaps.MaxCombinedTextureImageUnits, maxSupportedTextureUnits);
// Never advertise more attributes than the state layer can store: the current-value array and
// the Uint32 attribute masks the draw path passes around are both bounded by MAX_VERTEX_ATTRIBS.
m_dynamicParameters.MaxVertexAttribs =
std::min(m_vulkanCaps.MaxVertexAttribs,
static_cast<Int>(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS));
m_dynamicParameters.MaxVertexAttribs = std::min(
m_vulkanCaps.MaxVertexAttribs, static_cast<Int>(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS));
m_dynamicParameters.MaxComputeShaderStorageBlocks = m_vulkanCaps.MaxComputeShaderStorageBlocks;
m_dynamicParameters.MaxCombinedShaderStorageBlocks = m_vulkanCaps.MaxCombinedShaderStorageBlocks;
m_dynamicParameters.MaxComputeUniformBlocks = m_vulkanCaps.MaxComputeUniformBlocks;
m_dynamicParameters.MaxComputeWorkGroupInvocations = m_vulkanCaps.MaxComputeWorkGroupInvocations;
m_dynamicParameters.MaxShaderStorageBufferBindings = m_vulkanCaps.MaxShaderStorageBufferBindings;
m_dynamicParameters.MaxTextureBufferSize = m_vulkanCaps.MaxTextureBufferSize;
m_dynamicParameters.TextureBufferOffsetAlignment = m_vulkanCaps.TextureBufferOffsetAlignment;
m_dynamicParameters.MaxUniformBufferBindings = m_vulkanCaps.MaxUniformBufferBindings;
m_dynamicParameters.MaxUniformBlockSize = m_vulkanCaps.MaxUniformBlockSize;
m_dynamicParameters.MaxImageUnits =
std::max(std::min(m_vulkanCaps.MaxImageUnits, maxSupportedTextureUnits), 0);
m_dynamicParameters.MaxImageUnits = std::max(std::min(m_vulkanCaps.MaxImageUnits, maxSupportedTextureUnits), 0);
m_dynamicParameters.MaxCombinedImageUniforms = std::max(m_vulkanCaps.MaxCombinedImageUniforms, 0);
const Int maxPerStageImageUniforms =
std::min(m_dynamicParameters.MaxImageUnits, m_dynamicParameters.MaxCombinedImageUniforms);
@@ -785,8 +784,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_vulkanCaps.SupportsFragmentStoresAndAtomics ? maxPerStageImageUniforms : 0;
m_dynamicParameters.MaxComputeImageUniforms =
std::min(std::max(m_vulkanCaps.MaxComputeImageUniforms, 0), maxPerStageImageUniforms);
const Int maxSupportedDrawBuffers =
static_cast<Int>(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS);
const Int maxSupportedDrawBuffers = static_cast<Int>(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS);
m_dynamicParameters.MaxDrawBuffers = std::min(m_vulkanCaps.MaxDrawBuffers, maxSupportedDrawBuffers);
m_dynamicParameters.MaxColorAttachments = std::min(m_vulkanCaps.MaxColorAttachments, maxSupportedDrawBuffers);
m_dynamicParameters.MaxClipDistances = m_vulkanCaps.MaxClipDistances;
@@ -796,13 +794,29 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_dynamicParameters.ViewportBoundsRangeMin = m_vulkanCaps.ViewportBoundsRangeMin;
m_dynamicParameters.ViewportBoundsRangeMax = m_vulkanCaps.ViewportBoundsRangeMax;
m_dynamicParameters.ViewportSubpixelBits = m_vulkanCaps.ViewportSubpixelBits;
m_dynamicParameters.MinFragmentInterpolationOffset =
std::isfinite(m_vulkanCaps.MinFragmentInterpolationOffset) &&
m_vulkanCaps.MinFragmentInterpolationOffset <= -0.5f
? m_vulkanCaps.MinFragmentInterpolationOffset
: -0.5f;
m_dynamicParameters.MaxFragmentInterpolationOffset = 0.4375f;
m_dynamicParameters.FragmentInterpolationOffsetBits = 4;
if (m_vulkanCaps.FragmentInterpolationOffsetBits >= 4 &&
std::isfinite(m_vulkanCaps.MaxFragmentInterpolationOffset)) {
const Float requiredMaxOffset = 0.5f - std::ldexp(1.0f, -m_vulkanCaps.FragmentInterpolationOffsetBits);
if (m_vulkanCaps.MaxFragmentInterpolationOffset >= requiredMaxOffset) {
m_dynamicParameters.MaxFragmentInterpolationOffset = m_vulkanCaps.MaxFragmentInterpolationOffset;
m_dynamicParameters.FragmentInterpolationOffsetBits = m_vulkanCaps.FragmentInterpolationOffsetBits;
}
}
m_dynamicParameters.SupportsWideLines = m_vulkanCaps.SupportsWideLines;
m_dynamicParameters.MaxShaderStorageBlockSize =
std::min(m_vulkanCaps.MaxShaderStorageBlockSize, kMaxAdvertisedShaderStorageBlockSize);
if (m_vulkanCaps.SupportsShaderSubgroup) {
m_dynamicParameters.SubgroupSize = m_vulkanCaps.SubgroupSize;
m_dynamicParameters.SubgroupSupportedStages = mapShaderStages(m_vulkanCaps.SubgroupSupportedStages);
m_dynamicParameters.SubgroupSupportedFeatures = mapSubgroupFeatures(m_vulkanCaps.SubgroupSupportedOperations);
m_dynamicParameters.SubgroupSupportedFeatures =
mapSubgroupFeatures(m_vulkanCaps.SubgroupSupportedOperations);
m_dynamicParameters.SubgroupQuadOperationsInAllStages = m_vulkanCaps.SubgroupQuadOperationsInAllStages;
} else {
m_dynamicParameters.SubgroupSize = 0;
@@ -812,8 +826,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
if (m_dynamicParameters.MaxShaderStorageBlockSize != m_vulkanCaps.MaxShaderStorageBlockSize) {
MGLOG_I("DirectVulkan: clamped GL_MAX_SHADER_STORAGE_BLOCK_SIZE from %zu to %zu",
m_vulkanCaps.MaxShaderStorageBlockSize,
m_dynamicParameters.MaxShaderStorageBlockSize);
m_vulkanCaps.MaxShaderStorageBlockSize, m_dynamicParameters.MaxShaderStorageBlockSize);
}
switch (m_vulkanCaps.VendorId) {
case 0x5143u: // VK_VENDOR_ID: Qualcomm
@@ -440,6 +440,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
pVulkanRenderer->ClearNamedFramebufferfv(framebuffer, buffer, drawbuffer, value);
}
void ClearNamedFramebufferiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
GLint drawbuffer, const GLint* value) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ClearNamedFramebufferiv called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::ClearNamedFramebufferiv called with null GL context");
pVulkanRenderer->ClearNamedFramebufferiv(framebuffer, buffer, drawbuffer, value);
}
void ClearNamedFramebufferuiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
GLint drawbuffer, const GLuint* value) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ClearNamedFramebufferuiv called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::ClearNamedFramebufferuiv called with null GL context");
pVulkanRenderer->ClearNamedFramebufferuiv(framebuffer, buffer, drawbuffer, value);
}
void ClearNamedFramebufferfi(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
GLint drawbuffer, GLfloat depth, GLint stencil) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ClearNamedFramebufferfi called with null VulkanRenderer");
@@ -1250,10 +1264,76 @@ namespace MobileGL::MG_Backend::DirectVulkan {
pVulkanRenderer->Clear(mask);
}
// Vulkan has no LINE_LOOP topology; rewrite the draw as an indexed LINE_STRIP
// whose synthesized index list revisits the first vertex at the end.
static void DrawLineLoopAsIndexedStrip(const Vector<Uint32>& closedIndices, GLint basevertex) {
DrawIndexedCmd payload{};
payload.mode = GL_LINE_STRIP;
payload.indexBufferView.indexType = GL_UNSIGNED_INT;
payload.indexBufferView.indexByteOffset = reinterpret_cast<SizeT>(closedIndices.data());
payload.indexBufferView.indexByteSize = closedIndices.size() * sizeof(Uint32);
payload.indexBufferView.forceClientMemory = true;
payload.params.indexCount = static_cast<Uint32>(closedIndices.size());
payload.params.instanceCount = 1;
payload.params.vertexOffset = basevertex;
pVulkanRenderer->DrawElements(payload);
}
// Resolve a DrawElements index list (bound element-array buffer or client
// memory) into uint32 values with the loop-closing first index appended.
static Bool BuildClosedLineLoopIndices(GLsizei count, GLenum type, const void* indices,
Vector<Uint32>& outIndices) {
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
if (indexSize == 0 || count < 2) {
return false;
}
const Uint8* indexBytes = nullptr;
const auto& vao = *MG_State::pGLContext->GetBoundVertexArray();
const auto& indexBufferShared = vao.GetIndexBufferBindingSlot().GetBoundObject();
if (indexBufferShared != nullptr) {
const SizeT offset = reinterpret_cast<SizeT>(indices);
const SizeT bufferSize = indexBufferShared->GetSize();
if (indexBufferShared->MappedData() == nullptr || offset > bufferSize ||
static_cast<SizeT>(count) * indexSize > bufferSize - offset) {
return false;
}
indexBufferShared->SyncPersistentMappedRange();
indexBytes = indexBufferShared->MappedData() + offset;
} else {
indexBytes = static_cast<const Uint8*>(indices);
if (indexBytes == nullptr) {
return false;
}
}
outIndices.resize(static_cast<SizeT>(count) + 1);
for (GLsizei i = 0; i < count; ++i) {
switch (indexSize) {
case 1: outIndices[i] = indexBytes[i]; break;
case 2: outIndices[i] = reinterpret_cast<const Uint16*>(indexBytes)[i]; break;
default: outIndices[i] = reinterpret_cast<const Uint32*>(indexBytes)[i]; break;
}
}
outIndices[count] = outIndices[0];
return true;
}
void DrawArrays(GLenum mode, GLint first, GLsizei count) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawArrays called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawArrays called with null GL context");
if (mode == GL_LINE_LOOP) {
if (count < 2) {
return;
}
Vector<Uint32> closedIndices(static_cast<SizeT>(count) + 1);
for (GLsizei i = 0; i < count; ++i) {
closedIndices[i] = static_cast<Uint32>(first + i);
}
closedIndices[count] = static_cast<Uint32>(first);
DrawLineLoopAsIndexedStrip(closedIndices, 0);
return;
}
DrawCmd payload{};
payload.mode = mode;
payload.params.firstVertex = first;
@@ -1266,6 +1346,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawElements called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawElements called with null GL context");
if (mode == GL_LINE_LOOP) {
Vector<Uint32> closedIndices;
if (BuildClosedLineLoopIndices(count, type, indices, closedIndices)) {
DrawLineLoopAsIndexedStrip(closedIndices, 0);
}
return;
}
DrawIndexedCmd payload{};
payload.mode = mode;
payload.indexBufferView.indexType = type;
@@ -1334,6 +1422,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const GLvoid* indices, GLint basevertex) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawElementsBaseVertex called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawElementsBaseVertex called with null GL context");
if (mode == GL_LINE_LOOP) {
Vector<Uint32> closedIndices;
if (BuildClosedLineLoopIndices(count, type, indices, closedIndices)) {
DrawLineLoopAsIndexedStrip(closedIndices, basevertex);
}
return;
}
DrawIndexedCmd payload{};
payload.mode = mode;
payload.indexBufferView.indexType = type;
@@ -1483,8 +1578,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// records are shared (SharedPtr) with the owning pool's pending list,
// so deleting the query while results are still in flight is safe.
struct VulkanTimerQuery {
enum class Kind : Uint8 { Timer, Occlusion, XfbWritten, XfbGenerated };
Kind kind = Kind::Timer;
SharedPtr<VkTimerQueryManager::TimestampRecord> begin;
SharedPtr<VkTimerQueryManager::TimestampRecord> end;
// Kind::Occlusion - pool slots recorded between Begin/End; summed at result time.
Vector<Uint32> occlusionSlots;
// Renderer generation the records were written under (see
// g_rendererGeneration). A stale generation resolves as available
// with a final zero result: the records' pool indices and frame
@@ -1493,6 +1592,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// (and, via the SharedPtrs, the records), never pool slots, so
// stale queries are always safe to delete.
Uint64 rendererGeneration = 0;
// Kind::XfbGenerated - the frontend's paused-draw primitive counter when the
// query began. VK_QUERY_TYPE_TRANSFORM_FEEDBACK_STREAM_EXT counts only what the
// capture saw, so a draw made while the span was paused is invisible to it -
// but GL_PRIMITIVES_GENERATED counts what the last vertex processing stage
// emitted regardless. The delta closes that gap at result time.
Uint64 pausedPrimitiveSnapshot = 0;
};
} // namespace
@@ -1574,6 +1679,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// ever be produced, so resolve with a final 0.
return true;
}
if (query->kind == VulkanTimerQuery::Kind::Occlusion) {
Uint64 samples = 0;
if (!pVulkanRenderer->ResolveOcclusionQueryResult(query->occlusionSlots, samples)) {
return false;
}
query->occlusionSlots.clear(); // slots are recycled by the resolve
*outNanoseconds = samples;
return true;
}
if (query->kind == VulkanTimerQuery::Kind::XfbWritten ||
query->kind == VulkanTimerQuery::Kind::XfbGenerated) {
Uint64 primitives = 0;
if (!pVulkanRenderer->ResolveXfbQueryResult(query->occlusionSlots,
query->kind == VulkanTimerQuery::Kind::XfbGenerated,
primitives)) {
return false;
}
if (query->kind == VulkanTimerQuery::Kind::XfbGenerated && MG_State::pGLContext != nullptr) {
primitives += MG_State::pGLContext->GetTransformFeedbackPausedPrimitiveCounter() -
query->pausedPrimitiveSnapshot;
}
*outNanoseconds = primitives;
return true;
}
// With wait, mirrors ClientWaitSync: a query ended this frame cannot
// complete until Present submits the commands, so the wait refuses to
// block on the current unsubmitted serial. Returning false keeps the
@@ -1606,6 +1735,49 @@ namespace MobileGL::MG_Backend::DirectVulkan {
delete static_cast<VulkanTimerQuery*>(handle);
}
BackendQueryHandle BeginXfbPrimitivesQuery(Bool generated) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::BeginXfbPrimitivesQuery called with null VulkanRenderer");
if (!pVulkanRenderer->StartXfbQueryCapture(generated ? 1u : 0u)) {
return nullptr;
}
auto* query = new VulkanTimerQuery{};
query->kind = generated ? VulkanTimerQuery::Kind::XfbGenerated : VulkanTimerQuery::Kind::XfbWritten;
query->rendererGeneration = GetRendererGeneration();
query->pausedPrimitiveSnapshot =
MG_State::pGLContext ? MG_State::pGLContext->GetTransformFeedbackPausedPrimitiveCounter() : 0;
return query;
}
void EndXfbPrimitivesQuery(BackendQueryHandle handle) {
auto* query = static_cast<VulkanTimerQuery*>(handle);
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::EndXfbPrimitivesQuery called with null VulkanRenderer");
if (query == nullptr || query->rendererGeneration != GetRendererGeneration()) {
return;
}
pVulkanRenderer->StopXfbQueryCapture(
query->kind == VulkanTimerQuery::Kind::XfbGenerated ? 1u : 0u, query->occlusionSlots);
}
BackendQueryHandle BeginOcclusionQuery() {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::BeginOcclusionQuery called with null VulkanRenderer");
if (!pVulkanRenderer->StartOcclusionQueryCapture()) {
return nullptr;
}
auto* query = new VulkanTimerQuery{};
query->kind = VulkanTimerQuery::Kind::Occlusion;
query->rendererGeneration = GetRendererGeneration();
return query;
}
void EndOcclusionQuery(BackendQueryHandle handle) {
auto* query = static_cast<VulkanTimerQuery*>(handle);
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::EndOcclusionQuery called with null VulkanRenderer");
if (query == nullptr || query->rendererGeneration != GetRendererGeneration()) {
return;
}
pVulkanRenderer->StopOcclusionQueryCapture(query->occlusionSlots);
}
Int64 GetGpuTimestampNs() {
// Vulkan cannot synchronously sample the GPU clock: timestamps only
// exist as vkCmdWriteTimestamp results read back later, and
@@ -35,6 +35,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void ClearBufferiv(GLenum buffer, GLint drawbuffer, const GLint* value);
void ClearNamedFramebufferfv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
GLint drawbuffer, const GLfloat* value);
void ClearNamedFramebufferiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
GLint drawbuffer, const GLint* value);
void ClearNamedFramebufferuiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
GLint drawbuffer, const GLuint* value);
void ClearNamedFramebufferfi(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
GLint drawbuffer, GLfloat depth, GLint stencil);
void Clear(GLbitfield mask);
@@ -123,6 +127,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// only while a live renderer exists whose device can actually time.
Bool IsTimerQuerySupported();
BackendQueryHandle BeginTimeElapsedQuery();
BackendQueryHandle BeginXfbPrimitivesQuery(Bool generated);
void EndXfbPrimitivesQuery(BackendQueryHandle query);
BackendQueryHandle BeginOcclusionQuery();
void EndOcclusionQuery(BackendQueryHandle query);
void EndTimeElapsedQuery(BackendQueryHandle query);
BackendQueryHandle QueryCounterTimestamp();
Bool IsQueryResultAvailable(BackendQueryHandle query);
@@ -16,18 +16,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_device = device;
m_commandPool = commandPool;
Vector<VkCommandBuffer> commandBuffers(frameCount, VK_NULL_HANDLE);
Vector<VkCommandBuffer> commandBuffers(frameCount * 2, VK_NULL_HANDLE);
VkCommandBufferAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
allocInfo.commandPool = commandPool;
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocInfo.commandBufferCount = frameCount;
allocInfo.commandBufferCount = frameCount * 2;
VkResult result = vkAllocateCommandBuffers(device, &allocInfo, commandBuffers.data());
if (result != VK_SUCCESS) {
return result;
}
for (Uint32 i = 0; i < frameCount; ++i) {
m_frames[i].commandBuffer = commandBuffers[i];
m_frames[i].preCommandBuffer = commandBuffers[frameCount + i];
}
VkSemaphoreCreateInfo semaphoreInfo{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO};
@@ -47,9 +48,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void FrameContext::Destroy(VkDevice device, VkCommandPool commandPool) {
const Uint32 frameCount = static_cast<Uint32>(m_frames.size());
Vector<VkCommandBuffer> commandBuffers(frameCount, VK_NULL_HANDLE);
Vector<VkCommandBuffer> commandBuffers(frameCount * 2, VK_NULL_HANDLE);
for (Uint32 i = 0; i < frameCount; ++i) {
commandBuffers[i] = m_frames[i].commandBuffer;
commandBuffers[frameCount + i] = m_frames[i].preCommandBuffer;
}
for (Uint32 i = 0; i < frameCount; ++i) {
@@ -60,7 +62,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
for (auto& frame : m_frames) {
FreeRetiredCommandBuffers(frame);
}
vkFreeCommandBuffers(device, commandPool, frameCount, commandBuffers.data());
vkFreeCommandBuffers(device, commandPool, frameCount * 2, commandBuffers.data());
}
m_frames.clear();
currentFrameIndex = 0;
@@ -87,6 +89,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
currentFrameIndex = (currentFrameIndex + 1) % static_cast<Uint32>(m_frames.size());
GetCurrent().isCommandRecording = false;
GetCurrent().hasCommandBufferRecorded = false;
GetCurrent().isPreCommandRecording = false;
GetCurrent().hasPreCommandBufferRecorded = false;
}
VkCommandBuffer& FrameContext::BeginCommandRecording(VkCommandBufferUsageFlags flags,
@@ -118,6 +122,41 @@ namespace MobileGL::MG_Backend::DirectVulkan {
frame.hasCommandBufferRecorded = true;
}
VkCommandBuffer FrameContext::BeginPreCommandRecording() {
auto& frame = GetCurrent();
if (frame.isPreCommandRecording) {
return frame.preCommandBuffer;
}
MOBILEGL_ASSERT(!frame.hasPreCommandBufferRecorded,
"BeginPreCommandRecording: a recorded pre stream is still awaiting submission");
VK_VERIFY(vkResetCommandBuffer(frame.preCommandBuffer, 0), "BeginPreCommandRecording, vkResetCommandBuffer");
VkCommandBufferBeginInfo beginInfo{};
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
VK_VERIFY(vkBeginCommandBuffer(frame.preCommandBuffer, &beginInfo),
"BeginPreCommandRecording, vkBeginCommandBuffer");
frame.isPreCommandRecording = true;
return frame.preCommandBuffer;
}
void FrameContext::EndPreCommandRecordingIfOpen() {
auto& frame = GetCurrent();
if (!frame.isPreCommandRecording) {
return;
}
VK_VERIFY(vkEndCommandBuffer(frame.preCommandBuffer), "EndPreCommandRecordingIfOpen, vkEndCommandBuffer");
frame.isPreCommandRecording = false;
frame.hasPreCommandBufferRecorded = true;
}
void FrameContext::AbandonPreCommandRecording() {
auto& frame = GetCurrent();
if (frame.isPreCommandRecording) {
VK_VERIFY(vkEndCommandBuffer(frame.preCommandBuffer), "AbandonPreCommandRecording, vkEndCommandBuffer");
}
frame.isPreCommandRecording = false;
frame.hasPreCommandBufferRecorded = false;
}
VkResult FrameContext::InitializeSwapchainSemaphores(VkDevice device, Uint32 swapchainImageCount) {
DestroySwapchainSemaphores(device);
if (swapchainImageCount == 0) {
@@ -202,17 +241,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 swapchainImageIndex) const {
const auto& frame = GetCurrent();
MOBILEGL_ASSERT(!frame.isCommandRecording, "GetSubmitInfo called while command buffer recording is still active");
MOBILEGL_ASSERT(!frame.isPreCommandRecording,
"GetSubmitInfo called while the pre-pass stream is still recording");
AssertValidSwapchainImageIndex(swapchainImageIndex);
SubmitInfoPacket packet{};
packet.waitSemaphore = frame.imageAvailableSemaphore;
packet.signalSemaphore = m_swapchainImageRenderFinishedSemaphores[swapchainImageIndex];
packet.commandBuffer = frame.commandBuffer;
Uint32 commandBufferCount = 0;
// The pre-pass stream executes strictly before the frame's commands.
if (frame.hasPreCommandBufferRecorded) {
packet.commandBuffers[commandBufferCount++] = frame.preCommandBuffer;
}
if (shouldSubmitCommandBuffer) {
packet.commandBuffers[commandBufferCount++] = frame.commandBuffer;
}
packet.submitInfo.waitSemaphoreCount = frame.imageAvailableSemaphoreConsumed ? 0U : 1U;
packet.submitInfo.pWaitSemaphores = frame.imageAvailableSemaphoreConsumed ? nullptr : &packet.waitSemaphore;
packet.submitInfo.pWaitDstStageMask = frame.imageAvailableSemaphoreConsumed ? nullptr : &packet.waitDstStageMask;
packet.submitInfo.commandBufferCount = shouldSubmitCommandBuffer ? 1U : 0U;
packet.submitInfo.pCommandBuffers = shouldSubmitCommandBuffer ? &packet.commandBuffer : nullptr;
packet.submitInfo.commandBufferCount = commandBufferCount;
packet.submitInfo.pCommandBuffers = commandBufferCount > 0 ? packet.commandBuffers : nullptr;
packet.submitInfo.signalSemaphoreCount = 1;
packet.submitInfo.pSignalSemaphores = &packet.signalSemaphore;
return packet;
@@ -276,12 +325,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_recordingObserver = observer;
}
VkResult FrameContext::RetireCurrentCommandBuffer() {
VkResult FrameContext::RetireCurrentCommandBuffer(Bool retirePreCommandBuffer) {
MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE && m_commandPool != VK_NULL_HANDLE,
"RetireCurrentCommandBuffer requires an initialized FrameContext");
auto& frame = GetCurrent();
MOBILEGL_ASSERT(!frame.isCommandRecording,
"RetireCurrentCommandBuffer called while the command buffer is still recording");
MOBILEGL_ASSERT(!frame.isPreCommandRecording,
"RetireCurrentCommandBuffer called while the pre-pass stream is still recording");
VkCommandBufferAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
@@ -289,10 +340,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocInfo.commandBufferCount = 1;
VkCommandBuffer replacement = VK_NULL_HANDLE;
const VkResult result = vkAllocateCommandBuffers(m_device, &allocInfo, &replacement);
VkResult result = vkAllocateCommandBuffers(m_device, &allocInfo, &replacement);
if (result != VK_SUCCESS) {
return result;
}
if (retirePreCommandBuffer) {
VkCommandBuffer preReplacement = VK_NULL_HANDLE;
result = vkAllocateCommandBuffers(m_device, &allocInfo, &preReplacement);
if (result != VK_SUCCESS) {
vkFreeCommandBuffers(m_device, m_commandPool, 1, &replacement);
return result;
}
frame.retiredCommandBuffers.push_back({frame.preCommandBuffer, frame.lastSubmitIndex});
frame.preCommandBuffer = preReplacement;
}
// lastSubmitIndex was just written by the renderer for the submission
// that carried this command buffer.
frame.retiredCommandBuffers.push_back({frame.commandBuffer, frame.lastSubmitIndex});
@@ -29,7 +29,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkPipelineStageFlags waitDstStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
VkSemaphore waitSemaphore = VK_NULL_HANDLE;
VkSemaphore signalSemaphore = VK_NULL_HANDLE;
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
// [0] = pre-pass command buffer (when recorded), then the frame
// command buffer; submitInfo.pCommandBuffers points here.
VkCommandBuffer commandBuffers[2] = {VK_NULL_HANDLE, VK_NULL_HANDLE};
VkSubmitInfo submitInfo{VK_STRUCTURE_TYPE_SUBMIT_INFO};
};
@@ -52,10 +54,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
struct FrameData {
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
// Pre-pass work stream: out-of-pass commands (deferred clear
// materialization, sampled-layout transitions) for resources the
// frame's recording has not touched yet. Submitted immediately
// BEFORE commandBuffer in the same vkQueueSubmit, so recording
// into it never has to split the frame's active render pass.
VkCommandBuffer preCommandBuffer = VK_NULL_HANDLE;
VkSemaphore imageAvailableSemaphore = VK_NULL_HANDLE;
VkFence imageInFlightFence = VK_NULL_HANDLE;
Bool isCommandRecording = false;
Bool hasCommandBufferRecorded = false;
Bool isPreCommandRecording = false;
Bool hasPreCommandBufferRecorded = false;
Bool imageAvailableSemaphoreConsumed = false;
// Command buffers submitted mid-frame (FlushPendingCommands),
// appended in submit order; freed once their submission is known
@@ -77,6 +87,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkCommandBuffer& BeginCommandRecording(VkCommandBufferUsageFlags flags = 0,
const VkCommandBufferInheritanceInfo* pInheritanceInfo = nullptr);
void EndCommandRecording();
// Lazily opens the pre-pass work stream (see FrameData::preCommandBuffer).
VkCommandBuffer BeginPreCommandRecording();
// Closes the pre stream if open, marking it for submission ahead of the
// frame command buffer. Safe to call when it never opened.
void EndPreCommandRecordingIfOpen();
// Drops an in-progress or recorded-but-unsubmitted pre stream (dropped
// frame recordings, swapchain recreation).
void AbandonPreCommandRecording();
VkResult InitializeSwapchainSemaphores(VkDevice device, Uint32 swapchainImageCount);
void DestroySwapchainSemaphores(VkDevice device);
Bool TransitionToPresent(VkImage image, VkImageLayout oldLayout,
@@ -91,7 +109,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// can restart while the submitted buffer is still executing. Retired
// buffers are freed after the slot's fence is next waited, or as soon
// as their submission is observed complete.
VkResult RetireCurrentCommandBuffer();
VkResult RetireCurrentCommandBuffer(Bool retirePreCommandBuffer = false);
// Frees every retired command buffer whose tagged submission index is
// known complete. Driven by the renderer's submit tracker on completion
@@ -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.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.cullMode, sizeof(payload.cullMode)));
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.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};
vpci.viewportCount = 1;
vpci.scissorCount = 1;
@@ -444,6 +450,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
gpi.pStages = payload.stages->data();
gpi.pVertexInputState = payload.vertexInputState;
gpi.pInputAssemblyState = &ia;
gpi.pTessellationState =
payload.topology == VK_PRIMITIVE_TOPOLOGY_PATCH_LIST ? &tessellation : nullptr;
gpi.pViewportState = &vpci;
gpi.pRasterizationState = &raster;
gpi.pMultisampleState = &ms;
@@ -30,6 +30,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 subpass = 0;
VkPrimitiveTopology topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
Bool primitiveRestartEnable = false;
// GL_PATCH_VERTICES; only read for a PATCH_LIST topology.
Uint32 patchControlPoints = 3;
VkPolygonMode polygonMode = VK_POLYGON_MODE_FILL;
VkCullModeFlags cullMode = VK_CULL_MODE_BACK_BIT;
VkFrontFace frontFace = VK_FRONT_FACE_CLOCKWISE;
@@ -923,6 +923,163 @@ namespace MobileGL::MG_Backend::DirectVulkan {
ProgramFactory::CompileOptionFlags m_transformFlags;
};
// Decorates the module's captured varyings for VK_EXT_transform_feedback:
// user outputs get XfbBuffer/XfbStride/Offset directly; a captured
// gl_Position (a gl_PerVertex member) is mirrored into a dedicated output
// variable copied before every OpReturn, BEFORE the position fixup runs,
// so the captured value is the shader's own (pre-remap) gl_Position.
class XfbCaptureDecoratePass final : public spvtools::opt::Pass {
public:
struct CapturedVarying {
std::string name;
Uint32 bufferIndex = 0;
Uint32 offsetBytes = 0;
};
const char* name() const override { return "mobilegl-xfb-capture-decorate"; }
XfbCaptureDecoratePass(Vector<CapturedVarying> varyings, Vector<Uint32> strides)
: m_varyings(Move(varyings)), m_strides(Move(strides)) {}
Status Process() override {
using namespace spvtools::opt;
if (m_varyings.empty()) return Status::SuccessWithoutChange;
auto entryPointIter = get_module()->entry_points().begin();
if (entryPointIter == get_module()->entry_points().end()) return Status::SuccessWithoutChange;
spvtools::opt::Instruction* entryPoint = &*entryPointIter;
const Uint32 entryFunctionId = entryPoint->GetSingleWordInOperand(1);
// Name -> result id map from the debug section.
std::unordered_map<std::string, Uint32> idsByName;
for (auto& debugInst : get_module()->debugs2()) {
if (debugInst.opcode() != spv::Op::OpName) continue;
idsByName[debugInst.GetInOperand(1).AsString()] = debugInst.GetSingleWordInOperand(0);
}
auto* decorationManager = context()->get_decoration_mgr();
const auto decorateForXfb = [&](Uint32 targetId, Uint32 bufferIndex, Uint32 offsetBytes) {
const Uint32 stride = bufferIndex < m_strides.size() ? m_strides[bufferIndex] : 0;
decorationManager->AddDecorationVal(targetId, static_cast<Uint32>(spv::Decoration::XfbBuffer),
bufferIndex);
decorationManager->AddDecorationVal(targetId, static_cast<Uint32>(spv::Decoration::XfbStride),
stride);
decorationManager->AddDecorationVal(targetId, static_cast<Uint32>(spv::Decoration::Offset),
offsetBytes);
};
Bool modified = false;
Bool needsPositionMirror = false;
Uint32 positionBufferIndex = 0;
Uint32 positionOffset = 0;
for (const auto& varying : m_varyings) {
if (varying.name == "gl_Position") {
needsPositionMirror = true;
positionBufferIndex = varying.bufferIndex;
positionOffset = varying.offsetBytes;
continue;
}
const auto idIt = idsByName.find(varying.name);
if (idIt == idsByName.end()) {
MGLOG_E("XfbCaptureDecoratePass: no SPIR-V variable named '%s'", varying.name.c_str());
continue;
}
decorateForXfb(idIt->second, varying.bufferIndex, varying.offsetBytes);
modified = true;
}
if (needsPositionMirror) {
modified |= MirrorPositionForCapture(entryFunctionId, *entryPoint, positionBufferIndex,
positionOffset, decorateForXfb);
}
if (!modified) return Status::SuccessWithoutChange;
context()->AddCapability(spv::Capability::TransformFeedback);
{
auto executionMode = MakeUnique<spvtools::opt::Instruction>(
context(), spv::Op::OpExecutionMode, 0, 0,
std::initializer_list<spvtools::opt::Operand>{
{SPV_OPERAND_TYPE_ID, {entryPoint->GetSingleWordInOperand(1)}},
{SPV_OPERAND_TYPE_EXECUTION_MODE, {static_cast<Uint32>(spv::ExecutionMode::Xfb)}}});
get_module()->AddExecutionMode(Move(executionMode));
}
context()->InvalidateAnalysesExceptFor(spvtools::opt::IRContext::kAnalysisNone);
return Status::SuccessWithChange;
}
private:
template <typename DecorateFn>
Bool MirrorPositionForCapture(Uint32 entryFunctionId, spvtools::opt::Instruction& entryPoint,
Uint32 bufferIndex, Uint32 offsetBytes, const DecorateFn& decorateForXfb) {
const Uint32 entryPointModel = entryPoint.GetSingleWordInOperand(0);
using namespace spvtools::opt;
PositionTargetInfo target{};
if (!FindPositionTarget(context(), &target)) {
MGLOG_E("XfbCaptureDecoratePass: gl_Position capture requested but no position output found");
return false;
}
if (!target.isMember) {
// Standalone gl_Position variable: decorate it directly.
decorateForXfb(target.variableId, bufferIndex, offsetBytes);
return true;
}
auto* typeManager = context()->get_type_mgr();
const Uint32 mirrorPointerTypeId =
typeManager->FindPointerToType(target.vectorTypeId, spv::StorageClass::Output);
if (mirrorPointerTypeId == 0) return false;
const Uint32 mirrorVariableId = context()->TakeNextId();
auto mirrorVariable = MakeUnique<Instruction>(
context(), spv::Op::OpVariable, mirrorPointerTypeId, mirrorVariableId,
std::initializer_list<Operand>{
{SPV_OPERAND_TYPE_STORAGE_CLASS, {static_cast<Uint32>(spv::StorageClass::Output)}}});
get_module()->AddGlobalValue(Move(mirrorVariable));
// A free output location: past every explicitly decorated output.
Uint32 mirrorLocation = 0;
for (auto& annotation : get_module()->annotations()) {
if (annotation.opcode() != spv::Op::OpDecorate ||
annotation.GetSingleWordInOperand(1) != static_cast<Uint32>(spv::Decoration::Location)) {
continue;
}
mirrorLocation = std::max(mirrorLocation, annotation.GetSingleWordInOperand(2) + 1);
}
auto* decorationManager = context()->get_decoration_mgr();
decorationManager->AddDecorationVal(mirrorVariableId,
static_cast<Uint32>(spv::Decoration::Location), mirrorLocation);
decorateForXfb(mirrorVariableId, bufferIndex, offsetBytes);
entryPoint.AddOperand({SPV_OPERAND_TYPE_ID, {mirrorVariableId}});
auto* function = context()->GetFunction(entryFunctionId);
if (function == nullptr) return false;
const auto model = static_cast<spv::ExecutionModel>(entryPointModel);
Bool injected = false;
for (auto& block : *function) {
for (auto instIter = block.begin(); instIter != block.end(); ++instIter) {
// Geometry stages capture per emitted vertex; other stages at return.
const Bool isInjectionSite =
model == spv::ExecutionModel::Geometry
? instIter->opcode() == spv::Op::OpEmitVertex
: instIter->opcode() == spv::Op::OpReturn;
if (!isInjectionSite) continue;
InstructionBuilder builder(context(), &*instIter, IRContext::kAnalysisNone);
const Uint32 memberIndexId = builder.GetUintConstantId(target.memberIndex);
auto* access =
builder.AddAccessChain(target.vectorPtrTypeId, target.variableId, {memberIndexId});
if (access == nullptr) return injected;
auto* value = builder.AddLoad(target.vectorTypeId, access->result_id());
if (value == nullptr) return injected;
builder.AddStore(mirrorVariableId, value->result_id());
injected = true;
}
}
return injected;
}
Vector<CapturedVarying> m_varyings;
Vector<Uint32> m_strides;
};
// Adreno 650 (driver 512.502) faults the GPU on an implicit-LOD sample of a full-screen
// colour render target: the texture unit's derivative path reads outside the image's
// allocation even though the sampler clamps LOD to 0 and the mapping is 1:1. MobileGL's
@@ -1128,6 +1285,41 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return spvtools::Optimizer::PassToken(MakeUnique<GlToVulkanPositionFixPass>(transformFlags));
}
Bool TransformSpirvForXfbCapture(const Vector<Uint>& input, Vector<Uint>& output,
const MG_State::GLState::ProgramObject& program) {
if (input.empty()) {
output.clear();
return true;
}
Vector<XfbCaptureDecoratePass::CapturedVarying> varyings;
varyings.reserve(program.GetTransformFeedbackVaryingCount());
for (const auto& varying : program.GetTransformFeedbackVaryings()) {
varyings.push_back({varying.name, varying.bufferIndex, varying.offsetBytes});
}
Vector<Uint32> strides;
strides.reserve(program.GetTransformFeedbackBufferCount());
for (SizeT i = 0; i < program.GetTransformFeedbackBufferCount(); ++i) {
strides.push_back(program.GetTransformFeedbackStride(static_cast<Uint32>(i)));
}
spvtools::Optimizer optimizer(SPV_ENV_VULKAN_1_3);
spvtools::OptimizerOptions options;
options.set_run_validator(false);
optimizer.SetMessageConsumer([](spv_message_level_t, const char*, const spv_position_t&,
const char* message) {
MGLOG_E("Vulkan: xfb capture pass: %s", message != nullptr ? message : "");
});
optimizer.RegisterPass(spvtools::Optimizer::PassToken(
MakeUnique<XfbCaptureDecoratePass>(Move(varyings), Move(strides))));
const Bool success = optimizer.Run(input.data(), input.size(), &output, options);
if (!success) {
MGLOG_E("Vulkan: xfb capture decoration pass failed; keeping the original module");
output = input;
}
return success;
}
Bool TransformSpirvForVulkanPositionFix(const Vector<Uint>& input, Vector<Uint>& output,
ProgramFactory::CompileOptionFlags transformFlags) {
if (input.empty()) {
@@ -1569,6 +1761,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
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);
return hash;
}
@@ -2173,7 +2384,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Apply position fixup if needed
if (fixupStage != ShaderStage::Unknown && shaders[i] && shaders[i]->GetShaderStage() == fixupStage) {
TransformSpirvForVulkanPositionFix(spv, moduleSpirvs[i], flags);
const Vector<Uint>* fixupInput = &spv;
Vector<Uint> xfbSpirv;
if ((flags & ProgramFactory::CompileOptionBit::XfbCapture) &&
program.GetTransformFeedbackVaryingCount() > 0) {
// Decorate BEFORE the position fixup so a captured gl_Position
// mirror copies the shader's own (pre-remap) value.
if (TransformSpirvForXfbCapture(spv, xfbSpirv, program)) {
fixupInput = &xfbSpirv;
}
}
TransformSpirvForVulkanPositionFix(*fixupInput, moduleSpirvs[i], flags);
} else {
moduleSpirvs[i] = spv;
}
@@ -2186,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
// 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
@@ -47,6 +47,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// level, which makes the two forms produce identical texels (the implicit lambda is
// clamped into [minLod, maxLod] = [0, 0] regardless of derivatives or bias).
ExplicitLod0Sampling = 1 << 5,
// Decorates the last vertex-processing stage's captured varyings with
// XfbBuffer/XfbStride/Offset (VK_EXT_transform_feedback). Set only for draws
// recorded while GL transform feedback is active, so plain draws keep the
// undecorated variant.
XfbCapture = 1 << 6,
};
using CompileOptionFlags = Flags<CompileOptionBit>;
using HashType = Uint64;
@@ -262,6 +262,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_images.resize(imageCount, VK_NULL_HANDLE);
VK_VERIFY(vkGetSwapchainImagesKHR(device, m_swapchain, &imageCount, m_images.data()));
m_imageLayouts.assign(imageCount, VK_IMAGE_LAYOUT_UNDEFINED);
// Fresh swapchain images hold garbage until a render pass stores into them.
m_imageContentDefined.assign(imageCount, false);
m_depthStencilContentDefined.assign(imageCount, false);
CreateImageViews(device);
CreateDepthStencilResources(device, physicalDevice);
@@ -433,9 +436,39 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_images.clear();
m_imageLayouts.clear();
m_imageContentDefined.clear();
m_depthStencilContentDefined.clear();
m_preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
}
Bool SwapchainObject::IsImageContentDefined(Uint32 index) const {
MOBILEGL_ASSERT(index < m_imageContentDefined.size(), "Swapchain image content index out of range");
return m_imageContentDefined[index];
}
void SwapchainObject::SetImageContentDefined(Uint32 index, Bool defined) {
MOBILEGL_ASSERT(index < m_imageContentDefined.size(), "Swapchain image content index out of range");
m_imageContentDefined[index] = defined;
}
Bool SwapchainObject::IsDepthStencilContentDefined(Uint32 index) const {
MOBILEGL_ASSERT(index < m_depthStencilContentDefined.size(),
"Swapchain depth/stencil content index out of range");
return m_depthStencilContentDefined[index];
}
void SwapchainObject::SetDepthStencilContentDefined(Uint32 index, Bool defined) {
MOBILEGL_ASSERT(index < m_depthStencilContentDefined.size(),
"Swapchain depth/stencil content index out of range");
m_depthStencilContentDefined[index] = defined;
}
void SwapchainObject::SetAllDepthStencilContentUndefined() {
for (SizeT i = 0; i < m_depthStencilContentDefined.size(); ++i) {
m_depthStencilContentDefined[i] = false;
}
}
VkImage SwapchainObject::GetImage(Uint32 index) const {
MOBILEGL_ASSERT(index < m_images.size(), "Swapchain image index out of range");
return m_images[index];
@@ -52,6 +52,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void SetImageLayout(Uint32 index, VkImageLayout layout);
SizeT GetImageCount() const { return m_images.size(); }
// EGL content-validity tracking for the default framebuffer. A color
// buffer's content is undefined once its image has been presented
// (EGL_BUFFER_DESTROYED swap behaviour, the implementation default),
// and every ancillary (depth/stencil) buffer's content is undefined
// after ANY swap regardless of swap behaviour (EGL 1.5 §3.10.1). The
// render-pass manager turns an undefined attachment's tile load into
// LOAD_OP_DONT_CARE. Flags start false (a fresh swapchain image holds
// garbage) and a render pass storing into an attachment sets it back
// to defined.
Bool IsImageContentDefined(Uint32 index) const;
void SetImageContentDefined(Uint32 index, Bool defined);
Bool IsDepthStencilContentDefined(Uint32 index) const;
void SetDepthStencilContentDefined(Uint32 index, Bool defined);
void SetAllDepthStencilContentUndefined();
private:
void CreateImageViews(VkDevice device);
void CreateDepthStencilResources(VkDevice device, VkPhysicalDevice physicalDevice);
@@ -77,5 +92,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Vector<VkDeviceMemory> m_depthStencilImageMemories;
Vector<VkImageView> m_depthStencilImageViews;
Vector<VkImageLayout> m_depthStencilImageLayouts;
Vector<Bool> m_imageContentDefined;
Vector<Bool> m_depthStencilContentDefined;
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -18,6 +18,7 @@
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
#include "MG_Util/Metrics/TextureMetrics.h"
#include <Config.h>
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
@@ -204,6 +205,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// The frame's descriptor sets are recycled above, so last frame's reuse target
// is gone: start the per-draw descriptor-reuse cache fresh this frame.
m_hasLastDescriptor = false;
m_lastBindValid = false;
// Re-fingerprint the bound sampler set fresh this frame so any GL object address
// reuse cannot outlive a single frame (see SamplerResolveMemo).
for (auto& memo : m_samplerResolveMemo) {
@@ -264,12 +266,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const auto& samplerOverride = textureUnit.GetSamplerObject();
const auto preferredTarget = programObj.samplerTextureTargetByBinding[binding];
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) {
fallbackHolder = GetFallbackTexture(preferredTarget);
texture = fallbackHolder.get();
MOBILEGL_ASSERT(texture != nullptr,
"ResolveSamplerDescriptor: no fallback texture available for binding=%u location=%d unit=%d target=%d",
binding, location, unit, static_cast<Int>(preferredTarget));
if (texture == nullptr) {
MGLOG_E("ResolveSamplerDescriptor: no fallback texture available for binding=%u ('%s') "
"location=%d unit=%d target=%d",
binding, programObj.samplerNameByBinding[binding].c_str(), location, unit,
static_cast<Int>(preferredTarget));
return false;
}
MGLOG_W(
"ResolveSamplerDescriptor: using fallback texture for unbound sampler binding=%u ('%s') location=%d unit=%d target=%d",
binding, programObj.samplerNameByBinding[binding].c_str(), location, unit,
@@ -590,7 +604,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const VkDeviceSize texelSize =
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) {
viewRange = (viewRange / texelSize) * texelSize;
}
@@ -603,7 +621,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
viewInfo.sType = VK_STRUCTURE_TYPE_BUFFER_VIEW_CREATE_INFO;
viewInfo.buffer = slice.buffer;
viewInfo.format = vkFormat;
viewInfo.offset = slice.offset;
viewInfo.offset = slice.offset + rangeOffset;
viewInfo.range = viewRange;
VkBufferView bufferView = VK_NULL_HANDLE;
@@ -648,6 +666,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return false;
}
// The shader may write this buffer, and those writes land in GPU memory behind the
// frontend's CPU shadow - which is what MapBuffer and GetBufferSubData read.
// Host-visible coherent GPU residency makes the shadow BE that memory, so the
// results are visible without a readback path, exactly as for a capture buffer.
bufferObject->EnsureGpuResidentStorage();
// ... and the read that follows has to wait for this draw or dispatch to retire.
bufferObject->MarkGpuWritten();
BufferSlice slice{};
if (!m_bufferManager->AcquireResidentSlice(BufferKind::ShaderStorage, bufferObject, slice) || !slice.IsValid()) {
MGLOG_E("ResolveStorageBufferDescriptor: failed to sync GL buffer %u for block '%s'",
@@ -750,15 +776,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
SharedPtr<MG_State::GLState::ITextureObject> UniformManager::GetFallbackTexture(TextureTarget target) const {
MOBILEGL_ASSERT(target == TextureTarget::Texture2D || target == TextureTarget::TextureRectangle,
"UniformManager::GetFallbackTexture: unsupported fallback target=%d",
static_cast<Int>(target));
// The fallback is a single-sampled 2D image, so it can only stand in for a sampler that
// would accept one. A multisample sampler in particular cannot: its descriptor demands a
// 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) {
auto fallbackTexture = MakeShared<MG_State::GLState::TextureObject2D>(kFallbackTexture2DExternalIndex);
fallbackTexture->SetInternalFormat(TextureInternalFormat::RGBA8);
fallbackTexture->AllocateStorage(TextureUploadTarget::Texture2D, 0,
{.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);
m_fallbackTexture2D = fallbackTexture;
}
@@ -1189,16 +1227,47 @@ namespace MobileGL::MG_Backend::DirectVulkan {
bufferInfo.range = ubo.range;
dynOffset = static_cast<Uint32>(ubo.dynamicOffset);
} else {
BufferSlice slice{};
if (!m_bufferManager->UploadTransient(BufferKind::Uniform, frameIndex, ubo.payload,
ubo.payloadSize, m_minDynamicOffsetAlignment, slice)) {
MOBILEGL_ASSERT(false, "UniformDescriptorBinder::BindProgramUniformBuffers failed: UBO upload failed on binding %u element %u",
binding, element);
return false;
// Global-UBO slice reuse (see GlobalUboSliceMemo): unchanged
// uniform bytes re-use the slice already uploaded this frame.
const Bool isGlobalUbo =
programObj.globalUboBinding == static_cast<Int>(binding) && element == 0;
const Uint64 uboFrameSerial = m_bufferManager->GetFrameSerial();
const Uint64 uboProgramLifetimeId = program.GetLifetimeId();
const Uint32 uboContentVersion = program.GetUBOContentVersion();
Bool reusedSlice = false;
if (isGlobalUbo) {
for (const auto& memo : m_globalUboMemo) {
if (memo.buffer != VK_NULL_HANDLE &&
memo.programLifetimeId == uboProgramLifetimeId &&
memo.frameSerial == uboFrameSerial &&
memo.uboContentVersion == uboContentVersion &&
memo.range == static_cast<VkDeviceSize>(ubo.payloadSize)) {
bufferInfo.buffer = memo.buffer;
bufferInfo.range = memo.range;
dynOffset = static_cast<Uint32>(memo.offset);
reusedSlice = true;
break;
}
}
}
if (!reusedSlice) {
BufferSlice slice{};
if (!m_bufferManager->UploadTransient(BufferKind::Uniform, frameIndex, ubo.payload,
ubo.payloadSize, m_minDynamicOffsetAlignment, slice)) {
MOBILEGL_ASSERT(false, "UniformDescriptorBinder::BindProgramUniformBuffers failed: UBO upload failed on binding %u element %u",
binding, element);
return false;
}
bufferInfo.buffer = slice.buffer;
bufferInfo.range = ubo.payloadSize;
dynOffset = static_cast<Uint32>(slice.offset);
if (isGlobalUbo) {
m_globalUboMemo[m_globalUboMemoNext] = GlobalUboSliceMemo{
uboProgramLifetimeId, uboFrameSerial, uboContentVersion,
slice.buffer, slice.offset, static_cast<VkDeviceSize>(ubo.payloadSize)};
m_globalUboMemoNext = (m_globalUboMemoNext + 1) % kGlobalUboMemoSize;
}
}
bufferInfo.buffer = slice.buffer;
bufferInfo.range = ubo.payloadSize;
dynOffset = static_cast<Uint32>(slice.offset);
}
bufferInfos.push_back(bufferInfo);
// Dynamic offsets are consumed in binding order, then array element order,
@@ -1337,8 +1406,34 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_hasLastDescriptor = cacheable;
}
vkCmdBindDescriptorSets(commandBuffer, bindPoint, programObj.pipelineLayout, 0, 1,
&descriptorSet, static_cast<Uint32>(dynamicOffsets.size()), dynamicOffsets.data());
// Skip the driver call when this exact binding is already live on the
// command buffer (see the bind-dedup shadow in the header).
const Uint32 offsetCount = static_cast<Uint32>(dynamicOffsets.size());
Bool identicalBind = m_lastBindValid && m_lastBindSet == descriptorSet &&
m_lastBindLayout == programObj.pipelineLayout && m_lastBindPoint == bindPoint &&
m_lastBindOffsetCount == offsetCount && offsetCount <= kMaxShadowedDynamicOffsets;
if (identicalBind) {
for (Uint32 i = 0; i < offsetCount; ++i) {
if (m_lastBindOffsets[i] != dynamicOffsets[i]) {
identicalBind = false;
break;
}
}
}
if (!identicalBind) {
vkCmdBindDescriptorSets(commandBuffer, bindPoint, programObj.pipelineLayout, 0, 1,
&descriptorSet, offsetCount, dynamicOffsets.data());
if (offsetCount <= kMaxShadowedDynamicOffsets) {
m_lastBindValid = true;
m_lastBindSet = descriptorSet;
m_lastBindLayout = programObj.pipelineLayout;
m_lastBindPoint = bindPoint;
m_lastBindOffsetCount = offsetCount;
std::copy_n(dynamicOffsets.data(), offsetCount, m_lastBindOffsets);
} else {
m_lastBindValid = false;
}
}
return true;
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -39,6 +39,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void Shutdown();
void BeginFrame(Uint32 frameIndex);
// A command buffer (re)began recording: descriptor bindings recorded into
// the previous buffer do not carry over, so drop the bind-dedup shadow.
void OnCommandBufferBoundary() { m_lastBindValid = false; }
// A ProgramFactory eviction just destroyed this layout: purge every frame
// slot's cached descriptor sets for it, so a recycled handle value can never
// stale-hit sets written for the dead layout's bindings. The sets are
@@ -185,6 +188,35 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint64 m_lastDescriptorSignature = 0;
Bool m_hasLastDescriptor = false;
// vkCmdBindDescriptorSets dedup: consecutive draws with a static uniform
// block resolve to the same set AND the same dynamic offsets, so the
// driver call can be skipped outright. Command-buffer-scope state; reset
// via OnCommandBufferBoundary whenever a recording (re)begins. Keyed on
// layout+bind point, so a pipeline-layout switch always rebinds.
static constexpr Uint32 kMaxShadowedDynamicOffsets = 8;
Bool m_lastBindValid = false;
VkDescriptorSet m_lastBindSet = VK_NULL_HANDLE;
VkPipelineLayout m_lastBindLayout = VK_NULL_HANDLE;
VkPipelineBindPoint m_lastBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
Uint32 m_lastBindOffsetCount = 0;
Uint32 m_lastBindOffsets[kMaxShadowedDynamicOffsets] = {};
// Global-UBO transient-slice reuse: MC leaves the default uniform block
// untouched across long GUI/terrain runs, so the per-draw re-upload of
// the same bytes can reuse the slice uploaded earlier THIS frame (frame
// serial guards arena recycling; the content version guards writes).
struct GlobalUboSliceMemo {
Uint64 programLifetimeId = 0;
Uint64 frameSerial = 0;
Uint32 uboContentVersion = 0;
VkBuffer buffer = VK_NULL_HANDLE;
VkDeviceSize offset = 0;
VkDeviceSize range = 0;
};
static constexpr Uint32 kGlobalUboMemoSize = 4;
GlobalUboSliceMemo m_globalUboMemo[kGlobalUboMemoSize];
Uint32 m_globalUboMemoNext = 0;
// Per-binding fast path over VkSamplerManager's content-hashed sampler cache, which
// stays the source of truth: its key hashes all sampler+texture state, so two distinct
// sampler objects with identical state still resolve to one VkSampler. This memo only
@@ -58,15 +58,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const VertexInputStateFactory::BackendVertexInputState& VertexInputStateFactory::GetOrCreateVertexInputState(
const MG_State::GLState::VertexArrayObject& vao) {
return GetOrCreateVertexInputState(vao, GetOrComputeHash(vao));
// Per-draw fast path: the VAO carries a pointer to its resolved entry,
// valid while its config version and the cache's eviction epoch both
// match - no re-hash, no map lookup.
const void* memoState = nullptr;
Uint64 memoEpoch = 0;
if (vao.GetBackendStateMemo(memoState, memoEpoch) && memoEpoch == m_evictionEpoch) {
const auto* entry = static_cast<const BackendVertexInputState*>(memoState);
entry->lastUsedFrameBoundary = m_frameBoundaryCounter;
return *entry;
}
const BackendVertexInputState& entry = GetOrCreateVertexInputState(vao, GetOrComputeHash(vao));
vao.SetBackendStateMemo(&entry, m_evictionEpoch);
return entry;
}
const VertexInputStateFactory::BackendVertexInputState& VertexInputStateFactory::GetOrCreateVertexInputState(
const MG_State::GLState::VertexArrayObject& vao, HashType hash) {
auto it = m_cache.find(hash);
if (it != m_cache.end()) {
it->second.lastUsedFrameBoundary = m_frameBoundaryCounter;
return it->second;
it->second->lastUsedFrameBoundary = m_frameBoundaryCounter;
return *it->second;
}
VertexInputStateBuilder builder;
@@ -75,6 +87,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Vector<Uint32> bindingAttributeLocations;
Vector<Bool> bindingUsesClientMemory;
Vector<VertexStreamConversion> bindingConversions;
Vector<VkVertexInputBindingDivisorDescriptionEXT> bindingDivisors;
Uint32 unsupportedAttribMask = 0;
for (Uint32 location = 0; location < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS; ++location) {
@@ -168,15 +181,51 @@ namespace MobileGL::MG_Backend::DirectVulkan {
bindingConversions.push_back(conversion);
builder.AddBinding(binding, stride, inputRate);
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();
auto& entry = m_cache[hash];
auto& slot = m_cache[hash];
if (!slot) {
slot = MakeUnique<BackendVertexInputState>();
}
BackendVertexInputState& entry = *slot;
entry.hash = hash;
entry.lastUsedFrameBoundary = m_frameBoundaryCounter;
entry.bindingDivisors = Move(bindingDivisors);
entry.bindings = builder.GetBindings();
entry.attributes = builder.GetAttributes();
// See the layoutHash declaration: hash only the resolved layout, never
// buffer identities, so identical layouts across VAOs/buffers agree.
XXHASH_VERIFY(XXH64_reset(m_hashState, 0));
for (const auto& binding : entry.bindings) {
XXHASH_VERIFY(XXH64_update(m_hashState, &binding.binding, sizeof(binding.binding)));
XXHASH_VERIFY(XXH64_update(m_hashState, &binding.stride, sizeof(binding.stride)));
XXHASH_VERIFY(XXH64_update(m_hashState, &binding.inputRate, sizeof(binding.inputRate)));
}
for (const auto& attribute : entry.attributes) {
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.location, sizeof(attribute.location)));
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.binding, sizeof(attribute.binding)));
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.format, sizeof(attribute.format)));
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)));
entry.layoutHash = XXH64_digest(m_hashState);
entry.attributeLocationMask = 0;
for (const auto& attribute : entry.attributes) {
if (attribute.location < 32u) {
entry.attributeLocationMask |= (1u << attribute.location);
}
}
entry.bindingBufferKeys = std::move(bindingBufferKeys);
entry.bindingBaseOffsets = std::move(bindingBaseOffsets);
entry.bindingAttributeLocations = std::move(bindingAttributeLocations);
@@ -186,6 +235,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
entry.state = state;
entry.state.pVertexBindingDescriptions = entry.bindings.empty() ? nullptr : entry.bindings.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;
}
@@ -205,8 +261,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
for (auto it = m_cache.begin(); it != m_cache.end();) {
if (m_frameBoundaryCounter - it->second.lastUsedFrameBoundary > kRetireAgeBoundaries) {
if (m_frameBoundaryCounter - it->second->lastUsedFrameBoundary > kRetireAgeBoundaries) {
it = m_cache.erase(it);
// Invalidate every VAO's state-pointer memo: the erased node's
// address may be reused by a future insert.
++m_evictionEpoch;
} else {
++it;
}
@@ -27,9 +27,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
struct BackendVertexInputState {
HashType hash = 0;
// Hash of the resolved Vulkan vertex layout only (bindings, attributes,
// unsupported mask) - NO buffer identities. `hash` mixes buffer heap
// addresses so per-chunk VBOs mint a fresh identity per buffer; keying
// pipelines on that minted one VkPipeline per chunk section for an
// identical layout, defeating pipeline reuse and the per-draw memo.
// Pipelines depend only on the layout, so they key on this instead.
HashType layoutHash = 0;
// Frame boundary of the last cache hit; entries idle past the
// OnFrameBoundary retirement age are evicted (CPU heap only).
Uint64 lastUsedFrameBoundary = 0;
// Mutable: the VAO's state-pointer memo fast path stamps it through
// a const entry reference.
mutable Uint64 lastUsedFrameBoundary = 0;
Vector<VkVertexInputBindingDescription> bindings;
Vector<VkVertexInputAttributeDescription> attributes;
Vector<SizeT> bindingBufferKeys;
@@ -41,6 +50,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// absent from `attributes`, so without this mask the draw path cannot tell them apart from
// a genuinely disabled array and would silently feed the shader the current attribute value.
Uint32 unsupportedAttribMask = 0;
// Bitmask of `attributes[i].location` - the draw path needs it up to
// three times per draw, so it is baked once at build time.
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{
VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO
};
@@ -80,9 +100,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const VulkanRendererConfig& m_config;
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
UnorderedMap<HashType, BackendVertexInputState> m_cache;
// Values are heap-allocated: FastSTL::unordered_map is open-addressing,
// so INSERT invalidates references to stored values. The draw path (and
// the VAOs' state-pointer memos) hold entry pointers across inserts;
// only the unique_ptr cell moves, never the pointee.
UnorderedMap<HashType, UniquePtr<BackendVertexInputState>> m_cache;
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
Uint64 m_frameBoundaryCounter = 0;
// Bumped whenever any cache entry is erased. VAOs memo a raw pointer to
// their heap-allocated entry (stable across map insert/rehash by
// construction); a memo is honored only while its recorded epoch
// matches, so an evicted entry can never be dereferenced through a
// stale memo.
Uint64 m_evictionEpoch = 1;
static inline XXH64_state_t* m_hashState = XXH64_createState();
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -7,6 +7,8 @@
// End of Source File Header
#include "VkBufferManager.h"
#include "../DirectVulkan.h"
#include "VulkanRenderer.h"
namespace MobileGL::MG_Backend::DirectVulkan {
namespace {
@@ -22,6 +24,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT |
VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT |
VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
// Appended to kPersistentBackedUsage when VK_EXT_transform_feedback is enabled
// (see VkBufferManagerInitInfo::transformFeedbackUsageEnabled).
constexpr VkBufferUsageFlags kTransformFeedbackUsage =
VK_BUFFER_USAGE_TRANSFORM_FEEDBACK_BUFFER_BIT_EXT;
// The app writes into the persistent map with no explicit flush, so its memory must
// be host-coherent (Adreno host-visible memory is; requiring it keeps us portable).
constexpr VkMemoryPropertyFlags kPersistentBackedRequiredFlags =
@@ -53,6 +59,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
// The CPU is about to read a buffer a shader wrote. Its bytes live in coherent
// host-visible GPU storage (EnsureGpuResidentStorage adopts it when the buffer is
// bound as a shader storage buffer), so nothing needs copying - but coherence only
// says the writes are visible once they have happened, so the work has to retire
// first.
void Ops_ReadbackFromGpu(BufferObject& bufferObject) {
(void)bufferObject;
if (pVulkanRenderer) {
pVulkanRenderer->FinishPendingGpuWork();
}
}
void* Ops_AcquirePersistentMap(BufferObject& bufferObject) {
if (g_activeBufferManager) {
return g_activeBufferManager->AcquirePersistentMap(bufferObject);
@@ -76,6 +94,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
.FlushMappedRange = Ops_FlushMappedRange,
.OnDestroy = Ops_OnDestroy,
.AcquirePersistentMap = Ops_AcquirePersistentMap,
.ReadbackFromGpu = Ops_ReadbackFromGpu,
};
} // namespace
@@ -465,7 +484,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// it from the current shadow - MappedData() is still the shadow here because the
// frontend adopts (and drops) the shadow only after this returns.
DeferRelease(std::move(resource->buffer));
if (!CreateResidentStorage(*resource, size, kPersistentBackedUsage, kPersistentBackedRequiredFlags)) {
const VkBufferUsageFlags persistentUsage =
kPersistentBackedUsage |
(m_initInfo.transformFeedbackUsageEnabled ? kTransformFeedbackUsage : 0);
if (!CreateResidentStorage(*resource, size, persistentUsage, kPersistentBackedRequiredFlags)) {
resource->persistentMapped = false;
resource->storageSize = 0;
resource->usageFlags = 0;
@@ -539,6 +561,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
auto resource = GetOrCreateResource(bufferObject);
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());
if (size == 0) {
MGLOG_E("VkBufferManager::AcquireStreamedSlice failed: buffer size is zero");
@@ -31,6 +31,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VmaMemoryUsage transientMemoryUsage = VMA_MEMORY_USAGE_AUTO;
VmaAllocationCreateFlags transientAllocationFlags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT;
Bool transientPersistentMapping = false;
// VK_EXT_transform_feedback is enabled: persistent-map storage additionally
// carries the transform feedback usage so capture targets can bind directly.
Bool transformFeedbackUsageEnabled = false;
};
// The DirectVulkan storage behind one frontend buffer (pipe_resource analogue).
@@ -8,15 +8,75 @@
#include "VkClearManager.h"
#include "MG_State/GLState/Core.h"
#include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h"
#include "MG_Util/Converters/MGToStr/TextureEnumConverter.h"
#include <algorithm>
#include <cmath>
namespace MobileGL::MG_Backend::DirectVulkan {
static Bool IsCubeMapFaceUploadTarget(TextureUploadTarget target) {
return target >= TextureUploadTarget::CubeMapPositiveX &&
target <= TextureUploadTarget::CubeMapNegativeZ;
}
VkClearColorValue MakeVkClearColorValue(const ClearAttachmentPayload& payload, Bool formatLacksAlpha) {
VkClearColorValue clearValue{};
switch (payload.colorEncoding) {
case ClearColorEncoding::Int:
clearValue.int32[0] = payload.colorInt.x();
clearValue.int32[1] = payload.colorInt.y();
clearValue.int32[2] = payload.colorInt.z();
clearValue.int32[3] = formatLacksAlpha ? 1 : payload.colorInt.w();
break;
case ClearColorEncoding::Uint:
clearValue.uint32[0] = payload.colorUint.x();
clearValue.uint32[1] = payload.colorUint.y();
clearValue.uint32[2] = payload.colorUint.z();
clearValue.uint32[3] = formatLacksAlpha ? 1u : payload.colorUint.w();
break;
case ClearColorEncoding::Float:
clearValue.float32[0] = payload.color.x();
clearValue.float32[1] = payload.color.y();
clearValue.float32[2] = payload.color.z();
clearValue.float32[3] = formatLacksAlpha ? 1.0f : payload.color.w();
break;
}
return clearValue;
}
void PreCompensateSrgbClearColor(ClearAttachmentPayload& payload, VkFormat destinationFormat) {
if (payload.colorEncoding != ClearColorEncoding::Float) return;
// With GL_FRAMEBUFFER_SRGB enabled GL performs the encoding itself, so the driver doing it
// is exactly right and there is nothing to undo.
if (MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb)) return;
if (ResolveSrgbAttachmentWriteFormat(destinationFormat, false) == destinationFormat) return;
// sRGB -> linear (GL 4.6 core 8.24), applied to the colour channels only: alpha is stored
// linearly in an sRGB format and must pass through untouched.
const auto toLinear = [](Float encoded) {
const Float value = std::clamp(encoded, 0.0f, 1.0f);
return value <= 0.04045f ? value / 12.92f : std::pow((value + 0.055f) / 1.055f, 2.4f);
};
payload.color = FloatVec4(toLinear(payload.color.x()), toLinear(payload.color.y()),
toLinear(payload.color.z()), payload.color.w());
}
void ForceOpaqueClearAlpha(ClearAttachmentPayload& payload) {
switch (payload.colorEncoding) {
case ClearColorEncoding::Int:
payload.colorInt = IntVec4(payload.colorInt.x(), payload.colorInt.y(), payload.colorInt.z(), 1);
break;
case ClearColorEncoding::Uint:
payload.colorUint = UintVec4(payload.colorUint.x(), payload.colorUint.y(), payload.colorUint.z(), 1u);
break;
case ClearColorEncoding::Float:
payload.color = FloatVec4(payload.color.x(), payload.color.y(), payload.color.z(), 1.0f);
break;
}
}
static Bool PendingClearMatchesTextureIdentity(const PendingClearKey& key, const TextureIdentity& identity) {
return key.texture == identity.texture && key.textureLifetimeId == identity.lifetimeId;
}
@@ -93,6 +153,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const std::lock_guard<std::mutex> lock(m_mutex);
m_pendingClears.clear();
m_aliveObjects.clear();
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
}
TextureIdentity VkClearManager::MakeTextureIdentity(MG_State::GLState::ITextureObject* texture) {
@@ -127,6 +188,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_pendingClears.erase(key);
}
m_aliveObjects.erase(identity);
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
}
Bool VkClearManager::LockTextureIdentityLocked(const TextureIdentity& identity,
@@ -221,6 +283,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_aliveObjects[MakeTextureIdentity(texture.get())] = texture;
auto& pending = m_pendingClears[key];
MergeClearPayload(pending, clearPayload);
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
}
void VkClearManager::QueueClear(const ClearAttachmentPayload& clearPayload,
@@ -238,6 +301,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_aliveObjects[MakeTextureIdentity(texture.get())] = texture;
auto& pending = m_pendingClears[key];
MergeClearPayload(pending, clearPayload);
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
}
Bool VkClearManager::HasPendingClear(MG_State::GLState::ITextureObject* texture) {
@@ -245,6 +309,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return false;
}
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
return false; // per-draw hot path: nothing pending anywhere
}
const Uint64 lifetimeId = texture->GetLifetimeId();
const std::lock_guard<std::mutex> lock(m_mutex);
for (auto it = m_pendingClears.begin(); it != m_pendingClears.end(); ++it) {
@@ -260,6 +328,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (key.texture == nullptr) {
return false;
}
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
return false; // per-draw hot path: nothing pending anywhere
}
const std::lock_guard<std::mutex> lock(m_mutex);
if (m_pendingClears.find(key) == m_pendingClears.end()) {
@@ -287,6 +358,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (key.texture == nullptr) {
return false;
}
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
return false; // per-draw hot path: nothing pending anywhere
}
const std::lock_guard<std::mutex> lock(m_mutex);
if (!LockTextureLocked(key, outTexture)) {
@@ -325,6 +399,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (texture == nullptr) {
return false;
}
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
return false; // per-draw hot path: nothing pending anywhere
}
const Uint64 lifetimeId = texture->GetLifetimeId();
const std::lock_guard<std::mutex> lock(m_mutex);
@@ -345,6 +422,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return;
}
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
return; // per-draw hot path: nothing pending anywhere
}
const TextureIdentity identity = MakeTextureIdentity(texture);
MGLOG_D("%s: Pop all pending clears for texture %d", __func__, texture->GetExternalIndex());
const std::lock_guard<std::mutex> lock(m_mutex);
@@ -361,6 +441,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
auto it = m_pendingClears.find(key);
if (it != m_pendingClears.end()) {
m_pendingClears.erase(it);
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
}
}
@@ -14,6 +14,7 @@
#include "MG_Util/Math/VectorTypes.h"
#include <Includes.h>
#include <atomic>
#include <unordered_map>
namespace MobileGL::MG_Backend::DirectVulkan {
@@ -23,13 +24,41 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 stencil{};
};
// A colour clear reaches us from one of glClear/ClearBufferfv, ClearBufferiv or
// ClearBufferuiv, and Vulkan reads VkClearColorValue's union according to the destination
// image's format rather than converting between the members - a float written where an
// integer format is expected is reinterpreted bit for bit, not rounded. Remember which entry
// point supplied the value so the member written when the clear is materialized matches.
enum class ClearColorEncoding : Uint8 { Float, Int, Uint };
struct ClearAttachmentPayload {
GLbitfield mask = 0;
FloatVec4 color = FloatVec4(0.0f, 0.0f, 0.0f, 0.0f);
ClearColorEncoding colorEncoding = ClearColorEncoding::Float;
IntVec4 colorInt = IntVec4(0, 0, 0, 0);
UintVec4 colorUint = UintVec4(0u, 0u, 0u, 0u);
Float depth = 1.0f;
Uint32 stencil = 0;
};
// Builds the clear value for `payload` in the union member its encoding calls for.
// `formatLacksAlpha` applies GL's rule that a format without an alpha channel reads as one,
// expressed in whichever type matches (GL 4.6 core 15.2.3).
VkClearColorValue MakeVkClearColorValue(const ClearAttachmentPayload& payload, Bool formatLacksAlpha);
// Applies that same rule in place, for the paths that have to bake it into the payload before
// the destination is known.
void ForceOpaqueClearAlpha(ClearAttachmentPayload& payload);
// vkCmdClearColorImage names the image, so the driver applies the destination format's transfer
// function to whatever value it is handed. Every other write path in this backend goes through
// the UNORM twin view while GL_FRAMEBUFFER_SRGB is off (ResolveSrgbAttachmentWriteFormat) and
// therefore stores the raw value GL asked for. Rewrites `payload` to the linear colour whose
// encoding is that raw value, so a direct image clear of an sRGB destination agrees with them.
// A no-op for every other format, for integer clear encodings, and when GL is doing the
// encoding itself.
void PreCompensateSrgbClearColor(ClearAttachmentPayload& payload, VkFormat destinationFormat);
struct PendingClearKey {
MG_State::GLState::ITextureObject* texture = nullptr;
Uint64 textureLifetimeId = 0;
@@ -120,7 +149,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
SharedPtr<MG_State::GLState::ITextureObject>& outTexture);
Uint8 m_gcCounter = 0;
public:
// Lock-free probe for the consecutive-draw fast path: any pending clear
// forces the full SetupDraw path (which materializes/consumes it).
Bool HasAnyPendingClears() const { return m_pendingCount.load(std::memory_order_relaxed) != 0; }
private:
mutable std::mutex m_mutex;
// Lock-free mirror of m_pendingClears.size(), maintained under m_mutex
// by every mutation. The per-draw probes (HasPendingClear/GetPending*)
// read it before taking the lock: during draw batches the pending set
// is almost always empty, so this turns several locked map probes per
// draw into one relaxed load.
std::atomic<Uint32> m_pendingCount{0};
std::unordered_map<PendingClearKey, ClearAttachmentPayload, PendingClearKeyHash> m_pendingClears;
std::unordered_map<TextureIdentity, WeakPtr<MG_State::GLState::ITextureObject>, TextureIdentityHash> m_aliveObjects;
};
@@ -16,31 +16,21 @@
namespace MobileGL::MG_Backend::DirectVulkan {
static Bool TryResolveSampleCountFlagBits(Int requestedSamples, VkSampleCountFlagBits& outSampleCount) {
switch (requestedSamples <= 0 ? 1 : requestedSamples) {
case 1:
// GL promises "at least the requested samples", so a non-power-of-two
// request (legal in GL, e.g. 3) rounds up to the next Vulkan bit.
if (requestedSamples <= 1) {
outSampleCount = VK_SAMPLE_COUNT_1_BIT;
return true;
case 2:
outSampleCount = VK_SAMPLE_COUNT_2_BIT;
return true;
case 4:
outSampleCount = VK_SAMPLE_COUNT_4_BIT;
return true;
case 8:
outSampleCount = VK_SAMPLE_COUNT_8_BIT;
return true;
case 16:
outSampleCount = VK_SAMPLE_COUNT_16_BIT;
return true;
case 32:
outSampleCount = VK_SAMPLE_COUNT_32_BIT;
return true;
case 64:
outSampleCount = VK_SAMPLE_COUNT_64_BIT;
return true;
default:
}
if (requestedSamples > 64) {
return false;
}
Uint32 bit = 1;
while (bit < static_cast<Uint32>(requestedSamples)) {
bit <<= 1;
}
outSampleCount = static_cast<VkSampleCountFlagBits>(bit);
return true;
}
static VkImageAspectFlags ResolveImageAspectMaskForFormat(VkFormat format) {
@@ -60,7 +50,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
static Float ResolveColorClearAlpha(const MG_State::GLState::ITextureObject* texture, Float requestedAlpha) {
static Bool ColorFormatLacksAlpha(const MG_State::GLState::ITextureObject* texture) {
return texture != nullptr && MG_Util::GetBaseInternalFormatComponentCount(texture->GetFormat()) == 3;
}
[[maybe_unused]] static Float ResolveColorClearAlpha(const MG_State::GLState::ITextureObject* texture, Float requestedAlpha) {
if (texture != nullptr && MG_Util::GetBaseInternalFormatComponentCount(texture->GetFormat()) == 3) {
return 1.0f;
}
@@ -166,6 +160,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (view != VK_NULL_HANDLE) {
vkDestroyImageView(device, view, nullptr);
}
if (unormTwinView != VK_NULL_HANDLE) {
vkDestroyImageView(device, unormTwinView, nullptr);
}
if (image != VK_NULL_HANDLE && allocation != nullptr) {
vmaDestroyImage(allocator, image, allocation);
}
@@ -173,6 +170,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
image = VK_NULL_HANDLE;
allocation = nullptr;
view = VK_NULL_HANDLE;
unormTwinView = VK_NULL_HANDLE;
layout = VK_IMAGE_LAYOUT_UNDEFINED;
format = VK_FORMAT_UNDEFINED;
aspect = VK_IMAGE_ASPECT_NONE;
@@ -231,10 +229,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (resource.image == VK_NULL_HANDLE && resource.view == VK_NULL_HANDLE) {
return;
}
m_deferredRenderbufferReleases.push_back({resource.image, resource.allocation, resource.view, m_frameCounter});
m_deferredRenderbufferReleases.push_back(
{resource.image, resource.allocation, resource.view, resource.unormTwinView, m_frameCounter});
resource.image = VK_NULL_HANDLE;
resource.allocation = nullptr;
resource.view = VK_NULL_HANDLE;
resource.unormTwinView = VK_NULL_HANDLE;
}
void VkRenderPassManager::CollectDeferredRenderbufferReleases(Bool destroyAll) {
@@ -249,6 +249,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (release.view != VK_NULL_HANDLE) {
vkDestroyImageView(m_device, release.view, nullptr);
}
if (release.unormTwinView != VK_NULL_HANDLE) {
vkDestroyImageView(m_device, release.unormTwinView, nullptr);
}
if (release.image != VK_NULL_HANDLE) {
vmaDestroyImage(m_allocator, release.image, release.allocation);
}
@@ -304,7 +307,47 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
const auto internalFormat = renderbuffer->GetInternalFormat();
const VkFormat format = MG_Util::ConvertTextureInternalFormatToVkEnum(internalFormat);
// Three-channel color formats widen to their RGBA twin exactly like textures do
// (VkTextureManager::ResolveTextureFormatInfo): blits/resolves between a
// renderbuffer and a texture of the same GL format then see one VkFormat.
const VkFormat format = [&]() -> VkFormat {
switch (internalFormat) {
case TextureInternalFormat::RGB:
case TextureInternalFormat::RGB8:
case TextureInternalFormat::R3G3B2:
case TextureInternalFormat::RGB4:
case TextureInternalFormat::RGB5:
return VK_FORMAT_R8G8B8A8_UNORM;
case TextureInternalFormat::SRGB8:
return VK_FORMAT_R8G8B8A8_SRGB;
case TextureInternalFormat::RGB8Snorm:
return VK_FORMAT_R8G8B8A8_SNORM;
case TextureInternalFormat::RGB10:
case TextureInternalFormat::RGB12:
case TextureInternalFormat::RGB16:
return VK_FORMAT_R16G16B16A16_UNORM;
case TextureInternalFormat::RGB16Snorm:
return VK_FORMAT_R16G16B16A16_SNORM;
case TextureInternalFormat::RGB16F:
return VK_FORMAT_R16G16B16A16_SFLOAT;
case TextureInternalFormat::RGB32F:
return VK_FORMAT_R32G32B32A32_SFLOAT;
case TextureInternalFormat::RGB8I:
return VK_FORMAT_R8G8B8A8_SINT;
case TextureInternalFormat::RGB8UI:
return VK_FORMAT_R8G8B8A8_UINT;
case TextureInternalFormat::RGB16I:
return VK_FORMAT_R16G16B16A16_SINT;
case TextureInternalFormat::RGB16UI:
return VK_FORMAT_R16G16B16A16_UINT;
case TextureInternalFormat::RGB32I:
return VK_FORMAT_R32G32B32A32_SINT;
case TextureInternalFormat::RGB32UI:
return VK_FORMAT_R32G32B32A32_UINT;
default:
return MG_Util::ConvertTextureInternalFormatToVkEnum(internalFormat);
}
}();
const VkImageAspectFlags aspect = ResolveImageAspectMaskForFormat(format);
// Renderbuffers are never sampled (GL has no way to bind one to a sampler), so the
// usage set is attachment + transfer: transfer covers readback (vkCmdCopyImageToBuffer),
@@ -314,6 +357,46 @@ namespace MobileGL::MG_Backend::DirectVulkan {
: VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT) |
VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
// GL allows the implementation to allocate more samples than requested
// (glRenderbufferStorageMultisample only promises "at least"), and devices
// like llvmpipe expose 1x/4x but not 2x. Round the request up to the
// nearest supported count for this format.
if (renderbuffer->GetSamples() > 0) {
auto supportedIt = m_attachmentSampleCountsByFormat.find(format);
if (supportedIt == m_attachmentSampleCountsByFormat.end()) {
VkImageFormatProperties formatProperties{};
VkSampleCountFlags supported = VK_SAMPLE_COUNT_1_BIT;
if (vkGetPhysicalDeviceImageFormatProperties(m_physicalDevice, format, VK_IMAGE_TYPE_2D,
VK_IMAGE_TILING_OPTIMAL, imageUsage, 0,
&formatProperties) == VK_SUCCESS) {
supported = formatProperties.sampleCounts;
}
supportedIt = m_attachmentSampleCountsByFormat.emplace(format, supported).first;
}
const VkSampleCountFlags supported = supportedIt->second;
if ((supported & sampleCount) == 0) {
// Smallest supported count above the request, else the largest below it.
Uint32 rounded = 0;
for (Uint32 bit = static_cast<Uint32>(sampleCount) << 1; bit <= VK_SAMPLE_COUNT_64_BIT; bit <<= 1) {
if ((supported & bit) != 0) {
rounded = bit;
break;
}
}
if (rounded == 0) {
for (Uint32 bit = static_cast<Uint32>(sampleCount) >> 1; bit != 0; bit >>= 1) {
if ((supported & bit) != 0) {
rounded = bit;
break;
}
}
}
if (rounded != 0) {
sampleCount = static_cast<VkSampleCountFlagBits>(rounded);
}
}
}
auto& resource = m_renderbufferResources[renderbuffer.get()];
const Bool needsCreate =
resource.image == VK_NULL_HANDLE ||
@@ -351,6 +434,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
imageInfo.usage = imageUsage;
imageInfo.samples = sampleCount;
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
// sRGB renderbuffers attach through their UNORM twin while GL_FRAMEBUFFER_SRGB
// is disabled, which needs a format-reinterpreting second view.
const Bool hasUnormTwin = ResolveSrgbAttachmentWriteFormat(format, false) != format;
if (hasUnormTwin) {
imageInfo.flags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
}
VkImageFormatProperties imageFormatProperties{};
const VkResult imageFormatResult = vkGetPhysicalDeviceImageFormatProperties(
@@ -385,6 +474,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
viewInfo.subresourceRange.layerCount = 1;
VK_VERIFY(vkCreateImageView(m_device, &viewInfo, nullptr, &resource.view),
"vkCreateImageView(renderbuffer)");
if (hasUnormTwin) {
viewInfo.format = ResolveSrgbAttachmentWriteFormat(format, false);
VK_VERIFY(vkCreateImageView(m_device, &viewInfo, nullptr, &resource.unormTwinView),
"vkCreateImageView(renderbuffer unorm twin)");
}
resource.layout = VK_IMAGE_LAYOUT_UNDEFINED;
resource.format = format;
@@ -436,7 +530,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
pending.renderbuffer = renderbuffer;
pending.payload.mask |= clearPayload.mask;
if ((clearPayload.mask & GL_COLOR_BUFFER_BIT) != 0) {
// The whole colour description, not just the float vector: an integer clear keeps its
// value in colorInt/colorUint, and dropping the encoding here would leave the pending
// clear reading as an all-zero float one.
pending.payload.color = clearPayload.color;
pending.payload.colorEncoding = clearPayload.colorEncoding;
pending.payload.colorInt = clearPayload.colorInt;
pending.payload.colorUint = clearPayload.colorUint;
}
if ((clearPayload.mask & GL_DEPTH_BUFFER_BIT) != 0) {
pending.payload.depth = clearPayload.depth;
@@ -481,12 +581,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
VkRenderPassManager::HashType VkRenderPassManager::ComputeHash(
const MG_State::GLState::FramebufferObject& fbo, Uint32 swapchainImageIndex, Bool includePendingClear) {
const MG_State::GLState::FramebufferObject& fbo, Uint32 swapchainImageIndex, Bool includePendingClear,
Bool includeDefaultFboDepthStencil) {
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config.CacheVersion));
const Bool isDefaultFbo = fbo.IsDefaultFramebuffer();
if (isDefaultFbo) {
XXHASH_VERIFY(XXH64_update(m_hashState, &swapchainImageIndex, sizeof(swapchainImageIndex)));
}
// sRGB attachments switch between their sRGB and UNORM-twin views with this
// capability (ResolveSrgbAttachmentWriteFormat), changing the render pass formats.
const Bool framebufferSrgbEnabled =
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
XXHASH_VERIFY(XXH64_update(m_hashState, &framebufferSrgbEnabled, sizeof(framebufferSrgbEnabled)));
auto& drawBuffers = fbo.GetDrawBuffers();
XXHASH_VERIFY(XXH64_update(m_hashState, drawBuffers.data(), drawBuffers.size() * sizeof(drawBuffers[0])));
auto readBuffer = fbo.GetReadBuffer();
@@ -560,9 +666,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
attachment <= FramebufferAttachmentType::BackRight);
if (isDefaultColorAttachment) {
currentLayout = m_swapchainObject.GetImageLayout(swapchainImageIndex);
// Content validity feeds the attachment's loadOp (see the
// creation path), so it must key the cache as well.
if (!m_swapchainObject.IsImageContentDefined(swapchainImageIndex)) {
currentLayout = VK_IMAGE_LAYOUT_UNDEFINED;
}
} else if (attachment == FramebufferAttachmentType::Depth ||
attachment == FramebufferAttachmentType::Stencil) {
currentLayout = m_swapchainObject.GetDepthStencilImageLayout(swapchainImageIndex);
if (!m_swapchainObject.IsDepthStencilContentDefined(swapchainImageIndex)) {
currentLayout = VK_IMAGE_LAYOUT_UNDEFINED;
}
}
} else {
auto* textureResource = m_textureManager.SyncTextureAndGetDescriptor(*texture);
@@ -617,14 +731,49 @@ namespace MobileGL::MG_Backend::DirectVulkan {
combineFramebufferAttachmentObjHash(drawbuf);
}
combineFramebufferAttachmentObjHash(FramebufferAttachmentType::Depth);
combineFramebufferAttachmentObjHash(FramebufferAttachmentType::Stencil);
// The depth-less default-FBO flavor omits the depth/stencil attachment
// entirely, so it must hash differently from the depth-full flavor.
const Bool depthStencilIncluded = !isDefaultFbo || includeDefaultFboDepthStencil;
XXHASH_VERIFY(XXH64_update(m_hashState, &depthStencilIncluded, sizeof(depthStencilIncluded)));
if (depthStencilIncluded) {
combineFramebufferAttachmentObjHash(FramebufferAttachmentType::Depth);
combineFramebufferAttachmentObjHash(FramebufferAttachmentType::Stencil);
}
return XXH64_digest(m_hashState);
}
RenderPassEntry& VkRenderPassManager::GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo,
Uint32 swapchainImageIndex) {
Uint32 swapchainImageIndex,
Bool drawUsesDepthStencil) {
// Resolve the default-FBO depth flavor (see the header comment): keep the
// depth attachment when the caller needs it, when a depth/stencil clear is
// pending, or when the active pass already carries it (escalate-only, so
// alternating depth-less draws never split an established depth pass).
Bool includeDefaultFboDepthStencil = true;
if (fbo.IsDefaultFramebuffer()) {
Bool activeDefaultHasDepthStencil = false;
if (const auto* active = GetActiveRenderPass()) {
Bool activeIsSwapchainPass = false;
Bool activeHasSwapchainDepthStencil = false;
for (const auto& tracked : active->trackedAttachmentLayouts) {
activeIsSwapchainPass |= tracked.target == TrackedAttachmentTarget::SwapchainColor;
activeHasSwapchainDepthStencil |=
tracked.target == TrackedAttachmentTarget::SwapchainDepthStencil;
}
activeDefaultHasDepthStencil = activeIsSwapchainPass && activeHasSwapchainDepthStencil;
}
const auto& defaultDepthAtt = fbo.GetAttachment(FramebufferAttachmentType::Depth);
const auto& defaultStencilAtt = fbo.GetAttachment(FramebufferAttachmentType::Stencil);
const Bool pendingDepthStencilClear =
(defaultDepthAtt.IsTexture() && m_clearManager.HasPendingClear(defaultDepthAtt)) ||
HasPendingRenderbufferClear(defaultDepthAtt) ||
(defaultStencilAtt.IsTexture() && m_clearManager.HasPendingClear(defaultStencilAtt)) ||
HasPendingRenderbufferClear(defaultStencilAtt);
includeDefaultFboDepthStencil =
drawUsesDepthStencil || activeDefaultHasDepthStencil || pendingDepthStencilClear;
}
auto hasPendingClearOnFramebuffer = [&]() -> Bool {
const auto& drawBuffers = fbo.GetDrawBuffers();
for (auto attachment : drawBuffers) {
@@ -674,6 +823,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_rpFastFboVersion == fbo.GetObjectVersion() && m_rpFastSwapchainIndex == swapchainImageIndex &&
m_rpFastTexEpoch == m_textureManager.GetTextureImageEpoch() &&
m_rpFastRbEpoch == m_renderbufferImageEpoch &&
(!fbo.IsDefaultFramebuffer() || m_rpFastHadDepthStencil == includeDefaultFboDepthStencil) &&
m_rpFastRenderPassHash == activeRenderPass->hash && !hasPendingClearOnFramebuffer()) {
auto activeIt = m_renderPasses.find(activeRenderPass->hash);
if (activeIt != m_renderPasses.end()) {
@@ -682,7 +832,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
auto compatibilityHash = ComputeHash(fbo, swapchainImageIndex, false);
auto compatibilityHash = ComputeHash(fbo, swapchainImageIndex, false, includeDefaultFboDepthStencil);
if (activeRenderPass != nullptr &&
activeRenderPass->CompatibleWith(compatibilityHash) &&
!hasPendingClearOnFramebuffer()) {
@@ -699,10 +849,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_rpFastTexEpoch = m_textureManager.GetTextureImageEpoch();
m_rpFastRbEpoch = m_renderbufferImageEpoch;
m_rpFastRenderPassHash = activeRenderPass->hash;
m_rpFastHadDepthStencil = activeIt->second.hasDepthStencilAttachment;
activeIt->second.lastUsedFrame = m_frameCounter;
return activeIt->second;
}
auto hash = ComputeHash(fbo, swapchainImageIndex, true);
auto hash = ComputeHash(fbo, swapchainImageIndex, true, includeDefaultFboDepthStencil);
auto it = m_renderPasses.find(hash);
if (it != m_renderPasses.end()) {
it->second.lastUsedFrame = m_frameCounter;
@@ -783,14 +934,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (rbHasClear &&
MG_Util::GetBaseInternalFormatComponentCount(renderbuffer->GetInternalFormat()) == 3) {
// RGB renderbuffers are backed by an RGBA image; the missing alpha reads as 1.
rbClearPayload.color =
FloatVec4(rbClearPayload.color.x(), rbClearPayload.color.y(),
rbClearPayload.color.z(), 1.0f);
ForceOpaqueClearAlpha(rbClearPayload);
}
const VkImageLayout trackedRbLayout = rbResource->layout;
const Bool rbFramebufferSrgb =
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
const VkFormat rbAttachmentFormat =
ResolveSrgbAttachmentWriteFormat(rbResource->format, rbFramebufferSrgb);
rbDesc.flags = 0;
rbDesc.format = rbResource->format;
rbDesc.format = rbAttachmentFormat;
rbDesc.samples = rbResource->sampleCount;
rbDesc.loadOp = rbHasClear ? VK_ATTACHMENT_LOAD_OP_CLEAR :
(trackedRbLayout == VK_IMAGE_LAYOUT_UNDEFINED ? VK_ATTACHMENT_LOAD_OP_DONT_CARE
@@ -825,7 +978,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
.finalLayout = rbDesc.finalLayout,
});
textureResources.emplace_back(nullptr);
attachmentViews.emplace_back(rbResource->view);
attachmentViews.emplace_back(rbAttachmentFormat != rbResource->format ? rbResource->unormTwinView
: rbResource->view);
MOBILEGL_ASSERT(attachmentViews.back() != VK_NULL_HANDLE,
"GetOrCreateRenderPass: renderbuffer view missing at color attachment %d", i);
@@ -894,6 +1048,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MOBILEGL_ASSERT(swapchainImageIndex < swapchainViews.size(),
"GetOrCreateRenderPass: swapchain image index out of range");
trackedColorLayout = m_swapchainObject.GetImageLayout(swapchainImageIndex);
// EGL: a presented color buffer's content is undefined when its
// image comes back around (EGL_BUFFER_DESTROYED, the default
// swap behaviour) - skip the tile load instead of reloading
// stale pixels nobody may rely on.
if (!hasClear && !m_swapchainObject.IsImageContentDefined(swapchainImageIndex)) {
trackedColorLayout = VK_IMAGE_LAYOUT_UNDEFINED;
}
trackedAttachmentLayouts.emplace_back(TrackedAttachmentLayoutInfo {
.target = TrackedAttachmentTarget::SwapchainColor,
.swapchainImageIndex = swapchainImageIndex,
@@ -907,12 +1068,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MOBILEGL_ASSERT(textureResource,
"GetOrCreateRenderPass: SyncTextureAndGetDescriptor failed at color attachment %d", i);
textureResources.emplace_back(textureResource);
desc.format = textureResource->format;
desc.format = ResolveSrgbAttachmentWriteFormat(
textureResource->format,
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb));
attachmentSampleCount = textureResource->sampleCount;
trackedColorLayout = textureResource->layout;
trackedAttachmentLayouts.emplace_back(TrackedAttachmentLayoutInfo {
.target = TrackedAttachmentTarget::Texture,
.texture = att.GetTexture(),
.textureRaw = att.GetTexture().get(),
.textureMipLevel = attachmentMipLevel,
.finalLayout = desc.finalLayout,
});
@@ -976,6 +1140,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
};
const auto* selectedDepthStencilAttachment = isUsableDepthStencilAttachment(depthAtt) ? &depthAtt :
(isUsableDepthStencilAttachment(stencilAtt) ? &stencilAtt : nullptr);
// Depth-less default-FBO flavor: nothing in this pass touches depth/stencil
// and their content is undefined anyway (EGL swap), so drop the attachment
// and its whole tile load + store.
if (isDefaultFbo && !includeDefaultFboDepthStencil) {
selectedDepthStencilAttachment = nullptr;
}
const Bool hasDistinctDepthAndStencilAttachments =
isUsableDepthStencilAttachment(depthAtt) && isUsableDepthStencilAttachment(stencilAtt) &&
!sameDepthStencilAttachmentObject(depthAtt, stencilAtt);
@@ -994,6 +1164,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkImageLayout trackedDepthLayout = isDefaultFbo ?
m_swapchainObject.GetDepthStencilImageLayout(swapchainImageIndex) :
VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
// EGL 1.5 §3.10.1: every ancillary (depth/stencil) buffer's content is
// undefined after a swap, so the first default-FBO pass of a frame can
// skip the depth/stencil tile load outright.
if (isDefaultFbo && !m_swapchainObject.IsDepthStencilContentDefined(swapchainImageIndex)) {
trackedDepthLayout = VK_IMAGE_LAYOUT_UNDEFINED;
}
depthAttachmentDescription.flags = 0;
VkSampleCountFlagBits depthAttachmentSampleCount = VK_SAMPLE_COUNT_1_BIT;
Int depthAttachmentId = 0;
@@ -1077,6 +1253,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
trackedAttachmentLayouts.emplace_back(TrackedAttachmentLayoutInfo {
.target = TrackedAttachmentTarget::Texture,
.texture = selectedDepthStencilAttachment->GetTexture(),
.textureRaw = selectedDepthStencilAttachment->GetTexture().get(),
.textureMipLevel = attachmentMipLevel,
.finalLayout = depthAttachmentDescription.finalLayout,
});
@@ -1122,6 +1299,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
const Bool hasDepthStencilAttachment = depthAttachmentRef.attachment != VK_ATTACHMENT_UNUSED;
// Declare only the used colour-reference span. The GL draw-buffer array
// always spans 8 slots, so passes used to declare colorAttachmentCount=8
// with trailing VK_ATTACHMENT_UNUSED holes - and Adreno configures its
// per-pixel render-backend/export path from the DECLARED count, so every
// fragment of every pass paid the 8-target export cost (measured on
// Adreno 650 / MC 26.2: 11.9 -> 7.5 ms of GPU time per frame, with the
// single-quad swapchain blit pass alone dropping 1.26 -> 0.40 ms).
// Interior GL_NONE holes keep their slots so fragment-output locations
// still line up; a fragment output at a location past the trimmed count
// is discarded, which is exactly GL's semantic for writing to a draw
// buffer set to GL_NONE.
while (!colorAttachmentRefs.empty() &&
colorAttachmentRefs.back().attachment == VK_ATTACHMENT_UNUSED) {
colorAttachmentRefs.pop_back();
}
// Subpass
VkSubpassDescription subpassDesc;
subpassDesc.flags = 0;
@@ -1311,12 +1504,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
if ((clearPayload.mask & GL_COLOR_BUFFER_BIT) != 0) {
clearValues[pending.attachmentIndex].color = {
clearPayload.color.x(),
clearPayload.color.y(),
clearPayload.color.z(),
ResolveColorClearAlpha(liveTexture.get(), clearPayload.color.w())
};
clearValues[pending.attachmentIndex].color =
MakeVkClearColorValue(clearPayload, ColorFormatLacksAlpha(liveTexture.get()));
}
if ((clearPayload.mask & GL_DEPTH_BUFFER_BIT) != 0) {
clearValues[pending.attachmentIndex].depthStencil.depth = clearPayload.depth;
@@ -1330,6 +1519,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
renderPassBeginInfo.pClearValues = clearValues.data();
vkCmdBeginRenderPass(commandBuffer, &renderPassBeginInfo, VK_SUBPASS_CONTENTS_INLINE);
// Pre-pass stream bookkeeping: this pass's attachment images are now
// referenced by the open frame recording.
if (s_textureManager != nullptr) {
for (const auto& tracked : renderPassEntry.trackedAttachmentLayouts) {
if (tracked.target == TrackedAttachmentTarget::Texture) {
if (const auto texture = tracked.texture.lock()) {
s_textureManager->StampTextureRecordingUse(texture.get());
}
}
}
}
for (const auto& pending: renderPassEntry.pendingClearAttachments) {
if (pending.hasInlinePayload) {
if (s_renderPassManager != nullptr) {
@@ -1382,11 +1582,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
case TrackedAttachmentTarget::SwapchainColor:
MOBILEGL_ASSERT(s_swapchainObject != nullptr, "EndRenderPass: swapchain object is null");
s_swapchainObject->SetImageLayout(trackedAttachment.swapchainImageIndex, trackedAttachment.finalLayout);
// The pass stored into the attachment: its content is defined
// until the image is next presented.
s_swapchainObject->SetImageContentDefined(trackedAttachment.swapchainImageIndex, true);
break;
case TrackedAttachmentTarget::SwapchainDepthStencil:
MOBILEGL_ASSERT(s_swapchainObject != nullptr, "EndRenderPass: swapchain object is null");
s_swapchainObject->SetDepthStencilImageLayout(trackedAttachment.swapchainImageIndex,
trackedAttachment.finalLayout);
s_swapchainObject->SetDepthStencilContentDefined(trackedAttachment.swapchainImageIndex, true);
break;
default:
MOBILEGL_ASSERT(false, "EndRenderPass: unsupported tracked attachment target=%d",
@@ -42,6 +42,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
struct TrackedAttachmentLayoutInfo {
TrackedAttachmentTarget target = TrackedAttachmentTarget::Texture;
WeakPtr<MG_State::GLState::ITextureObject> texture;
// Identity-compare shortcut for the per-draw "does the active pass use
// this sampled texture" probe: comparing this against a LIVE texture's
// address needs no weak_ptr::lock (two refcount atomics per probe).
// May dangle once the texture dies - compare only, never dereference.
MG_State::GLState::ITextureObject* textureRaw = nullptr;
WeakPtr<MG_State::GLState::RenderbufferObject> renderbuffer;
Uint32 textureMipLevel = 0;
Uint32 swapchainImageIndex = 0;
@@ -188,8 +193,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
HashType ComputeHash(
const MG_State::GLState::FramebufferObject& fbo,
Uint32 swapchainImageIndex,
Bool includePendingClear = true);
RenderPassEntry& GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo, Uint32 swapchainImageIndex);
Bool includePendingClear = true,
Bool includeDefaultFboDepthStencil = true);
// drawUsesDepthStencil: whether the operation about to run inside the pass
// reads or writes the depth/stencil buffer (depth test or stencil test
// enabled, or a depth/stencil clear). Only consulted for the DEFAULT
// framebuffer: EGL undefines its ancillary buffers at every swap, so a
// default-FBO pass whose draws provably never touch depth/stencil is
// created WITHOUT the depth attachment - on a tiler that skips the whole
// depth tile load AND store. The flavor only escalates: once a pass with
// depth is active, later depth-less draws keep using it, and a depth-using
// draw against a depth-less active pass resolves to a new (incompatible)
// entry, which the caller's compatibility check turns into a pass split;
// the new pass's depth loads DONT_CARE (content was undefined all along).
RenderPassEntry& GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo,
Uint32 swapchainImageIndex,
Bool drawUsesDepthStencil = true);
void QueueRenderbufferClear(GLbitfield mask, const ClearFramebufferPayload& clearPayload,
const MG_State::GLState::FramebufferObject& drawFbo);
void QueueRenderbufferClear(const ClearAttachmentPayload& clearPayload,
@@ -219,6 +238,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// image recreation.
Uint64 m_renderbufferImageEpoch = 1;
public:
// Bumped whenever a renderbuffer backing is (re)created; consecutive-draw
// snapshots include it so an attachment respecify forces a re-resolve.
Uint64 GetRenderbufferImageEpoch() const { return m_renderbufferImageEpoch; }
private:
// Per-draw fast-path memo for GetOrCreateRenderPass (dirty-flag state tracking): when the
// framebuffer state is provably unchanged since the last resolution, the active render pass
// is reused WITHOUT recomputing the expensive per-draw hash. Invalidated by FBO switch /
@@ -231,6 +257,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint64 m_rpFastTexEpoch = 0;
Uint64 m_rpFastRbEpoch = 0;
Uint64 m_rpFastRenderPassHash = 0;
// Whether the memoized entry carries a depth/stencil attachment; a
// default-FBO resolution whose effective depth request differs must
// miss the memo (the depth-less/depth-full flavors hash differently).
Bool m_rpFastHadDepthStencil = false;
public:
struct RenderbufferResource {
@@ -244,6 +274,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkImage image = VK_NULL_HANDLE;
VmaAllocation allocation = nullptr;
VkImageView view = VK_NULL_HANDLE;
// UNORM reinterpretation of an sRGB image, used as the attachment view while
// GL_FRAMEBUFFER_SRGB is disabled (raw writes). Null for non-sRGB formats.
VkImageView unormTwinView = VK_NULL_HANDLE;
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
VkFormat format = VK_FORMAT_UNDEFINED;
VkImageAspectFlags aspect = VK_IMAGE_ASPECT_NONE;
@@ -277,12 +310,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkImage image = VK_NULL_HANDLE;
VmaAllocation allocation = nullptr;
VkImageView view = VK_NULL_HANDLE;
VkImageView unormTwinView = VK_NULL_HANDLE;
Uint64 deferredAtFrame = 0;
};
UnorderedMap<MG_State::GLState::RenderbufferObject*, RenderbufferResource> m_renderbufferResources;
UnorderedMap<MG_State::GLState::RenderbufferObject*, PendingRenderbufferClear> m_pendingRenderbufferClears;
Vector<DeferredRenderbufferRelease> m_deferredRenderbufferReleases;
// Supported sample counts per attachment format, so per-draw resource lookups
// do not repeat vkGetPhysicalDeviceImageFormatProperties.
UnorderedMap<VkFormat, VkSampleCountFlags> m_attachmentSampleCountsByFormat;
Bool HasPendingRenderbufferClear(
const MG_State::GLState::FramebufferAttachmentObject& attachment) const;
@@ -164,7 +164,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
XXHASH_VERIFY(XXH64_update(m_hashState, &maxAnisotropy, sizeof(maxAnisotropy)));
const auto compareMode = sampler.GetCompareMode();
XXHASH_VERIFY(XXH64_update(m_hashState, &compareMode, sizeof(compareMode)));
const auto compareFunc = ResolveCompareFunc(sampler, texture);
const auto compareFunc = sampler.GetSamplerCompareFunc();
XXHASH_VERIFY(XXH64_update(m_hashState, &compareFunc, sizeof(compareFunc)));
const auto borderColor = ResolveVkBorderColor(sampler, texture);
XXHASH_VERIFY(XXH64_update(m_hashState, &borderColor, sizeof(borderColor)));
@@ -207,7 +207,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
samplerInfo.anisotropyEnable = maxAnisotropy > 1.0f ? VK_TRUE : VK_FALSE;
samplerInfo.maxAnisotropy = maxAnisotropy;
samplerInfo.compareEnable = sampler.GetCompareMode() == SamplerCompareMode::CompareToTexture ? VK_TRUE : VK_FALSE;
samplerInfo.compareOp = ToVkCompareOp(ResolveCompareFunc(sampler, texture));
samplerInfo.compareOp = ToVkCompareOp(sampler.GetSamplerCompareFunc());
// Must match BuildSamplerKey's resolution exactly.
samplerInfo.maxLod = ResolveSingleLevelMaxLod(sampler, singleLevelView);
samplerInfo.minLod = ResolveEffectiveMinLod(sampler, samplerInfo.maxLod);
@@ -281,24 +281,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
SamplerCompareFunc VkSamplerManager::ResolveCompareFunc(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture) {
const auto compareFunc = sampler.GetSamplerCompareFunc();
if (sampler.GetCompareMode() == SamplerCompareMode::CompareToTexture &&
IsDepthTextureFormat(texture.GetFormat()) && compareFunc == SamplerCompareFunc::Always) {
return SamplerCompareFunc::LessEqual;
}
return compareFunc;
}
VkBorderColor VkSamplerManager::ResolveVkBorderColor(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture) {
if (!UsesBorderColor(sampler)) {
return VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK;
}
const auto& borderColor = texture.GetBorderColor();
// Border colour is sampler state: a bound sampler object supplies its own, and a texture
// with none reaches the very same value through the sampler object it owns.
const auto& borderColor = sampler.GetBorderColor();
const Bool isDepthTexture = IsDepthTextureFormat(texture.GetFormat());
if (isDepthTexture) {
@@ -69,8 +69,6 @@ private:
static VkSamplerMipmapMode ToVkMipmapMode(SamplerMipmapMode mode);
static VkSamplerAddressMode ToVkAddressMode(SamplerWrapMode mode);
static VkCompareOp ToVkCompareOp(SamplerCompareFunc func);
static SamplerCompareFunc ResolveCompareFunc(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture);
static VkBorderColor ResolveVkBorderColor(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture);
// The anisotropy Vulkan will actually apply: 1.0 (i.e. disabled) unless the feature is on and
@@ -120,31 +120,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
static Bool TryResolveSampleCountFlagBits(Int requestedSamples, VkSampleCountFlagBits& outSampleCount) {
switch (requestedSamples) {
case 1:
// GL promises "at least the requested samples", so a non-power-of-two
// request (legal in GL, e.g. 3) rounds up to the next Vulkan bit.
if (requestedSamples <= 1) {
outSampleCount = VK_SAMPLE_COUNT_1_BIT;
return true;
case 2:
outSampleCount = VK_SAMPLE_COUNT_2_BIT;
return true;
case 4:
outSampleCount = VK_SAMPLE_COUNT_4_BIT;
return true;
case 8:
outSampleCount = VK_SAMPLE_COUNT_8_BIT;
return true;
case 16:
outSampleCount = VK_SAMPLE_COUNT_16_BIT;
return true;
case 32:
outSampleCount = VK_SAMPLE_COUNT_32_BIT;
return true;
case 64:
outSampleCount = VK_SAMPLE_COUNT_64_BIT;
return true;
default:
}
if (requestedSamples > 64) {
return false;
}
Uint32 bit = 1;
while (bit < static_cast<Uint32>(requestedSamples)) {
bit <<= 1;
}
outSampleCount = static_cast<VkSampleCountFlagBits>(bit);
return true;
}
static Bool IsCubeMapFaceUploadTarget(TextureUploadTarget target) {
@@ -607,7 +597,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
void VkTextureManager::Shutdown() {
if (m_device != VK_NULL_HANDLE) {
ReclaimCompletedUploads(/*waitAll=*/true);
}
DestroyDeferredReleases();
++m_resourceEraseEpoch; // every memoized resource pointer dies with the map
m_textureResources.clear();
m_aliveObjects.clear();
m_storageImageTextures.clear();
@@ -629,6 +623,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
frameIndex, m_deferredViewReleases.size());
m_currentFrameIndex = frameIndex;
CollectDeferredReleases(frameIndex);
ReclaimCompletedUploads();
// Frame-boundary GC: every 64 frame boundaries (~1 s at 60 fps) bounds the reclaim
// latency for dead textures regardless of draw traffic — workloads that churn
@@ -659,6 +654,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
m_aliveObjects.erase(identity);
m_storageImageTextures.erase(identity);
// Invalidate every cross-draw sampled-texture memo: the erased
// resource's address may be reused by a future emplace.
++m_resourceEraseEpoch;
}
void VkTextureManager::PruneStaleTextureAliases(MG_State::GLState::ITextureObject* texture) {
@@ -714,45 +712,63 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
auto aliveIt = m_aliveObjects.find(identity);
if (aliveIt != m_aliveObjects.end() && aliveIt->second.expired()) {
EraseTrackedTexture(aliveIt->first);
aliveIt = m_aliveObjects.end();
}
// Only (re)register and prune when this (texture, lifetime) pair is new: stale
// aliases can only come into existence through an address reuse, which by
// construction introduces a new identity. Doing this unconditionally made every
// sampled-texture sync scan the entire alive-texture map per draw.
if (aliveIt == m_aliveObjects.end()) {
WeakPtr<MG_State::GLState::ITextureObject> aliveTexture;
const auto& liveTexture = MG_State::pGLContext->GetTextureObject(texture.GetExternalIndex());
if (liveTexture && liveTexture.get() == &texture) {
aliveTexture = liveTexture;
} else {
// The name lookup legally fails while the object is alive: the name was
// deleted with the texture still attached to an FBO (the attachment's
// SharedPtr keeps it alive), or the name was reused by a new texture, or
// this is a default texture object (name 0 lives outside the name map).
// Register through the object's own control block so the resource created
// below still participates in weak-expiry GC instead of becoming an
// orphan no reclamation path can reach until Shutdown.
aliveTexture = texture.weak_from_this();
}
if (!aliveTexture.expired()) {
m_aliveObjects[identity] = Move(aliveTexture);
PruneStaleTextureAliases(&texture);
// Cross-draw memo probe (see SyncedTextureMemoEntry): skips both map
// lookups and the (re)registration path for repeat-bound textures.
TextureResource* resourcePtr = nullptr;
for (Uint32 i = 0; i < kSyncedTextureMemoSize; ++i) {
const SyncedTextureMemoEntry& memo = m_syncedTextureMemo[i];
if (memo.texture == &texture && memo.lifetimeId == identity.lifetimeId &&
memo.eraseEpoch == m_resourceEraseEpoch) {
resourcePtr = memo.resource;
break;
}
}
auto it = m_textureResources.find(identity);
if (it == m_textureResources.end()) {
TextureResource initial{};
auto [insertIt, _] = m_textureResources.emplace(identity, Move(initial));
it = insertIt;
if (resourcePtr == nullptr) {
auto aliveIt = m_aliveObjects.find(identity);
if (aliveIt != m_aliveObjects.end() && aliveIt->second.expired()) {
EraseTrackedTexture(aliveIt->first);
aliveIt = m_aliveObjects.end();
}
// Only (re)register and prune when this (texture, lifetime) pair is new: stale
// aliases can only come into existence through an address reuse, which by
// construction introduces a new identity. Doing this unconditionally made every
// sampled-texture sync scan the entire alive-texture map per draw.
if (aliveIt == m_aliveObjects.end()) {
WeakPtr<MG_State::GLState::ITextureObject> aliveTexture;
const auto& liveTexture = MG_State::pGLContext->GetTextureObject(texture.GetExternalIndex());
if (liveTexture && liveTexture.get() == &texture) {
aliveTexture = liveTexture;
} else {
// The name lookup legally fails while the object is alive: the name was
// deleted with the texture still attached to an FBO (the attachment's
// SharedPtr keeps it alive), or the name was reused by a new texture, or
// this is a default texture object (name 0 lives outside the name map).
// Register through the object's own control block so the resource created
// below still participates in weak-expiry GC instead of becoming an
// orphan no reclamation path can reach until Shutdown.
aliveTexture = texture.weak_from_this();
}
if (!aliveTexture.expired()) {
m_aliveObjects[identity] = Move(aliveTexture);
PruneStaleTextureAliases(&texture);
}
}
auto it = m_textureResources.find(identity);
if (it == m_textureResources.end()) {
TextureResource initial{};
auto [insertIt, _] = m_textureResources.emplace(identity, Move(initial));
it = insertIt;
}
resourcePtr = &(it->second);
m_syncedTextureMemo[m_syncedTextureMemoNext] =
SyncedTextureMemoEntry{&texture, identity.lifetimeId, m_resourceEraseEpoch, resourcePtr};
m_syncedTextureMemoNext = (m_syncedTextureMemoNext + 1) % kSyncedTextureMemoSize;
}
if (!SyncTexture(texture, it->second)) {
if (!SyncTexture(texture, *resourcePtr)) {
MGLOG_D("%s: Syncing texture %d failed", __func__, texture.GetExternalIndex());
return nullptr;
}
@@ -766,11 +782,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
if (!recorded) {
m_drawSyncedThisDraw.push_back({identity, &(it->second)});
m_drawSyncedThisDraw.push_back({identity, resourcePtr});
}
}
return &(it->second);
return resourcePtr;
}
VkImageView VkTextureManager::GetOrCreateViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel) {
@@ -814,7 +830,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return VK_NULL_HANDLE;
}
if (baseArrayLayer == 0 && layerCount == resource->arrayLayers && viewType == resource->viewType) {
const Bool framebufferSrgbEnabled =
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
const VkFormat attachmentFormat = ResolveSrgbAttachmentWriteFormat(resource->format, framebufferSrgbEnabled);
if (attachmentFormat == resource->format && baseArrayLayer == 0 && layerCount == resource->arrayLayers &&
viewType == resource->viewType) {
return GetOrCreateViewAtMipLevel(texture, mipLevel);
}
@@ -823,6 +844,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
.baseArrayLayer = baseArrayLayer,
.layerCount = layerCount,
.viewType = viewType,
.viewFormat = attachmentFormat,
};
auto it = resource->attachmentViews.find(key);
if (it == resource->attachmentViews.end()) {
@@ -833,7 +855,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return attachmentView;
}
attachmentView = CreateImageView(resource->image, resource->format, resource->aspect, viewType,
attachmentView = CreateImageView(resource->image, attachmentFormat, resource->aspect, viewType,
mipLevel, 1, baseArrayLayer, layerCount);
if (attachmentView == VK_NULL_HANDLE) {
MGLOG_D("%s: CreateImageView failed for textureId=%d mipLevel=%u baseArrayLayer=%u layerCount=%u viewType=%d",
@@ -1049,6 +1071,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return view;
}
void VkTextureManager::StampTextureRecordingUse(MG_State::GLState::ITextureObject* texture) {
if (texture == nullptr) {
return;
}
auto it = m_textureResources.find(MakeTextureIdentity(texture));
if (it != m_textureResources.end()) {
it->second.lastRecordingGeneration = m_recordingGeneration;
}
}
void VkTextureManager::UpdateTrackedImageLayout(MG_State::GLState::ITextureObject* texture, VkImageLayout newLayout) {
MOBILEGL_ASSERT(texture != nullptr, "UpdateTrackedImageLayout: texture is null");
auto it = m_textureResources.find(MakeTextureIdentity(texture));
@@ -1076,6 +1108,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MOBILEGL_ASSERT(writtenMipLevel < resource.mipLevels,
"UpdateTrackedImageLayoutAfterAttachmentWrite: textureId=%d mipLevel=%u out of range %u",
texture->GetExternalIndex(), writtenMipLevel, resource.mipLevels);
// Pre-pass stream bookkeeping: the render pass that just ended wrote this image.
StampResourceRecordingUse(resource);
if (resource.layout != newLayout && resource.mipLevels > 1) {
VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
@@ -1160,6 +1194,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VK_ACCESS_SHADER_READ_BIT, resource->aspect, 0, resource->mipLevels,
resource->arrayLayers);
MOBILEGL_ASSERT(ok, "TransitionTextureForSampling: transition failed for textureId=%d", texture.GetExternalIndex());
// Pre-pass stream bookkeeping: a command referencing the image was recorded.
StampResourceRecordingUse(*resource);
return ok;
}
@@ -1189,6 +1225,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
resource->aspect, 0, resource->mipLevels, resource->arrayLayers);
MOBILEGL_ASSERT(ok, "TransitionTextureForStorageImage: transition failed for textureId=%d",
texture.GetExternalIndex());
// Pre-pass stream bookkeeping: a command referencing the image was recorded.
StampResourceRecordingUse(*resource);
return ok;
}
@@ -1208,6 +1246,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
!it->second.storageUsageResolved;
}
Bool VkTextureManager::NeedsMipChainGrowth(MG_State::GLState::ITextureObject& texture) const {
const TextureIdentity identity = MakeTextureIdentity(&texture);
const auto it = m_textureResources.find(identity);
// No image yet: the first sync sizes the chain from the levels the texture already
// defines, so nothing is recreated and there is nothing to order against.
if (it == m_textureResources.end() || it->second.image == VK_NULL_HANDLE) {
return false;
}
const TextureResource& resource = it->second;
const IntVec3 extent = {static_cast<Int>(resource.extent.width), static_cast<Int>(resource.extent.height),
static_cast<Int>(resource.depth)};
return resource.mipLevels < ComputeFullMipLevelCount(extent);
}
Bool VkTextureManager::NeedsStorageImagePreparation(MG_State::GLState::ITextureObject& texture) const {
const TextureIdentity identity = MakeTextureIdentity(&texture);
const auto it = m_textureResources.find(identity);
@@ -1406,11 +1458,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const IntVec3 &texelSize, SizeT byteSize, Uint32 mipLevels,
TextureResource &resource) {
const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(texture.GetFormat());
const VkFormat format = formatInfo.format;
VkFormat format = formatInfo.format;
if (format == VK_FORMAT_UNDEFINED) {
MGLOG_D("%s: format == VK_FORMAT_UNDEFINED", __func__);
return false;
}
// X8_D24 lacks optimal-tiling support on several drivers (lavapipe included);
// D32_SFLOAT holds every 24-bit depth value exactly, and the upload path
// converts the shadow words to float (see the pure-depth branch below).
if (format == VK_FORMAT_X8_D24_UNORM_PACK32) {
VkFormatProperties formatProperties{};
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties);
constexpr VkFormatFeatureFlags kDepthAttachmentAndSample =
VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT;
if ((formatProperties.optimalTilingFeatures & kDepthAttachmentAndSample) != kDepthAttachmentAndSample) {
format = VK_FORMAT_D32_SFLOAT;
}
}
if (texelSize.x() <= 0 || texelSize.y() <= 0 /*|| byteSize == 0*/) {
MGLOG_D("%s: texelSize or byteSize is zero", __func__);
return false;
@@ -1420,19 +1484,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return false;
}
const Bool isMultisampleTexture = IsMultisampleTextureUploadTarget(uploadTarget);
// A texture that has only ever defined level 0 gets a single-level backing
// (ANGLE's model). Preallocating the full chain put every render target
// onto Adreno's multi-mip image layout and grew each texture by a third
// for levels most textures never define. Once a second level is defined
// the backing is recreated ONE time with the full chain (the
// preserve-copy path below carries the pixels over), so sequentially-
// defined atlas mips do not recreate per level, and glGenerateMipmap -
// which defines every level before syncing - works unchanged.
const Uint32 backingMipLevels =
isMultisampleTexture ? 1u : std::max(mipLevels, ComputeFullMipLevelCount(texelSize));
isMultisampleTexture ? 1u
: (mipLevels > 1 ? std::max(mipLevels, ComputeFullMipLevelCount(texelSize)) : 1u);
TextureShapeInfo shapeInfo{};
const Bool supportedShape = TryResolveTextureShapeInfo(texture, uploadTarget, texelSize, shapeInfo);
MOBILEGL_ASSERT(supportedShape,
"SyncTextureResource: unsupported uploadTarget=%s textureTarget=%s textureId=%d size=(%d,%d,%d) "
"mipLevels=%u vkViewType=%d",
MG_Util::ConvertTextureUploadTargetToString(uploadTarget).c_str(),
MG_Util::ConvertTextureTargetToString(texture.GetTarget()).c_str(),
texture.GetExternalIndex(), texelSize.x(), texelSize.y(), texelSize.z(), mipLevels,
static_cast<Int>(MG_Util::ConvertTextureUploadTargetToVkEnum(uploadTarget)));
if (!supportedShape) {
MGLOG_D("%s: not Texture2D, unsupported", __func__);
// A gap in this backend's coverage, not a broken invariant: the GL front end accepts
// targets this manager has no Vulkan image shape for yet (cube map arrays above all).
// Declining the sync leaves the texture unbacked - wrong, but recoverable - where an
// assertion would take the whole process down instead.
MGLOG_W("SyncTextureResource: unsupported uploadTarget=%s textureTarget=%s textureId=%d size=(%d,%d,%d) "
"mipLevels=%u vkViewType=%d",
MG_Util::ConvertTextureUploadTargetToString(uploadTarget).c_str(),
MG_Util::ConvertTextureTargetToString(texture.GetTarget()).c_str(), texture.GetExternalIndex(),
texelSize.x(), texelSize.y(), texelSize.z(), mipLevels,
static_cast<Int>(MG_Util::ConvertTextureUploadTargetToVkEnum(uploadTarget)));
return false;
}
VkSampleCountFlagBits resolvedSampleCount = VK_SAMPLE_COUNT_1_BIT;
@@ -1443,6 +1518,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MG_Util::ConvertTextureUploadTargetToString(uploadTarget).c_str());
return false;
}
// glTexStorage*Multisample(samples = 1) is legal GL, but a one-sample image cannot back a
// sampler2DMS: VUID-RuntimeSpirv-samples-08726 forbids an OpTypeImage with MS = 1 from
// reading an image created with VK_SAMPLE_COUNT_1_BIT, and the fetch returns undefined data
// rather than an error. GL only promises "at least the requested number of samples", so
// giving a multisample texture two is both legal and the only way to keep the shader's view
// of it honest. GL_TEXTURE_SAMPLES still reports what the application asked for - that is
// read off the texture object, not off the image.
if (isMultisampleTexture && resolvedSampleCount == VK_SAMPLE_COUNT_1_BIT) {
resolvedSampleCount = VK_SAMPLE_COUNT_2_BIT;
}
const VkImageAspectFlags aspect = GetAspectMaskForFormat(format);
VkFormatProperties formatProperties{};
@@ -1473,6 +1558,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_mutableFormatUnsupported.find(format) == m_mutableFormatUnsupported.end()) {
imageCreateFlags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
}
// sRGB color images attach through their UNORM twin while GL_FRAMEBUFFER_SRGB is
// disabled (see ResolveSrgbAttachmentWriteFormat), which needs format-reinterpreting
// views - multisample sRGB render targets included.
if (ResolveSrgbAttachmentWriteFormat(format, false) != format &&
(aspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0 &&
m_mutableFormatUnsupported.find(format) == m_mutableFormatUnsupported.end()) {
imageCreateFlags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
}
VkImageUsageFlags desiredUsage =
VK_IMAGE_USAGE_SAMPLED_BIT |
@@ -1485,6 +1578,48 @@ namespace MobileGL::MG_Backend::DirectVulkan {
desiredUsage |= VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
}
// Round a multisample request up to a count the device supports for this
// format (GL only promises "at least"), mirroring the renderbuffer path.
if (isMultisampleTexture && resolvedSampleCount != VK_SAMPLE_COUNT_1_BIT) {
auto supportedIt = m_multisampleCountsByFormat.find(format);
if (supportedIt == m_multisampleCountsByFormat.end()) {
VkImageFormatProperties imageFormatProperties{};
VkSampleCountFlags supported = VK_SAMPLE_COUNT_1_BIT;
if (vkGetPhysicalDeviceImageFormatProperties(m_physicalDevice, format, shapeInfo.imageType,
VK_IMAGE_TILING_OPTIMAL, desiredUsage, imageCreateFlags,
&imageFormatProperties) == VK_SUCCESS) {
supported = imageFormatProperties.sampleCounts;
}
supportedIt = m_multisampleCountsByFormat.emplace(format, supported).first;
}
const VkSampleCountFlags supported = supportedIt->second;
if ((supported & resolvedSampleCount) == 0) {
Uint32 rounded = 0;
for (Uint32 bit = static_cast<Uint32>(resolvedSampleCount) << 1; bit <= VK_SAMPLE_COUNT_64_BIT;
bit <<= 1) {
if ((supported & bit) != 0) {
rounded = bit;
break;
}
}
if (rounded == 0) {
// Never land on one sample: that is the VUID-RuntimeSpirv-samples-08726
// violation the floor above exists to avoid, and it would come back silently
// for any format whose only supported count is 1.
for (Uint32 bit = static_cast<Uint32>(resolvedSampleCount) >> 1;
bit > static_cast<Uint32>(VK_SAMPLE_COUNT_1_BIT); bit >>= 1) {
if ((supported & bit) != 0) {
rounded = bit;
break;
}
}
}
if (rounded != 0) {
resolvedSampleCount = static_cast<VkSampleCountFlagBits>(rounded);
}
}
}
const Bool compatible = resource.image != VK_NULL_HANDLE && resource.format == format &&
resource.extent.width == static_cast<Uint32>(texelSize.x()) &&
resource.extent.height == static_cast<Uint32>(texelSize.y()) &&
@@ -1614,8 +1749,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VmaAllocationCreateInfo allocationInfo{};
allocationInfo.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE;
allocationInfo.requiredFlags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT;
VK_VERIFY(vmaCreateImage(m_allocator, &imageInfo, &allocationInfo, &resource.image, &resource.allocation, nullptr),
"vmaCreateImage(texture)");
// Soft failure like the unsupported-sample-count path above: a driver can pass the
// vkGetPhysicalDeviceImageFormatProperties pre-check yet still refuse the creation
// (e.g. multisampled depth on lavapipe); the texture simply stays unbacked.
const VkResult createImageResult =
vmaCreateImage(m_allocator, &imageInfo, &allocationInfo, &resource.image, &resource.allocation, nullptr);
if (createImageResult != VK_SUCCESS) {
MGLOG_F("SyncTextureResource: vmaCreateImage failed (%d) textureId=%d extent=%ux%u depth=%u layers=%u "
"mips=%u samples=%d format=%d",
createImageResult, texture.GetExternalIndex(), imageInfo.extent.width, imageInfo.extent.height,
imageInfo.extent.depth, imageInfo.arrayLayers, imageInfo.mipLevels,
static_cast<Int>(imageInfo.samples), static_cast<Int>(imageInfo.format));
resource.image = VK_NULL_HANDLE;
resource.allocation = nullptr;
return false;
}
++m_textureImageEpoch; // a new attachment image invalidates cached render passes
resource.layout = VK_IMAGE_LAYOUT_UNDEFINED;
@@ -1683,6 +1831,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_deferredViewReleases[frameIndex].clear();
}
void VkTextureManager::ReclaimCompletedUploads(Bool waitAll) {
if (m_pendingUploadReclaims.empty()) {
return;
}
SizeT completed = 0;
for (; completed < m_pendingUploadReclaims.size(); ++completed) {
PendingUploadReclaim& entry = m_pendingUploadReclaims[completed];
if (waitAll) {
VK_VERIFY(vkWaitForFences(m_device, 1, &entry.fence, VK_TRUE, UINT64_MAX),
"vkWaitForFences(texture upload reclaim)");
} else if (vkGetFenceStatus(m_device, entry.fence) != VK_SUCCESS) {
break;
}
vkDestroyFence(m_device, entry.fence, nullptr);
vkFreeCommandBuffers(m_device, m_commandPool, 1, &entry.commandBuffer);
vmaDestroyBuffer(m_allocator, entry.stagingBuffer, entry.stagingAllocation);
}
m_pendingUploadReclaims.erase(m_pendingUploadReclaims.begin(),
m_pendingUploadReclaims.begin() + static_cast<std::ptrdiff_t>(completed));
}
void VkTextureManager::DestroyDeferredReleases() {
for (auto& deferredReleases : m_deferredReleases) {
deferredReleases.clear();
@@ -1884,17 +2054,119 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return true;
}
// Combined depth-stencil images need per-aspect de-interleaved copies (VkBufferImageCopy
// aspectMask must have exactly one bit set). Until that is implemented, skip the upload
// instead of recording an invalid command buffer that kills the process.
// Combined depth-stencil images need per-aspect copies (VkBufferImageCopy aspectMask
// must have exactly one bit set), so de-interleave the shadow's GL wire format into
// a depth plane followed by a stencil plane per upload item.
const VkImageAspectFlags uploadAspectMask = GetAspectMaskForFormat(outResource.format);
if ((uploadAspectMask & VK_IMAGE_ASPECT_DEPTH_BIT) && (uploadAspectMask & VK_IMAGE_ASPECT_STENCIL_BIT)) {
MGLOG_E("UploadDirtyMipLevels: skipping unimplemented depth-stencil data upload for textureId=%d",
mipmapTexture.GetExternalIndex());
for (const auto& item : uploadItems) {
mipmapTexture.MarkStorageDirty(item.target, item.level, false);
const Bool isCombinedDepthStencil =
(uploadAspectMask & VK_IMAGE_ASPECT_DEPTH_BIT) && (uploadAspectMask & VK_IMAGE_ASPECT_STENCIL_BIT);
if (isCombinedDepthStencil) {
const Bool srcIsD24S8 = outResource.format == VK_FORMAT_D24_UNORM_S8_UINT;
const Bool srcIsD32FS8 = outResource.format == VK_FORMAT_D32_SFLOAT_S8_UINT;
if (!srcIsD24S8 && !srcIsD32FS8) {
MGLOG_E("UploadDirtyMipLevels: unsupported combined depth-stencil format %d for textureId=%d",
static_cast<Int>(outResource.format), mipmapTexture.GetExternalIndex());
for (const auto& item : uploadItems) {
mipmapTexture.MarkStorageDirty(item.target, item.level, false);
}
return true;
}
stagingSize = 0;
for (auto& item : uploadItems) {
const SizeT texelCount = static_cast<SizeT>(item.texelSize.x()) *
static_cast<SizeT>(item.texelSize.y()) *
static_cast<SizeT>(std::max(item.texelSize.z(), 1));
const SizeT shadowTexelSize = item.uploadByteSize / std::max<SizeT>(texelCount, 1);
MOBILEGL_ASSERT(shadowTexelSize == 4 || shadowTexelSize == 8,
"UploadDirtyMipLevels: unexpected depth-stencil shadow texel size %zu for textureId=%d",
shadowTexelSize, mipmapTexture.GetExternalIndex());
// Depth plane as the aspect's buffer-copy format (32-bit word for
// D24: low 24 bits; float for D32F), then one stencil byte per texel.
Vector<Uint8> deinterleaved(texelCount * 4 + texelCount);
Uint8* depthPlane = deinterleaved.data();
Uint8* stencilPlane = deinterleaved.data() + texelCount * 4;
const Uint8* shadow = static_cast<const Uint8*>(item.source);
for (SizeT t = 0; t < texelCount; ++t) {
if (shadowTexelSize == 8) {
// GL_FLOAT_32_UNSIGNED_INT_24_8_REV: float depth, then a word
// with stencil in its low 8 bits.
float depthValue;
Uint32 stencilWord;
std::memcpy(&depthValue, shadow + t * 8, sizeof(depthValue));
std::memcpy(&stencilWord, shadow + t * 8 + 4, sizeof(stencilWord));
if (srcIsD32FS8) {
std::memcpy(depthPlane + t * 4, &depthValue, sizeof(depthValue));
} else {
const float clamped = std::min(std::max(depthValue, 0.0f), 1.0f);
const Uint32 depthWord = static_cast<Uint32>(clamped * 16777215.0f + 0.5f);
std::memcpy(depthPlane + t * 4, &depthWord, sizeof(depthWord));
}
stencilPlane[t] = static_cast<Uint8>(stencilWord & 0xFFu);
} else {
// GL_UNSIGNED_INT_24_8: depth in the high 24 bits, stencil low 8.
Uint32 packed;
std::memcpy(&packed, shadow + t * 4, sizeof(packed));
if (srcIsD24S8) {
const Uint32 depthWord = packed >> 8;
std::memcpy(depthPlane + t * 4, &depthWord, sizeof(depthWord));
} else {
const float depthValue = static_cast<float>(packed >> 8) / 16777215.0f;
std::memcpy(depthPlane + t * 4, &depthValue, sizeof(depthValue));
}
stencilPlane[t] = static_cast<Uint8>(packed & 0xFFu);
}
}
item.expandedData = Move(deinterleaved);
item.source = item.expandedData.data();
item.uploadByteSize = item.expandedData.size();
item.offset = stagingSize;
stagingSize += static_cast<VkDeviceSize>(item.uploadByteSize);
}
}
// Pure-depth images whose canonical shadow layout differs from the image texel
// layout (the shadow keeps a full-scale 16/32-bit unorm word or a float; the
// image may be X8_D24 or a D32_SFLOAT fallback) convert per texel here.
if (uploadAspectMask == VK_IMAGE_ASPECT_DEPTH_BIT) {
const TextureInternalFormat depthInternal = mipmapTexture.GetFormat();
const Bool shadowIsFloat = depthInternal == TextureInternalFormat::DepthComponent32F;
const Bool dstIsFloat = outResource.format == VK_FORMAT_D32_SFLOAT;
const Bool dstIsD24Word = outResource.format == VK_FORMAT_X8_D24_UNORM_PACK32;
stagingSize = 0;
for (auto& item : uploadItems) {
const SizeT texelCount = static_cast<SizeT>(item.texelSize.x()) *
static_cast<SizeT>(item.texelSize.y()) *
static_cast<SizeT>(std::max(item.texelSize.z(), 1));
const SizeT shadowTexelSize = item.uploadByteSize / std::max<SizeT>(texelCount, 1);
const Bool needsConversion =
(dstIsFloat && !shadowIsFloat) || (dstIsD24Word && shadowTexelSize == 4 && !shadowIsFloat);
if (needsConversion) {
Vector<Uint8> converted(texelCount * 4);
const Uint8* shadow = static_cast<const Uint8*>(item.source);
for (SizeT t = 0; t < texelCount; ++t) {
Uint32 wide = 0;
if (shadowTexelSize == 2) {
Uint16 raw = 0;
std::memcpy(&raw, shadow + t * 2, sizeof(raw));
wide = (static_cast<Uint32>(raw) << 16) | raw;
} else {
std::memcpy(&wide, shadow + t * 4, sizeof(wide));
}
if (dstIsFloat) {
const float value = static_cast<float>(static_cast<double>(wide) / 4294967295.0);
std::memcpy(converted.data() + t * 4, &value, sizeof(value));
} else { // X8_D24: depth in the low 24 bits of a 32-bit word
const Uint32 word = wide >> 8;
std::memcpy(converted.data() + t * 4, &word, sizeof(word));
}
}
item.expandedData = Move(converted);
item.source = item.expandedData.data();
item.uploadByteSize = item.expandedData.size();
}
item.offset = stagingSize;
stagingSize += static_cast<VkDeviceSize>(item.uploadByteSize);
}
return true;
}
VkBuffer stagingBuffer = VK_NULL_HANDLE;
@@ -1947,7 +2219,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
aspectMask, 0, outResource.mipLevels, outResource.arrayLayers);
MOBILEGL_ASSERT(ok, "TransitionImageLayout to VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL failed");
// Array textures keep their GL "depth" in VkImage array layers, so the
// copy must address layerCount, not imageExtent.depth (which is invalid
// for 2D images and silently dropped every layer past the first).
const Bool depthSelectsArrayLayer = outResource.viewType == VK_IMAGE_VIEW_TYPE_1D_ARRAY ||
outResource.viewType == VK_IMAGE_VIEW_TYPE_2D_ARRAY ||
outResource.viewType == VK_IMAGE_VIEW_TYPE_CUBE_ARRAY;
for (const auto& item : uploadItems) {
const Uint32 depthOrLayers = item.texelSize.z() > 0 ? static_cast<Uint32>(item.texelSize.z()) : 1u;
VkBufferImageCopy copy{};
copy.bufferOffset = item.offset;
copy.bufferRowLength = 0;
@@ -1955,10 +2234,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
copy.imageSubresource.aspectMask = aspectMask;
copy.imageSubresource.mipLevel = item.level;
copy.imageSubresource.baseArrayLayer = item.baseArrayLayer;
copy.imageSubresource.layerCount = 1;
copy.imageSubresource.layerCount = depthSelectsArrayLayer ? depthOrLayers : 1;
copy.imageOffset = {0, 0, 0};
copy.imageExtent = {static_cast<Uint32>(item.texelSize.x()), static_cast<Uint32>(item.texelSize.y()),
item.texelSize.z() > 0 ? static_cast<Uint32>(item.texelSize.z()) : 1u};
depthSelectsArrayLayer ? 1u : depthOrLayers};
if (isCombinedDepthStencil) {
const SizeT texelCount = static_cast<SizeT>(item.texelSize.x()) *
static_cast<SizeT>(item.texelSize.y()) *
static_cast<SizeT>(std::max(item.texelSize.z(), 1));
VkBufferImageCopy depthCopy = copy;
depthCopy.imageSubresource.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
VkBufferImageCopy stencilCopy = copy;
stencilCopy.imageSubresource.aspectMask = VK_IMAGE_ASPECT_STENCIL_BIT;
stencilCopy.bufferOffset = item.offset + static_cast<VkDeviceSize>(texelCount) * 4;
const VkBufferImageCopy copies[2] = {depthCopy, stencilCopy};
vkCmdCopyBufferToImage(commandBuffer, stagingBuffer, outResource.image,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 2, copies);
continue;
}
vkCmdCopyBufferToImage(commandBuffer, stagingBuffer, outResource.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
1, &copy);
}
@@ -1989,11 +2282,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VK_VERIFY(vkCreateFence(m_device, &fenceInfo, nullptr, &uploadFence), "vkCreateFence(texture upload)");
VK_VERIFY(vkQueueSubmit(m_graphicsQueue, 1, &submitInfo, uploadFence), "vkQueueSubmit(texture)");
VK_VERIFY(vkWaitForFences(m_device, 1, &uploadFence, VK_TRUE, UINT64_MAX), "vkWaitForFences(texture upload)");
vkDestroyFence(m_device, uploadFence, nullptr);
vkFreeCommandBuffers(m_device, m_commandPool, 1, &commandBuffer);
vmaDestroyBuffer(m_allocator, stagingBuffer, stagingAllocation);
// Do NOT wait the fence here: this submit sits behind the previous
// frame's rendering on the queue, so a synchronous wait stalls the CPU
// until the GPU drains - a per-frame vkQueueWaitIdle for any workload
// with animated textures. Ordering against the current frame's draws is
// already guaranteed (its command buffer is submitted later, at
// present), so only the transient objects need to survive execution;
// park them until the fence signals.
m_pendingUploadReclaims.push_back({uploadFence, commandBuffer, stagingBuffer, stagingAllocation});
ReclaimCompletedUploads();
// Backstop for pathological upload storms: bound in-flight staging
// memory by blocking on the oldest upload only once the list is deep.
constexpr SizeT kMaxPendingTextureUploads = 16;
if (m_pendingUploadReclaims.size() > kMaxPendingTextureUploads) {
VK_VERIFY(vkWaitForFences(m_device, 1, &m_pendingUploadReclaims.front().fence, VK_TRUE, UINT64_MAX),
"vkWaitForFences(texture upload backstop)");
ReclaimCompletedUploads();
}
if (!ok) {
MGLOG_D("%s: texture upload cmd failed", __func__);
@@ -28,6 +28,9 @@ public:
// manager keys its per-draw fast path on this so an attachment's image recreation
// invalidates the cached render pass (dirty-flag tracking; portable to Vulkan 1.1).
Uint64 GetTextureImageEpoch() const { return m_textureImageEpoch; }
// Bumped whenever any tracked texture resource is erased; cached
// TextureResource pointers are valid only while this is unchanged.
Uint64 GetResourceEraseEpoch() const { return m_resourceEraseEpoch; }
struct TextureIdentity {
MG_State::GLState::ITextureObject* texture = nullptr;
@@ -64,12 +67,16 @@ public:
Uint32 baseArrayLayer = 0;
Uint32 layerCount = 1;
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
// May differ from the image format: sRGB images attach through their UNORM
// twin while GL_FRAMEBUFFER_SRGB is disabled.
VkFormat viewFormat = VK_FORMAT_UNDEFINED;
Bool operator==(const AttachmentViewKey& other) const {
return mipLevel == other.mipLevel &&
baseArrayLayer == other.baseArrayLayer &&
layerCount == other.layerCount &&
viewType == other.viewType;
viewType == other.viewType &&
viewFormat == other.viewFormat;
}
};
@@ -80,6 +87,8 @@ public:
hash ^= std::hash<Uint32>{}(key.layerCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.viewType)) +
0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.viewFormat)) +
0x9e3779b9u + (hash << 6) + (hash >> 2);
return hash;
}
};
@@ -172,6 +181,13 @@ public:
// NeedsStorageImagePreparation cannot ask for a recreate that will never happen.
Bool storageUsageResolved = false;
Uint16 syncedTextureParamsVersion = 0;
// Recording generation (VkTextureManager::GetRecordingGeneration) of the last
// command referencing this image that was recorded into the CURRENT frame
// command buffer. An image untouched by the open recording may have its
// out-of-pass work (deferred clears, sampled-layout transitions) recorded
// into the frame's PRE command buffer - which executes strictly before the
// frame's commands - instead of splitting the active render pass.
Uint64 lastRecordingGeneration = 0;
// Snapshot of ITextureObject::GetContentVersion() at the last successful sync;
// lets SyncTexture skip the whole re-check/re-upload when content is unchanged.
Uint64 syncedContentVersion = 0;
@@ -207,6 +223,7 @@ public:
std::swap(this->usageFlags, that.usageFlags);
std::swap(this->storageUsageResolved, that.storageUsageResolved);
std::swap(this->syncedTextureParamsVersion, that.syncedTextureParamsVersion);
std::swap(this->lastRecordingGeneration, that.lastRecordingGeneration);
std::swap(this->syncedContentVersion, that.syncedContentVersion);
std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount);
}
@@ -307,6 +324,21 @@ public:
VkImageLayout newLayout);
Bool TransitionTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
Bool TransitionTextureForStorageImage(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
// Recording-generation bookkeeping for the pre-pass command stream. The
// generation advances every time the frame command buffer (re)begins
// recording; a resource whose stamp does not match was not referenced by
// any command in the open recording, so its out-of-pass work may safely
// execute ahead of the whole recording (in the pre command buffer).
void AdvanceRecordingGeneration() { ++m_recordingGeneration; }
void StampResourceRecordingUse(TextureResource& resource) const {
resource.lastRecordingGeneration = m_recordingGeneration;
}
// Map-lookup variant for callers that only hold the GL texture object.
void StampTextureRecordingUse(MG_State::GLState::ITextureObject* texture);
Bool WasTouchedThisRecording(const TextureResource& resource) const {
return resource.lastRecordingGeneration == m_recordingGeneration;
}
// Records that this texture is bound to a GL image unit, so its image must carry
// VK_IMAGE_USAGE_STORAGE_BIT. Must be called before NeedsStorageImagePreparation, and
// therefore before the render pass is committed: an image that has to be upgraded is
@@ -318,6 +350,10 @@ public:
// will recreate it with STORAGE usage and copy the old contents forward. Callers use this to
// submit their pending recording first, so that copy cannot read pre-flush content.
Bool NeedsStorageUsageUpgrade(MG_State::GLState::ITextureObject& texture) const;
// The same ordering question for the other recreate-and-preserve trigger: true when this
// texture's live image carries a shorter mip chain than a full one, so defining the missing
// levels recreates it and copies the old contents forward.
Bool NeedsMipChainGrowth(MG_State::GLState::ITextureObject& texture) const;
// Non-mutating probe for the per-draw storage-image fast path: true when preparing this
// texture as a storage image may need work that is illegal inside a render pass (resource
// creation, dirty-content upload, or a layout transition to GENERAL). Unknown state reports
@@ -364,6 +400,9 @@ public:
private:
// Bumped in SyncTextureResource right after vmaCreateImage(texture). See GetTextureImageEpoch().
Uint64 m_textureImageEpoch = 1;
// See AdvanceRecordingGeneration. Starts above every resource's default
// stamp of 0 so a fresh resource counts as untouched.
Uint64 m_recordingGeneration = 1;
Bool SyncTexture(MG_State::GLState::ITextureObject &texture,
TextureResource &outResource);
@@ -394,6 +433,11 @@ private:
void DeferViewRelease(VkImageView view);
void CollectDeferredReleases(Uint32 frameIndex);
void DestroyDeferredReleases();
// Frees the fence/command buffer/staging buffer of every in-flight texture
// upload whose fence has signaled (submission order = completion order on
// the single queue, so the scan stops at the first still-pending entry).
// waitAll blocks on every entry - Shutdown's drain.
void ReclaimCompletedUploads(Bool waitAll = false);
static TextureIdentity MakeTextureIdentity(MG_State::GLState::ITextureObject* texture);
void EraseTrackedTexture(const TextureIdentity& identity);
void PruneStaleTextureAliases(MG_State::GLState::ITextureObject* texture);
@@ -423,6 +467,23 @@ private:
TextureResource* resource = nullptr;
};
Vector<DrawSyncedTexture> m_drawSyncedThisDraw;
// Cross-draw sampled-texture memo: the same few textures (atlas, lightmap)
// are resolved on every draw, so cache their resource pointers and skip the
// alive/resource map lookups. Node-based std::unordered_map keeps the
// pointees stable across inserts; erases bump m_resourceEraseEpoch, which
// every memo entry must match. SyncTexture still runs on memo hits, so
// content/param freshness is unaffected. A dead-then-reused texture address
// cannot false-hit: the new object carries a new lifetime id.
struct SyncedTextureMemoEntry {
const MG_State::GLState::ITextureObject* texture = nullptr;
Uint64 lifetimeId = 0;
Uint64 eraseEpoch = 0;
TextureResource* resource = nullptr;
};
static constexpr Uint32 kSyncedTextureMemoSize = 8;
SyncedTextureMemoEntry m_syncedTextureMemo[kSyncedTextureMemoSize];
Uint32 m_syncedTextureMemoNext = 0;
Uint64 m_resourceEraseEpoch = 1;
// Formats whose mutable-image probe failed on this device; their images are created
// without MUTABLE_FORMAT_BIT so repeat syncs neither re-probe nor flag-mismatch.
std::unordered_set<VkFormat> m_mutableFormatUnsupported;
@@ -430,7 +491,21 @@ private:
std::unordered_map<TextureIdentity, TextureResource, TextureIdentityHash> m_textureResources;
// Textures that have been bound to a GL image unit (see MarkStorageImageTexture).
std::unordered_set<TextureIdentity, TextureIdentityHash> m_storageImageTextures;
// Supported multisample counts per format, so repeat texture syncs do not
// re-query vkGetPhysicalDeviceImageFormatProperties.
std::unordered_map<VkFormat, VkSampleCountFlags> m_multisampleCountsByFormat;
Vector<Vector<TextureResource>> m_deferredReleases;
Vector<Vector<VkImageView>> m_deferredViewReleases;
// Texture uploads are submitted out-of-band but NOT waited on (waiting
// behind the queue serialized the CPU against the previous frame's GPU
// work every time an animated atlas re-uploaded). Their transient objects
// are parked here and reclaimed once the upload fence signals.
struct PendingUploadReclaim {
VkFence fence = VK_NULL_HANDLE;
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
VkBuffer stagingBuffer = VK_NULL_HANDLE;
VmaAllocation stagingAllocation = nullptr;
};
Vector<PendingUploadReclaim> m_pendingUploadReclaims;
};
} // namespace MobileGL::MG_Backend::DirectVulkan
File diff suppressed because it is too large Load Diff
@@ -76,6 +76,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
GLenum indexType = GL_UNSIGNED_SHORT;
SizeT indexByteOffset = 0;
SizeT indexByteSize = 0;
// Interpret indexByteOffset as a raw client pointer even when an element
// array buffer is bound (backend-synthesized index lists, e.g. the
// GL_LINE_LOOP -> LINE_STRIP rewrite).
Bool forceClientMemory = false;
};
struct DrawIndexedCmd {
@@ -151,6 +155,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool SetupDraw(FrameContext::FrameData& frame, GLenum mode, Flags<DrawSetupAspect> aspects,
const DrawCmdParam& drawParams,
const IndexBufferView* pIndexBufferView = nullptr);
// ANGLE-style consecutive-draw fast path: SetupDraw snapshots the fully
// resolved draw configuration; the next draw whose cheap version/identity
// checks all match skips the resolution half (LOD probe, sampled-set
// walk, render-pass and pipeline resolution) and jumps straight to the
// per-draw tail. Returns false (leaving no side effects that the full
// path cannot redo idempotently) whenever anything might have changed.
Bool TrySetupDrawFastPath(FrameContext::FrameData& frame, GLenum mode, Flags<DrawSetupAspect> aspects,
const DrawCmdParam& drawParams, const IndexBufferView* pIndexBufferView);
void ClearAttachmentsOnActiveRenderPass(VkCommandBuffer commandBuffer,
const RenderPassEntry& compatibleRenderPassEntry);
@@ -169,6 +181,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void ClearBufferiv(GLenum buffer, GLint drawbuffer, const GLint* value);
void ClearNamedFramebufferfv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
GLenum buffer, GLint drawbuffer, const GLfloat* value);
void ClearNamedFramebufferiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
GLenum buffer, GLint drawbuffer, const GLint* value);
void ClearNamedFramebufferuiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
GLenum buffer, GLint drawbuffer, const GLuint* value);
void ClearNamedFramebufferfi(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
@@ -188,6 +204,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
void GenerateMipmap(GLenum target);
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels);
// GL_DEPTH_COMPONENT / GL_DEPTH_STENCIL / GL_STENCIL_INDEX readback from the
// read framebuffer's depth/stencil attachment (per-aspect buffer copies with
// CPU repacking into the requested client layout).
void ReadDepthStencilPixels(MG_State::GLState::FramebufferObject& readFbo, GLint x, GLint y, GLsizei width,
GLsizei height, GLenum format, GLenum type, void* pixels);
// Copy-and-repack core shared by depth-stencil ReadPixels and GetTexImage;
// expects command recording to be active and any render pass already ended.
void ReadDepthStencilImageToClient(VkImage image, VkFormat vkFormat, VkImageLayout* trackedLayout,
VkImageAspectFlags imageAspect, Uint32 mipLevel, Uint32 baseArrayLayer,
GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type,
void* pixels);
// Same-extent depth blit between images of different depth formats: host
// round-trip with a per-texel re-encode (see BlitNamedFramebuffer).
Bool BlitDepthAcrossFormats(FrameContext::FrameData& frame, VkImage srcImage, VkFormat srcFormat,
VkImageLayout* srcTrackedLayout, Uint32 srcMipLevel, Uint32 srcBaseArrayLayer,
VkImage dstImage, VkFormat dstFormat, VkImageLayout* dstTrackedLayout,
Uint32 dstMipLevel, Uint32 dstBaseArrayLayer, GLint srcX, GLint srcY, GLint dstX,
GLint dstY, GLint width, GLint height, VkImageLayout srcRestoreLayout,
VkImageLayout dstRestoreLayout, Bool stencilAspect);
static SizeT GetReadbackTexelSize(VkFormat sourceFormat);
static Bool ConvertReadbackPixels(const Uint8* sourcePixels, VkFormat sourceFormat,
GLsizei width, GLsizei height, GLenum destinationFormat,
@@ -273,6 +308,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const VkTimerQueryManager::TimestampRecord& end) const;
Uint64 GetTimerQueryTimestampNs(const VkTimerQueryManager::TimestampRecord& record) const;
// GL_SAMPLES_PASSED occlusion queries: every app draw between Start and Stop is
// wrapped in a Vulkan occlusion query slot; the result is the slot sum. Requires
// hostQueryReset for slot recycling - Start fails (frontend keeps the query
// unsupported) when the device lacks it.
Bool StartOcclusionQueryCapture();
void StopOcclusionQueryCapture(Vector<Uint32>& outSlots);
// Flushes pending commands, waits, sums the slots, and recycles them.
Bool ResolveOcclusionQueryResult(const Vector<Uint32>& slots, Uint64& outSamples);
void RequestSwapchainResize(Uint32 width, Uint32 height);
// Re-query the surface and report whether the live swapchain no longer matches it
// (size or orientation). This - not a VK_SUBOPTIMAL_KHR result - is what decides a
@@ -391,6 +435,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void* m_platformDisplay = nullptr;
void* m_platformLibrary = nullptr;
void* m_platformCloseDisplay = nullptr;
// Some real ICDs (e.g. NVIDIA's proprietary Linux driver) don't implement
// VK_EXT_headless_surface at all. Detected once in CreateInstance() from the
// enumerated instance extensions; when false, CreateSurface() falls back to a
// hidden Xlib window instead of vkCreateHeadlessSurfaceEXT.
Bool m_headlessSurfaceSupported = true;
// Set when CreateSurface() had to create its own Xlib window for the fallback
// above (rather than being handed one by the caller), so Shutdown() knows it
// owns that window and must destroy it.
Bool m_ownsFallbackXlibWindow = false;
VulkanRendererConfig m_config;
Bool m_swapchainResizeRequested = false;
// Presentation is suspended while the window is zero-area (minimized): the
@@ -443,6 +496,74 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 stride);
static inline PFNDrawIndexedIndirectCountFunc s_vkCmdDrawIndexedIndirectCount = nullptr;
// VK_EXT_transform_feedback (GL transform feedback capture)
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_vkCmdBeginTransformFeedbackEXT s_vkCmdBeginTransformFeedbackEXT = nullptr;
static inline PFN_vkCmdEndTransformFeedbackEXT s_vkCmdEndTransformFeedbackEXT = nullptr;
// Counter buffers (one 4-byte slot per capture binding) let consecutive
// draws within one glBeginTransformFeedback append GL-style. Transform feedback
// objects can each hold an open, paused span at the same time, so the counters are
// per object: one group of four slots each, handed out on first use.
static constexpr SizeT kXfbCounterObjectSlots = 16;
VkBufferObject m_xfbCounterBuffer;
UnorderedMap<Uint, Uint32> m_xfbCounterSlotByObject;
Uint32 m_xfbNextCounterSlot = 0;
// Set for a slot once a captured draw has been recorded into its span; selects
// counter-buffer resume on the next captured draw of the same span.
Array<Bool, kXfbCounterObjectSlots> m_xfbCountersValid{};
Array<Uint64, kXfbCounterObjectSlots> m_xfbLastSeenGeneration{};
// Counter slot group of the bound transform feedback object.
Uint32 CurrentXfbCounterSlot();
// Wraps a recorded draw with BeginTransformFeedbackEXT/EndTransformFeedbackEXT
// when GL transform feedback is active; binds capture buffers on demand.
Bool BeginXfbCaptureForDraw(FrameContext::FrameData& frame);
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
// query is active. Returns whether a slot was begun (End must mirror it).
Bool BeginOcclusionForDraw(VkCommandBuffer commandBuffer);
void EndOcclusionForDraw(VkCommandBuffer commandBuffer, Bool began);
Bool m_occlusionQueryPreciseEnabled = false;
Bool m_hostQueryResetEnabled = false;
PFN_vkResetQueryPool s_vkResetQueryPool = nullptr;
VkQueryPool m_occlusionQueryPool = VK_NULL_HANDLE;
static constexpr Uint32 kOcclusionQuerySlots = 8192;
Uint32 m_occlusionSlotCursor = 0;
Bool m_occlusionCaptureActive = false;
Vector<Uint32> m_occlusionActiveSlots;
// Transform feedback primitive queries: one pool slot per captured draw yields
// the (written, needed) pair; GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN sums the
// first, GL_PRIMITIVES_GENERATED the second - exact with geometry shaders,
// unlike the CPU fallback accounting.
Bool m_xfbQueriesSupported = false;
PFN_vkCmdBeginQueryIndexedEXT s_vkCmdBeginQueryIndexedEXT = nullptr;
PFN_vkCmdEndQueryIndexedEXT s_vkCmdEndQueryIndexedEXT = nullptr;
VkQueryPool m_xfbQueryPool = VK_NULL_HANDLE;
static constexpr Uint32 kXfbQuerySlots = 8192;
Uint32 m_xfbQuerySlotCursor = 0;
Bool m_xfbQueryCaptureActive[2] = {false, false}; // [0]=written, [1]=generated
Vector<Uint32> m_xfbQueryActiveSlots[2];
Bool m_xfbQuerySlotOpen = false;
Uint32 m_xfbQueryOpenSlot = 0;
public:
// kind: 0 = PRIMITIVES_WRITTEN, 1 = PRIMITIVES_GENERATED.
Bool StartXfbQueryCapture(Uint32 kind);
void StopXfbQueryCapture(Uint32 kind, Vector<Uint32>& outSlots);
Bool ResolveXfbQueryResult(const Vector<Uint32>& slots, Bool wantGenerated, Uint64& outPrimitives);
private:
void BeginXfbQueryForDraw(VkCommandBuffer commandBuffer);
void EndXfbQueryForDraw(VkCommandBuffer commandBuffer);
VkCommandPool m_commandPool = VK_NULL_HANDLE;
VkBufferManager m_bufferManager;
@@ -455,14 +576,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// gather + synthetic vertex-input rebuild + payload hash + lookup) when the full pipeline
// state is unchanged from the previous draw. The key provably covers every pipeline field.
// Reset per-frame and on pipeline destruction so the cached handle can never dangle.
Bool m_lastPipelineValid = false;
GLenum m_lastPipelineMode = 0;
Uint64 m_lastPipelineProgramHash = 0;
Uint64 m_lastPipelineVertexInputHash = 0;
Uint64 m_lastPipelineRenderPassHash = 0;
Uint m_lastPipelineRenderStateVersion = 0;
ProgramFactory::CompileOptionFlags m_lastPipelineTransformFlags = {};
VkPipeline m_lastPipelineResult = VK_NULL_HANDLE;
// Small N-way pipeline-resolution memo (round-robin replacement). A
// single-entry memo thrashed on draw sequences that alternate a few
// pipelines (GUI text/quad program ping-pong), paying the full
// payload-hash lookup per draw; eight entries cover such working sets
// while keeping the hit path a trivial linear scan.
struct PipelineMemoEntry {
GLenum mode = 0;
Uint64 programHash = 0;
Uint64 vertexInputHash = 0;
Uint64 renderPassHash = 0;
Uint renderStateVersion = 0;
ProgramFactory::CompileOptionFlags transformFlags = {};
VkPipeline pipeline = VK_NULL_HANDLE;
};
static constexpr Uint32 kPipelineMemoSize = 8;
PipelineMemoEntry m_pipelineMemo[kPipelineMemoSize];
Uint32 m_pipelineMemoCount = 0;
Uint32 m_pipelineMemoNext = 0;
// Drops every memoized pipeline handle. Required at command-buffer
// boundaries and whenever any pipeline may have been destroyed.
void InvalidatePipelineMemo() {
m_pipelineMemoCount = 0;
m_pipelineMemoNext = 0;
}
UnorderedMap<ProgramFactory::HashType, VkPipeline> m_computePipelines;
UniquePtr<ProgramFactory> m_programFactory;
UniquePtr<UniformManager> m_uniformManager;
@@ -491,9 +628,61 @@ namespace MobileGL::MG_Backend::DirectVulkan {
ProgramFactory::CompileOptionFlags m_lastSampledSetTransformFlags = {};
Uint64 m_lastSampledSetBindGeneration = 0;
// Memo for the per-draw explicit-LOD-0 eligibility probe
// (ProgramSamplesOnlySingleLevelTextures): same key family as the
// sampled-set memo, plus the sampled textures' params-version sum so a
// level-range or filter change re-probes. On a hit the resolved
// transform flags are reused, which also collapses the two
// GetOrCreateProgram lookups into one.
Bool m_lastLodDecisionValid = false;
Uint64 m_lastLodProgramLifetimeId = 0;
Uint32 m_lastLodProgramVersion = 0;
Uint64 m_lastLodBindGeneration = 0;
Uint64 m_lastLodParamsSum = 0;
ProgramFactory::CompileOptionFlags m_lastLodBaseFlags = {};
ProgramFactory::CompileOptionFlags m_lastLodResultFlags = {};
// Snapshot behind TrySetupDrawFastPath. Values only: the program and
// render-pass caches are open-addressing maps whose entries move on
// insert, so no pointers into them are cached; the pipeline handle is
// protected by the command-buffer-boundary reset plus the mid-frame
// pipeline-destruction resets, and monotonic epochs guard everything
// that can be destroyed or recreated between draws.
struct SetupDrawSnapshot {
Bool valid = false;
Uint8 aspects = 0;
GLenum mode = 0;
Uint64 programLifetimeId = 0;
Uint32 programVersion = 0;
const void* vao = nullptr;
Uint32 vaoConfigVersion = 0;
const void* drawFbo = nullptr;
Uint16 fboVersion = 0;
Bool drawFboIsDefault = false;
Uint renderStateVersion = 0;
Uint64 bindGeneration = 0;
Uint32 baseTransformFlags = 0;
Uint32 resolvedTransformFlags = 0;
Uint64 renderPassHash = 0;
Uint32 imageIndex = 0;
Uint64 textureEraseEpoch = 0;
Uint64 textureImageEpoch = 0;
Uint64 renderbufferImageEpoch = 0;
Uint64 sampledContentSum = 0;
Uint64 sampledParamsSum = 0;
IntVec2 renderPassExtent = {0, 0};
VkPipeline pipeline = VK_NULL_HANDLE;
};
SetupDrawSnapshot m_setupDrawSnapshot;
// Per-draw scratch buffers (clear keeps capacity) — these paths run for every
// draw call and must not allocate.
Vector<MG_State::GLState::ITextureObject*> m_sampledTexturesScratch;
// Parallel to m_sampledTexturesScratch, refilled by every SetupDraw's
// first sampled-texture loop: the resolved backend resources, so the
// post-transition loop can skip re-resolving textures whose layout is
// already sampleable.
Vector<VkTextureManager::TextureResource*> m_sampledResourcesScratch;
Vector<MG_State::GLState::ITextureObject*> m_storageImageTexturesScratch;
Vector<VkBuffer> m_vertexBuffersScratch;
Vector<VkDeviceSize> m_vertexOffsetsScratch;
@@ -618,6 +807,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkImageLayout finalLayout);
Bool SubmitReadbackCommandsAndWait(FrameContext::FrameData& frame);
public:
// Submits whatever is recorded and waits for it. The CPU is about to read memory
// a shader wrote (a mapped shader storage buffer), and coherent host-visible
// storage only guarantees visibility once the work that produced it has retired.
Bool FinishPendingGpuWork();
private:
void ShutdownSwapchain();
// Static functions
+32 -3
View File
@@ -52,19 +52,48 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
} // namespace MobileGL::MG_Backend::DirectVulkan
namespace MobileGL::MG_Backend::DirectVulkan {
// GL renders into sRGB color attachments RAW while GL_FRAMEBUFFER_SRGB is disabled
// (the core-profile default); Vulkan sRGB attachments always encode on write. The
// attachment view (and render pass format) therefore drops to the UNORM twin
// whenever the capability is off. Sampled views keep the sRGB format (decode on
// sample is unconditional in GL).
inline VkFormat ResolveSrgbAttachmentWriteFormat(VkFormat format, bool framebufferSrgbEnabled) {
if (framebufferSrgbEnabled) return format;
switch (format) {
case VK_FORMAT_R8G8B8A8_SRGB:
return VK_FORMAT_R8G8B8A8_UNORM;
case VK_FORMAT_B8G8R8A8_SRGB:
return VK_FORMAT_B8G8R8A8_UNORM;
default:
return format;
}
}
} // namespace MobileGL::MG_Backend::DirectVulkan
// The context line (__VA_ARGS__ = its own format string + args) must be a SEPARATE log
// call: appending its format to the base format while its arguments precede the base
// arguments makes every conversion read the wrong slot (a %s pulling an int crashes).
#define VK_VERIFY(expr, ...) \
do { \
VkResult _vk_verify_result = (expr); \
if (_vk_verify_result != VK_SUCCESS) { \
MGLOG_F("Vulkan error %s (%d) at %s:%d" __VA_OPT__(" - ") __VA_ARGS__, \
__VA_OPT__(MGLOG_F(__VA_ARGS__);) \
MGLOG_F("Vulkan error %s (%d) at %s:%d", \
MobileGL::MG_Backend::DirectVulkan::VkResultToString(_vk_verify_result), \
_vk_verify_result, __FILE__, __LINE__); \
} \
MOBILEGL_ASSERT(_vk_verify_result == VK_SUCCESS, "Vulkan error %s (%d) at %s:%d" __VA_OPT__(" - ") __VA_ARGS__, MobileGL::MG_Backend::DirectVulkan::VkResultToString(_vk_verify_result), _vk_verify_result, __FILE__, __LINE__); \
MOBILEGL_ASSERT(_vk_verify_result == VK_SUCCESS, "Vulkan error %s (%d) at %s:%d", \
MobileGL::MG_Backend::DirectVulkan::VkResultToString(_vk_verify_result), \
_vk_verify_result, __FILE__, __LINE__); \
} while (0)
#define XXHASH_VERIFY(expr, ...) \
do { \
XXH_errorcode _xxh_verify_result = (expr); \
MOBILEGL_ASSERT(_xxh_verify_result == XXH_OK, "XXHash error %d at %s:%d" __VA_OPT__(" - ") __VA_ARGS__, _xxh_verify_result, __FILE__, __LINE__); \
if (_xxh_verify_result != XXH_OK) { \
__VA_OPT__(MGLOG_F(__VA_ARGS__);) \
} \
MOBILEGL_ASSERT(_xxh_verify_result == XXH_OK, "XXHash error %d at %s:%d", _xxh_verify_result, __FILE__, \
__LINE__); \
} while (0)
+137 -14
View File
@@ -8,6 +8,9 @@
#include "GL_Buffer.h"
#include "Validators.h"
#include "../Texture/GL_Texture.h"
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
#include <MG_Util/Metrics/TextureMetrics.h>
#include <Config.h>
#include <MG_State/GLState/Core.h>
#include <MG_State/GLState/ErrorState/Error.h>
@@ -38,6 +41,7 @@ namespace MobileGL::MG_Impl::GLImpl {
GetNamedBufferParameteriv,
GetNamedBufferParameteri64v,
GetNamedBufferPointerv,
GetNamedBufferSubData,
};
const char* GetBufferOpName(BufferOp op) {
@@ -76,6 +80,8 @@ namespace MobileGL::MG_Impl::GLImpl {
return "UnmapNamedBuffer";
case BufferOp::FlushMappedNamedBufferRange:
return "FlushMappedNamedBufferRange";
case BufferOp::GetNamedBufferSubData:
return "GetNamedBufferSubData";
case BufferOp::GetNamedBufferParameteriv:
return "GetNamedBufferParameteriv";
case BufferOp::GetNamedBufferParameteri64v:
@@ -89,25 +95,64 @@ namespace MobileGL::MG_Impl::GLImpl {
SharedPtr<MG_State::GLState::BufferObject> GetNamedBufferObject(GLuint buffer, BufferOp op);
// The size of one cleared element, which is what offset and size must be multiples of
// (GL 4.6 core 6.3). `internalformat` is restricted to the buffer-texture format table, and
// `format`/`type` describe the client-side pattern, so both are validated here and the
// caller only has to know how wide an element is.
SizeT GetClearPatternSize(GLenum internalformat, GLenum format, GLenum type, BufferOp op) {
if (format != GL_RED_INTEGER) {
if (!IsBufferTextureInternalFormat(internalformat)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", GetBufferOpName(op),
"Only GL_RED_INTEGER buffer clears are currently supported."));
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", GetBufferOpName(op),
std::format("internalformat 0x{:X} is not one of the sized formats a buffer clear accepts.",
internalformat)));
return 0;
}
if (internalformat == GL_R8UI && type == GL_UNSIGNED_BYTE) return sizeof(GLubyte);
if (internalformat == GL_R32UI && type == GL_UNSIGNED_INT) return sizeof(GLuint);
// Unlike internalformat, a bad format or type here is INVALID_VALUE rather than
// INVALID_ENUM (GL 4.6 core 6.3) - the odd one out among the enum arguments.
const TextureInputFormat inputFormat = MG_Util::ConvertGLEnumToTextureInputFormat(format);
if (inputFormat == TextureInputFormat::Unknown) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", GetBufferOpName(op),
std::format("format 0x{:X} is not a pixel format.", format)));
return 0;
}
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", GetBufferOpName(op),
std::format("Unsupported clear format tuple: internalformat=0x{:X}, "
"format=0x{:X}, type=0x{:X}",
internalformat, format, type)));
return 0;
const TexturePixelDataType pixelType = MG_Util::ConvertGLEnumToTexturePixelDataType(type);
if (pixelType == TexturePixelDataType::Unknown) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", GetBufferOpName(op),
std::format("type 0x{:X} is not a pixel type.", type)));
return 0;
}
const TextureInternalFormat internal =
MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);
const SizeT elementSize = MG_Util::GetSizedInternalFormatSizeInBytes(internal);
if (elementSize == 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", GetBufferOpName(op),
std::format("internalformat 0x{:X} has no known element size.",
internalformat)));
return 0;
}
// The pattern is replicated verbatim, which is only the whole story while the client
// layout already matches the internal format - the case every entry point in practice
// uses, and the only one the conversion machinery here can express. Say so rather than
// quietly writing a differently-sized pattern.
const SizeT sourceSize = MG_Util::GetInputBytesPerPixel(inputFormat, pixelType);
if (sourceSize != elementSize) {
MGLOG_W("%s: clear pattern is %zu bytes but internalformat 0x%X stores %zu; "
"converting between them is not implemented",
GetBufferOpName(op), sourceSize, internalformat, elementSize);
}
return elementSize;
}
Bool ValidateBufferClearRange(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject, GLintptr offset,
@@ -330,12 +375,21 @@ namespace MobileGL::MG_Impl::GLImpl {
} else if (access & BufferMappingAccessBit::Write) {
*params = GL_WRITE_ONLY;
} else {
*params = 0;
*params = GL_READ_WRITE;
}
} else {
*params = 0;
// Initial value, and what glUnmapBuffer restores (GL 4.6 core table 6.2).
*params = GL_READ_WRITE;
}
break;
case GL_BUFFER_ACCESS_FLAGS:
// The MapBufferRange flags verbatim; glMapBuffer's access enum has already been
// normalised into the same bits. Zero while the buffer is not mapped.
*params = bufferObject->IsMapped()
? static_cast<GLint>(
MG_Util::ConvertBufferMappingAccessToGLEnum(bufferObject->GetMappingAccess()))
: 0;
break;
case GL_BUFFER_MAPPED:
*params = bufferObject->IsMapped() ? GL_TRUE : GL_FALSE;
break;
@@ -878,6 +932,45 @@ namespace MobileGL::MG_Impl::GLImpl {
return;
}
bufferObject->SyncGpuWrites();
bufferObject->DownloadSubData(data, static_cast<SizeT>(offset), static_cast<SizeT>(size));
}
void GetNamedBufferSubData_State(GLuint buffer, GLintptr offset, GLsizeiptr size, void* data) {
if (!data) {
// Match GetBufferSubData_State: a null pointer is a caller bug, not a GL-specified error.
return;
}
if (size < 0 || offset < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetNamedBufferSubData_State",
"Offset and size must be non-negative."));
return;
}
auto bufferObject = GetNamedBufferObject(buffer, BufferOp::GetNamedBufferSubData);
if (!bufferObject) return;
if (static_cast<SizeT>(offset) + static_cast<SizeT>(size) > bufferObject->GetSize()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetNamedBufferSubData_State",
"Offset and size exceed buffer size."));
return;
}
if (bufferObject->IsMapped() &&
!(bufferObject->GetMappingAccess() & BufferMappingAccessBit::Persistent)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetNamedBufferSubData_State",
"Cannot read from a buffer object mapped without GL_MAP_PERSISTENT_BIT."));
return;
}
bufferObject->SyncGpuWrites();
bufferObject->DownloadSubData(data, static_cast<SizeT>(offset), static_cast<SizeT>(size));
}
@@ -1351,6 +1444,14 @@ namespace MobileGL::MG_Impl::GLImpl {
BufferTarget bufferTarget = MG_Util::ConvertGLEnumToBufferTarget(target);
if (!BufferImpl::ValidateBufferBindingPointTarget(bufferTarget)) return;
if (!BufferImpl::ValidateBufferBindingPointIndex(bufferTarget, pointIndex)) return;
if (bufferTarget == BufferTarget::TransformFeedback && MG_State::pGLContext->IsTransformFeedbackActive()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"Transform feedback buffer bindings cannot change while transform "
"feedback is active."));
return;
}
MG_State::pGLContext->TouchBufferBindingPoint(bufferTarget, pointIndex);
auto& point = MG_State::pGLContext->GetBufferBindingPoint(bufferTarget, pointIndex);
@@ -1358,6 +1459,7 @@ namespace MobileGL::MG_Impl::GLImpl {
if (buffer == 0) {
point.Bind(nullptr);
point.SetRange(Range1D(0, 0));
GetBufferBindingSlot(bufferTarget).Bind(nullptr);
return;
}
@@ -1376,6 +1478,12 @@ namespace MobileGL::MG_Impl::GLImpl {
} else {
point.ClearRange();
}
// The indexed bind also binds to the generic binding point of the same target
// (GL 4.6 core 6.1.1). Callers rely on it: the texture_gather tests set up their
// SSBO with BindBufferBase and then size it through glBufferData on the generic
// target alone, which would otherwise raise GL_INVALID_OPERATION and leave the
// buffer with no storage.
GetBufferBindingSlot(bufferTarget).Bind(bufferObject);
}
void BindBufferRange_State(GLenum target, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size) {
@@ -1384,6 +1492,14 @@ namespace MobileGL::MG_Impl::GLImpl {
BufferTarget bufferTarget = MG_Util::ConvertGLEnumToBufferTarget(target);
if (!BufferImpl::ValidateBufferBindingPointTarget(bufferTarget)) return;
if (!BufferImpl::ValidateBufferBindingPointIndex(bufferTarget, index)) return;
if (bufferTarget == BufferTarget::TransformFeedback && MG_State::pGLContext->IsTransformFeedbackActive()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"Transform feedback buffer bindings cannot change while transform "
"feedback is active."));
return;
}
MG_State::pGLContext->TouchBufferBindingPoint(bufferTarget, index);
auto& point = MG_State::pGLContext->GetBufferBindingPoint(bufferTarget, index);
@@ -1391,6 +1507,7 @@ namespace MobileGL::MG_Impl::GLImpl {
if (buffer == 0) {
point.Bind(nullptr);
point.SetRange(Range1D(0, 0));
GetBufferBindingSlot(bufferTarget).Bind(nullptr);
return;
}
@@ -1408,6 +1525,8 @@ namespace MobileGL::MG_Impl::GLImpl {
} else {
point.ClearRange();
}
// Also the generic binding point, exactly as BindBufferBase (GL 4.6 core 6.1.1).
GetBufferBindingSlot(bufferTarget).Bind(bufferObject);
}
/* @INSERTION_POINT:FUNCTION_IMPLEMENTATION@ */
@@ -1517,6 +1636,10 @@ namespace MobileGL::MG_Impl::GLImpl {
BufferSubData_State(target, offset, size, data);
}
void GetNamedBufferSubData(GLuint buffer, GLintptr offset, GLsizeiptr size, void* data) {
GetNamedBufferSubData_State(buffer, offset, size, data);
}
void GetBufferSubData(GLenum target, GLintptr offset, GLsizeiptr size, void* data) {
GetBufferSubData_State(target, offset, size, data);
}
@@ -41,6 +41,7 @@ namespace MobileGL::MG_Impl::GLImpl {
GLsizeiptr size);
void BufferSubData(GLenum target, GLintptr offset, GLsizeiptr size, const void* data);
void GetBufferSubData(GLenum target, GLintptr offset, GLsizeiptr size, void* data);
void GetNamedBufferSubData(GLuint buffer, GLintptr offset, GLsizeiptr size, void* data);
void BufferData(GLenum target, GLsizeiptr size, const void* data, GLenum usage);
void BindBuffer(GLenum target, GLuint buffer);
void GenBuffers(GLsizei n, GLuint* buffers);
@@ -60,6 +60,11 @@ namespace MobileGL::MG_Impl::GLImpl::BufferImpl {
MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxShaderStorageBufferBindings;
pointCount = std::min(pointCount, static_cast<SizeT>(std::max(backendCount, 0)));
}
if (target == BufferTarget::TransformFeedback) {
// GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS bounds the indexed capture
// binding points in GL 3.3 (no ARB_transform_feedback3).
pointCount = std::min<SizeT>(pointCount, 4);
}
if (index < pointCount) {
return true;
@@ -107,14 +112,10 @@ namespace MobileGL::MG_Impl::GLImpl::BufferImpl {
}
Bool ValidateBufferMappingAccess(Flags<BufferMappingAccessBit> accessBits) {
if (accessBits == BufferMappingAccessBit::Null) {
MG_State::pGLContext->RecordError(ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl/BufferImpl",
"ValidateBufferMappingAccess",
"Access bits cannot be null."));
return false;
}
// An empty mask is a legal value for a bitfield - it just fails the rule that a mapping
// must ask for read or write access, which is INVALID_OPERATION and belongs to the callers
// (both of them check it immediately after this). Rejecting it here as INVALID_ENUM
// reported the wrong error and hid theirs.
const auto validBits = BufferMappingAccessBit::Read | BufferMappingAccessBit::Write |
BufferMappingAccessBit::InvalidateRange | BufferMappingAccessBit::InvalidateBuffer |
BufferMappingAccessBit::FlushExplicit | BufferMappingAccessBit::Unsynchronized |
+734 -9
View File
@@ -11,6 +11,7 @@
#include <MG_State/GLState/Core.h>
#include <MG_State/EGLState/Core.h>
#include <MG_Backend/BackendObjects.h>
#include "../Getter/GL_Getter.h"
namespace MobileGL::MG_Impl::GLImpl {
static Bool ValidateCurrentProgramForExecution(const char* functionName) {
@@ -48,7 +49,109 @@ namespace MobileGL::MG_Impl::GLImpl {
return true;
}
// Primitives a draw of `count` vertices in `mode` assembles (0 for
// incomplete primitives). Used for the CPU-side transform feedback
// primitive accounting.
static Uint64 CountPrimitivesForDraw(GLenum mode, GLsizei count) {
if (count <= 0) return 0;
switch (mode) {
case GL_POINTS: return static_cast<Uint64>(count);
case GL_LINES: return static_cast<Uint64>(count / 2);
case GL_LINE_STRIP: return count >= 2 ? static_cast<Uint64>(count - 1) : 0;
case GL_LINE_LOOP: return count >= 2 ? static_cast<Uint64>(count) : 0;
case GL_TRIANGLES: return static_cast<Uint64>(count / 3);
case GL_TRIANGLE_STRIP:
case GL_TRIANGLE_FAN: return count >= 3 ? static_cast<Uint64>(count - 2) : 0;
default: return 0;
}
}
// Accumulate the transform feedback primitive counter for a captured draw.
// Draws without a geometry stage write exactly the primitives they assemble,
// clamped by the capture buffers' remaining capacity (a full buffer stops
// recording whole primitives, which is what PRIMITIVES_WRITTEN reports).
// Geometry amplification is not modelled here.
static void AccountTransformFeedbackPrimitives(GLenum mode, GLsizei count) {
if (!MG_State::pGLContext->IsTransformFeedbackActive()) return;
// A paused span captures nothing, so a draw made while paused contributes to
// PRIMITIVES_GENERATED but not to TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN.
if (MG_State::pGLContext->IsTransformFeedbackPaused()) {
MG_State::pGLContext->AddTransformFeedbackPausedPrimitives(CountPrimitivesForDraw(mode, count));
return;
}
Uint64 primitives = CountPrimitivesForDraw(mode, count);
if (primitives == 0) return;
MG_State::pGLContext->AddTransformFeedbackInputPrimitives(primitives);
Uint64 verticesPerPrimitive = 1;
switch (mode) {
case GL_LINES:
case GL_LINE_STRIP:
case GL_LINE_LOOP:
verticesPerPrimitive = 2;
break;
case GL_TRIANGLES:
case GL_TRIANGLE_STRIP:
case GL_TRIANGLE_FAN:
verticesPerPrimitive = 3;
break;
default:
break;
}
const auto& program = MG_State::pGLContext->GetTransformFeedbackProgram();
if (program != nullptr) {
// Capacity in captured vertices = the tightest bound buffer.
Uint64 capacityVertices = ~0ull;
for (SizeT i = 0; i < program->GetTransformFeedbackBufferCount(); ++i) {
const Uint32 stride = program->GetTransformFeedbackStride(static_cast<Uint32>(i));
if (stride == 0) continue;
const auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::TransformFeedback,
static_cast<Uint>(i));
const Range1D range = point.GetRange();
const Uint64 bytes = range.end > range.start ? static_cast<Uint64>(range.end - range.start) : 0;
capacityVertices = std::min<Uint64>(capacityVertices, bytes / stride);
}
if (capacityVertices != ~0ull) {
const Uint64 usedVertices = MG_State::pGLContext->GetTransformFeedbackCapturedVertices();
const Uint64 remainingVertices = capacityVertices > usedVertices ? capacityVertices - usedVertices : 0;
primitives = std::min<Uint64>(primitives, remainingVertices / verticesPerPrimitive);
}
}
MG_State::pGLContext->AddTransformFeedbackPrimitives(primitives);
MG_State::pGLContext->AddTransformFeedbackCapturedVertices(primitives * verticesPerPrimitive);
}
// Every primitive mode a draw command accepts (GL 4.6 core table 10.1, plus
// GL_PATCHES for the tessellation pipeline). Anything else is GL_INVALID_ENUM.
static Bool IsAcceptedPrimitiveMode(GLenum mode) {
switch (mode) {
case GL_POINTS:
case GL_LINES:
case GL_LINE_LOOP:
case GL_LINE_STRIP:
case GL_LINES_ADJACENCY:
case GL_LINE_STRIP_ADJACENCY:
case GL_TRIANGLES:
case GL_TRIANGLE_STRIP:
case GL_TRIANGLE_FAN:
case GL_TRIANGLES_ADJACENCY:
case GL_TRIANGLE_STRIP_ADJACENCY:
case GL_PATCHES:
return true;
default:
return false;
}
}
static Bool ValidatePrimitiveModeForBackend(const char* functionName, GLenum mode) {
if (!IsAcceptedPrimitiveMode(mode)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "mode is not an accepted primitive type."));
return false;
}
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
if (!activeBackendObject) {
MG_State::pGLContext->RecordError(
@@ -57,15 +160,6 @@ namespace MobileGL::MG_Impl::GLImpl {
return false;
}
if (activeBackendObject->GetBackendType() == BackendType::DirectVulkan && mode == GL_LINE_LOOP) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", functionName,
"Primitive mode GL_LINE_LOOP is not supported by the DirectVulkan backend."));
return false;
}
const auto& vao = MG_State::pGLContext->GetBoundVertexArray();
if (vao && vao->GetExternalIndex() == 0 && !MG_State::IsRelaxedSemanticsActive()) {
MG_State::pGLContext->RecordError(
@@ -75,9 +169,133 @@ namespace MobileGL::MG_Impl::GLImpl {
return false;
}
// A geometry stage only accepts the primitive types that decompose into its declared
// input primitive (GL 4.6 core 11.3.1); anything else is INVALID_OPERATION. GL_PATCHES
// is the tessellation pipeline's input and reaches the geometry stage already
// converted, so it is not constrained here.
const auto& currentProgram = MG_State::pGLContext->GetCurrentProgram();
const GLenum gsInput = currentProgram ? currentProgram->GetGeometryInputType() : GL_NONE;
if (gsInput != GL_NONE && mode != GL_PATCHES) {
Bool compatible = false;
switch (gsInput) {
case GL_POINTS:
compatible = mode == GL_POINTS;
break;
case GL_LINES:
compatible = mode == GL_LINES || mode == GL_LINE_STRIP || mode == GL_LINE_LOOP;
break;
case GL_LINES_ADJACENCY:
compatible = mode == GL_LINES_ADJACENCY || mode == GL_LINE_STRIP_ADJACENCY;
break;
case GL_TRIANGLES:
compatible = mode == GL_TRIANGLES || mode == GL_TRIANGLE_STRIP || mode == GL_TRIANGLE_FAN;
break;
case GL_TRIANGLES_ADJACENCY:
compatible = mode == GL_TRIANGLES_ADJACENCY || mode == GL_TRIANGLE_STRIP_ADJACENCY;
break;
default:
break;
}
if (!compatible) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", functionName,
"Primitive mode is incompatible with the geometry shader's input primitive type."));
return false;
}
}
// While transform feedback is active the draw's primitive type must match
// the feedback primitive mode (GL 3.3 core 13.2.2). With a geometry shader
// the constraint moves to the shader's output primitive type instead, so
// the draw mode itself is unconstrained here. A paused span is exempt: it
// captures nothing, so there is nothing for the mode to be incompatible with
// (GL 4.6 core 13.2.3).
if (MG_State::pGLContext->IsTransformFeedbackActive() &&
!MG_State::pGLContext->IsTransformFeedbackPaused() &&
!(MG_State::pGLContext->GetTransformFeedbackProgram() &&
MG_State::pGLContext->GetTransformFeedbackProgram()->GetShaderIndexByStage(ShaderStage::Geometry) >= 0)) {
const GLenum feedbackMode = MG_State::pGLContext->GetTransformFeedbackPrimitiveMode();
Bool compatible = false;
switch (feedbackMode) {
case GL_POINTS:
compatible = mode == GL_POINTS;
break;
case GL_LINES:
compatible = mode == GL_LINES || mode == GL_LINE_STRIP || mode == GL_LINE_LOOP;
break;
case GL_TRIANGLES:
compatible = mode == GL_TRIANGLES || mode == GL_TRIANGLE_STRIP || mode == GL_TRIANGLE_FAN;
break;
default:
break;
}
if (!compatible) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", functionName,
"Primitive mode is incompatible with the active transform feedback primitive mode."));
return false;
}
}
return true;
}
// Byte size of the command structures the indirect draws read (GL 4.6 core 10.3.10).
constexpr SizeT kDrawArraysIndirectCommandBytes = 4 * sizeof(Uint32);
constexpr SizeT kDrawElementsIndirectCommandBytes = 5 * sizeof(Uint32);
// Shared preconditions of every *Indirect draw: `indirect` is a byte offset into the
// buffer bound to GL_DRAW_INDIRECT_BUFFER, must be 4-byte aligned, and the whole
// command has to lie inside that buffer.
static Bool ValidateIndirectDrawSource(const char* functionName, const void* indirect, SizeT commandBytes) {
const auto offset = reinterpret_cast<uintptr_t>(indirect);
if (offset % 4 != 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
"indirect offset must be a multiple of 4."));
return false;
}
const auto& buffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
if (!buffer) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
"No buffer is bound to GL_DRAW_INDIRECT_BUFFER."));
return false;
}
if (offset + commandBytes > buffer->GetSize()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
"The indirect command extends past the end of the bound "
"GL_DRAW_INDIRECT_BUFFER."));
return false;
}
return true;
}
// Index type accepted by the DrawElements family (GL 4.6 core 10.3.9).
static Bool ValidateDrawElementsIndexType(const char* functionName, GLenum type) {
switch (type) {
case GL_UNSIGNED_BYTE:
case GL_UNSIGNED_SHORT:
case GL_UNSIGNED_INT:
return true;
default:
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "type is not an accepted index type."));
return false;
}
}
void Clear_Backend(GLbitfield mask) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
@@ -272,6 +490,28 @@ namespace MobileGL::MG_Impl::GLImpl {
dispatchComputeIndirect(indirect);
}
void PatchParameteri(GLenum pname, GLint value) {
if (pname != GL_PATCH_VERTICES) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "pname must be GL_PATCH_VERTICES."));
return;
}
GLint maxPatchVertices = 32;
GetIntegerv(GL_MAX_PATCH_VERTICES, &maxPatchVertices);
if (value <= 0 || value > maxPatchVertices) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"value must be in [1, GL_MAX_PATCH_VERTICES]."));
return;
}
MG_State::pGLContext->SetPatchVertices(static_cast<Uint>(value));
if (const auto patchParameteri = MG_Backend::gBackendFunctionsTable.GL.PatchParameteri) {
patchParameteri(pname, value);
}
}
void MemoryBarrier(GLbitfield barriers) {
auto memoryBarrier = MG_Backend::gBackendFunctionsTable.GL.MemoryBarrier;
if (!memoryBarrier) {
@@ -377,6 +617,8 @@ namespace MobileGL::MG_Impl::GLImpl {
void DrawElementsIndirect(GLenum mode, GLenum type, const void* indirect) {
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
if (!ValidateDrawElementsIndexType(__func__, type)) return;
if (!ValidateIndirectDrawSource(__func__, indirect, kDrawElementsIndirectCommandBytes)) return;
DrawElementsIndirect_Backend(mode, type, indirect);
}
@@ -396,18 +638,21 @@ namespace MobileGL::MG_Impl::GLImpl {
void DrawArraysIndirect(GLenum mode, const void* indirect) {
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
if (!ValidateIndirectDrawSource(__func__, indirect, kDrawArraysIndirectCommandBytes)) return;
DrawArraysIndirect_Backend(mode, indirect);
}
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices, GLint basevertex) {
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
AccountTransformFeedbackPrimitives(mode, count);
DrawElementsBaseVertex_Backend(mode, count, type, indices, basevertex);
}
void DrawArrays(GLenum mode, GLint first, GLsizei count) {
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
AccountTransformFeedbackPrimitives(mode, count);
DrawArrays_Backend(mode, first, count);
}
@@ -444,7 +689,487 @@ namespace MobileGL::MG_Impl::GLImpl {
void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices) {
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
AccountTransformFeedbackPrimitives(mode, count);
DrawElements_Backend(mode, count, type, indices);
}
void BeginTransformFeedback(GLenum primitiveMode) {
if (primitiveMode != GL_POINTS && primitiveMode != GL_LINES && primitiveMode != GL_TRIANGLES) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"primitiveMode must be GL_POINTS, GL_LINES or GL_TRIANGLES."));
return;
}
if (MG_State::pGLContext->IsTransformFeedbackActive()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Transform feedback is already active."));
return;
}
const auto& program = MG_State::pGLContext->GetCurrentProgram();
if (!program || !program->GetLinkStatus() || program->GetTransformFeedbackVaryingCount() == 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", __func__,
"No program with transform feedback varyings is active."));
return;
}
// Every capture buffer slot the program's mode uses must have a buffer bound. A slot
// of stride 0 - two consecutive gl_NextBuffer entries - captures nothing and so needs
// no binding.
const SizeT usedBufferCount = program->GetTransformFeedbackBufferCount();
for (SizeT i = 0; i < usedBufferCount; ++i) {
if (program->GetTransformFeedbackStride(static_cast<Uint32>(i)) == 0) continue;
const auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::TransformFeedback,
static_cast<Uint>(i));
if (point.GetBoundObject() == nullptr) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", __func__,
"Transform feedback buffer binding point " + std::to_string(i) + " has no buffer bound."));
return;
}
}
MG_State::pGLContext->BeginTransformFeedback(primitiveMode, program);
if (const auto beginXfb = MG_Backend::gBackendFunctionsTable.GL.BeginTransformFeedback) {
beginXfb(primitiveMode);
}
}
// Vulkan transform feedback captures triangle strips in plain (i, i+1, i+2)
// vertex order, but GL decomposes odd strip triangles as (i+1, i, i+2)
// (GL 4.6 table 10.1). With the geometry stage's statically-known strip
// lengths the captured records are reordered in place: swap the first two
// vertex records of every odd triangle within each emitted strip.
static void FixupGsStripCaptureOrder(const SharedPtr<MG_State::GLState::ProgramObject>& program,
Uint64 inputPrimitives) {
// Only Vulkan-order captures need this. A backend that runs the capture on its
// own GL/ES driver (it owns the span, hence the EndTransformFeedback entry) has
// already produced GL's vertex order, and reordering it again would corrupt it.
if (MG_Backend::gBackendFunctionsTable.GL.EndTransformFeedback != nullptr) {
return;
}
if (program == nullptr || !program->HasGsTriangleStripCaptureFixup() || inputPrimitives == 0) {
return;
}
const auto& stripTriangles = program->GetGsStripTriangles();
// Global triangle indices whose leading vertex pair must swap.
Vector<Uint64> swapTriangles;
Uint64 triangleBase = 0;
for (Uint64 input = 0; input < inputPrimitives; ++input) {
for (const Uint32 stripLength : stripTriangles) {
for (Uint32 t = 1; t < stripLength; t += 2) {
swapTriangles.push_back(triangleBase + t);
}
triangleBase += stripLength;
}
}
if (swapTriangles.empty()) {
return;
}
for (SizeT bufferIndex = 0; bufferIndex < program->GetTransformFeedbackBufferCount(); ++bufferIndex) {
const Uint32 stride = program->GetTransformFeedbackStride(static_cast<Uint32>(bufferIndex));
if (stride == 0) continue;
const auto& bindingPoint =
MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::TransformFeedback,
static_cast<Uint>(bufferIndex));
const auto& buffer = bindingPoint.GetBoundObject();
if (buffer == nullptr) continue;
const Range1D range = bindingPoint.GetRange();
const Uint8* mapped = buffer->MappedData();
if (mapped == nullptr) continue;
// The geometry stage amplifies, so the CPU vertex counter does not bound
// the capture; the binding range's whole-triangle capacity does.
const Uint64 rangeBytes = range.end > range.start ? static_cast<Uint64>(range.end - range.start) : 0;
const Uint64 capturedTriangles = std::min<Uint64>(triangleBase, (rangeBytes / stride) / 3);
// Observed Vulkan capture order for odd strip triangles is (i, i+2, i+1)
// (winding preserved by swapping the trailing pair); GL wants
// (i+1, i, i+2), which is one rotation away: (a,b,c) -> (c,a,b).
Vector<Uint8> scratch(stride);
for (const Uint64 triangle : swapTriangles) {
if (triangle >= capturedTriangles) break;
const SizeT v0Offset = static_cast<SizeT>(range.start) + static_cast<SizeT>(triangle * 3) * stride;
const SizeT v1Offset = v0Offset + stride;
const SizeT v2Offset = v1Offset + stride;
Memcpy(scratch.data(), mapped + v2Offset, stride);
buffer->WritebackFromBackend({const_cast<Uint8*>(mapped) + v1Offset, stride}, v2Offset);
buffer->WritebackFromBackend({const_cast<Uint8*>(mapped) + v0Offset, stride}, v1Offset);
buffer->WritebackFromBackend({scratch.data(), stride}, v0Offset);
}
}
}
void EndTransformFeedback(void) {
if (!MG_State::pGLContext->IsTransformFeedbackActive()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Transform feedback is not active."));
return;
}
const auto capturedProgram = MG_State::pGLContext->GetTransformFeedbackProgram();
const Uint64 inputPrimitives = MG_State::pGLContext->GetTransformFeedbackInputPrimitives();
// Closed while the capture state is still active: a backend that captures
// through its own driver reads the capture program and buffer bindings here.
if (const auto endXfb = MG_Backend::gBackendFunctionsTable.GL.EndTransformFeedback) {
endXfb();
}
MG_State::pGLContext->EndTransformFeedback();
// Captured results must be visible to MapBuffer/GetBufferSubData after
// End; the capture targets are host-coherent GPU memory, so completing
// the GPU work is all that is required.
auto& backendGL = MG_Backend::gBackendFunctionsTable.GL;
if (backendGL.FenceSync && backendGL.ClientWaitSync) {
if (auto sync = backendGL.FenceSync()) {
backendGL.ClientWaitSync(sync, GL_SYNC_FLUSH_COMMANDS_BIT, ~0ull);
if (backendGL.DeleteSync) {
backendGL.DeleteSync(sync);
}
}
}
FixupGsStripCaptureOrder(capturedProgram, inputPrimitives);
}
void PauseTransformFeedback(void) {
if (!MG_State::pGLContext->IsTransformFeedbackActive() ||
MG_State::pGLContext->IsTransformFeedbackPaused()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"Transform feedback is not active, or is already paused."));
return;
}
MG_State::pGLContext->SetTransformFeedbackPaused(true);
if (const auto pauseXfb = MG_Backend::gBackendFunctionsTable.GL.PauseTransformFeedback) {
pauseXfb();
}
}
void ResumeTransformFeedback(void) {
if (!MG_State::pGLContext->IsTransformFeedbackActive() ||
!MG_State::pGLContext->IsTransformFeedbackPaused()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Transform feedback is not paused."));
return;
}
MG_State::pGLContext->SetTransformFeedbackPaused(false);
if (const auto resumeXfb = MG_Backend::gBackendFunctionsTable.GL.ResumeTransformFeedback) {
resumeXfb();
}
}
void GenTransformFeedbacks(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);
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) {
if (n < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "n must be non-negative."));
return;
}
if (ids == nullptr) return;
for (GLsizei i = 0; i < n; ++i) {
const GLuint id = ids[i];
// Unknown names and 0 are silently ignored; an object whose capture span is
// still open is not (GL 4.6 core 13.2.1).
if (id == 0 || !MG_State::pGLContext->ValidateTransformFeedbackName(id)) continue;
if (id == MG_State::pGLContext->GetBoundTransformFeedbackName() &&
MG_State::pGLContext->IsTransformFeedbackActive()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"Cannot delete a transform feedback object whose capture is active."));
continue;
}
if (const auto deleteXfb = MG_Backend::gBackendFunctionsTable.GL.DeleteTransformFeedback) {
deleteXfb(id);
}
MG_State::pGLContext->MarkTransformFeedbackObjectForDeletion(id);
}
}
void BindTransformFeedback(GLenum target, GLuint id) {
if (target != GL_TRANSFORM_FEEDBACK) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "target must be GL_TRANSFORM_FEEDBACK."));
return;
}
// A running capture pins its object; only a paused one may be swapped out.
if (MG_State::pGLContext->IsTransformFeedbackActive() &&
!MG_State::pGLContext->IsTransformFeedbackPaused()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"Transform feedback is active and not paused."));
return;
}
if (!MG_State::pGLContext->ValidateTransformFeedbackName(id)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
std::to_string(id) + " is not a transform feedback object name."));
return;
}
MG_State::pGLContext->BindTransformFeedbackObject(id);
if (const auto bindXfb = MG_Backend::gBackendFunctionsTable.GL.BindTransformFeedback) {
bindXfb(id);
}
}
GLboolean IsTransformFeedback(GLuint id) {
// Name 0 is the default object, and a name glGenTransformFeedbacks handed out only
// becomes the name of an object once it has been bound.
return MG_State::pGLContext->IsTransformFeedbackObject(id) ? GL_TRUE : GL_FALSE;
}
// glDrawTransformFeedback[Stream][Instanced]: replays the vertices the named object
// captured in its last completed span, as if by glDrawArraysInstanced with that count
// (GL 4.6 core 10.3.7).
static void DrawTransformFeedbackImpl(const char* functionName, GLenum mode, GLuint id, GLuint stream,
GLsizei instancecount) {
if (!ValidateCurrentProgramForExecution(functionName)) return;
if (!ValidatePrimitiveModeForBackend(functionName, mode)) return;
if (instancecount < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "instancecount must be non-negative."));
return;
}
if (!MG_State::pGLContext->ValidateTransformFeedbackName(id)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
std::to_string(id) + " is not a transform feedback object name."));
return;
}
// GL_MAX_VERTEX_STREAMS is 1, so stream 0 is the only one that exists.
if (stream != 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
"stream must be less than GL_MAX_VERTEX_STREAMS."));
return;
}
// Drawing from an object whose capture is currently open is legal and deliberate:
// it is how a transform feedback result is fed straight back into the next span
// (ARB_transform_feedback2 lists no such restriction).
if (!MG_State::pGLContext->HasTransformFeedbackCompletedSpan(id)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
"glEndTransformFeedback has never been called for this object."));
return;
}
const Uint64 vertices = MG_State::pGLContext->GetTransformFeedbackRecordedVertices(id);
if (vertices == 0) return;
const auto count = static_cast<GLsizei>(vertices);
AccountTransformFeedbackPrimitives(mode, count);
if (instancecount == 1) {
DrawArrays_Backend(mode, 0, count);
} else {
DrawArraysInstanced_Backend(mode, 0, count, instancecount);
}
}
void DrawTransformFeedback(GLenum mode, GLuint id) {
DrawTransformFeedbackImpl(__func__, mode, id, 0, 1);
}
void DrawTransformFeedbackInstanced(GLenum mode, GLuint id, GLsizei instancecount) {
DrawTransformFeedbackImpl(__func__, mode, id, 0, instancecount);
}
void DrawTransformFeedbackStream(GLenum mode, GLuint id, GLuint stream) {
DrawTransformFeedbackImpl(__func__, mode, id, stream, 1);
}
void DrawTransformFeedbackStreamInstanced(GLenum mode, GLuint id, GLuint stream, GLsizei instancecount) {
DrawTransformFeedbackImpl(__func__, mode, id, stream, instancecount);
}
} // namespace MobileGL::MG_Impl::GLImpl
@@ -11,8 +11,27 @@
namespace MobileGL::MG_Impl::GLImpl {
/* @INSERTION_POINT:FUNCTION_DECLARATION@ */
void BeginTransformFeedback(GLenum primitiveMode);
void EndTransformFeedback(void);
void PauseTransformFeedback(void);
void ResumeTransformFeedback(void);
void GenTransformFeedbacks(GLsizei n, GLuint* ids);
void CreateTransformFeedbacks(GLsizei n, 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);
GLboolean IsTransformFeedback(GLuint id);
void DrawTransformFeedback(GLenum mode, GLuint id);
void DrawTransformFeedbackInstanced(GLenum mode, GLuint id, GLsizei instancecount);
void DrawTransformFeedbackStream(GLenum mode, GLuint id, GLuint stream);
void DrawTransformFeedbackStreamInstanced(GLenum mode, GLuint id, GLuint stream, GLsizei instancecount);
void DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ);
void DispatchComputeIndirect(GLintptr indirect);
void PatchParameteri(GLenum pname, GLint value);
void MemoryBarrier(GLbitfield barriers);
void MemoryBarrierByRegion(GLbitfield barriers);
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride);
+101 -106
View File
@@ -15,6 +15,7 @@
#include "../Texture/GL_Texture.h"
#include "../Drawing/GL_Drawing.h"
#include "../Program/GL_Program.h"
#include "../Program/GL_ProgramPipeline.h"
#include "../RenderState/GL_RenderState.h"
#include "../Framebuffer/GL_Framebuffer.h"
#include "../VertexArray/GL_VertexArray.h"
@@ -236,12 +237,12 @@ DECLARE_GL_FUNCTION_HEAD(void, DeleteVertexArrays, GLsizei n, const GLuint* arra
DECLARE_GL_FUNCTION_HEAD(void, GenVertexArrays, GLsizei n, GLuint* arrays) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GenVertexArrays, n, arrays)
DECLARE_GL_FUNCTION_HEAD(GLboolean, IsVertexArray, GLuint array) DECLARE_GL_FUNCTION_END(GLboolean, IsVertexArray, array)
DECLARE_GL_FUNCTION_HEAD(void, GetIntegeri_v, GLenum target, GLuint index, GLint* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetIntegeri_v, target, index, data)
DECLARE_GL_FUNCTION_STUB_HEAD(void, BeginTransformFeedback, GLenum primitiveMode) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BeginTransformFeedback, primitiveMode)
DECLARE_GL_FUNCTION_STUB_HEAD(void, EndTransformFeedback) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, EndTransformFeedback)
DECLARE_GL_FUNCTION_HEAD(void, BeginTransformFeedback, GLenum primitiveMode) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BeginTransformFeedback, primitiveMode)
DECLARE_GL_FUNCTION_HEAD(void, EndTransformFeedback) DECLARE_GL_FUNCTION_END_NO_RETURN(void, EndTransformFeedback)
DECLARE_GL_FUNCTION_HEAD(void, BindBufferRange, GLenum target, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindBufferRange, target, index, buffer, offset, size)
DECLARE_GL_FUNCTION_HEAD(void, BindBufferBase, GLenum target, GLuint index, GLuint buffer) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindBufferBase, target, index, buffer)
DECLARE_GL_FUNCTION_STUB_HEAD(void, TransformFeedbackVaryings, GLuint program, GLsizei count, const GLchar* const* varyings, GLenum bufferMode) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TransformFeedbackVaryings, program, count, varyings, bufferMode)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetTransformFeedbackVarying, GLuint program, GLuint index, GLsizei bufSize, GLsizei* length, GLsizei* size, GLenum* type, GLchar* name) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetTransformFeedbackVarying, program, index, bufSize, length, size, type, name)
DECLARE_GL_FUNCTION_HEAD(void, TransformFeedbackVaryings, GLuint program, GLsizei count, const GLchar* const* varyings, GLenum bufferMode) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TransformFeedbackVaryings, program, count, varyings, bufferMode)
DECLARE_GL_FUNCTION_HEAD(void, GetTransformFeedbackVarying, GLuint program, GLuint index, GLsizei bufSize, GLsizei* length, GLsizei* size, GLenum* type, GLchar* name) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetTransformFeedbackVarying, program, index, bufSize, length, size, type, name)
DECLARE_GL_FUNCTION_HEAD(void, VertexAttribIPointer, GLuint index, GLint size, GLenum type, GLsizei stride, const void* pointer) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribIPointer, index, size, type, stride, pointer)
DECLARE_GL_FUNCTION_HEAD(void, GetVertexAttribIiv, GLuint index, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetVertexAttribIiv, index, pname, params)
DECLARE_GL_FUNCTION_HEAD(void, GetVertexAttribIuiv, GLuint index, GLenum pname, GLuint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetVertexAttribIuiv, index, pname, params)
@@ -292,23 +293,17 @@ DECLARE_GL_FUNCTION_HEAD(void, SamplerParameterfv, GLuint sampler, GLenum pname,
DECLARE_GL_FUNCTION_HEAD(void, GetSamplerParameteriv, GLuint sampler, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetSamplerParameteriv, sampler, pname, params)
DECLARE_GL_FUNCTION_HEAD(void, GetSamplerParameterfv, GLuint sampler, GLenum pname, GLfloat* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetSamplerParameterfv, sampler, pname, params)
DECLARE_GL_FUNCTION_HEAD(void, VertexAttribDivisor, GLuint index, GLuint divisor) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribDivisor, index, divisor)
DECLARE_GL_FUNCTION_STUB_HEAD(void, BindTransformFeedback, GLenum target, GLuint id) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BindTransformFeedback, target, id)
DECLARE_GL_FUNCTION_STUB_HEAD(void, DeleteTransformFeedbacks, GLsizei n, const GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DeleteTransformFeedbacks, n, ids)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GenTransformFeedbacks, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GenTransformFeedbacks, n, ids)
// Transform feedback objects are not implemented, so no name is ever a live object. The shared
// stub returns (type)1, telling a probing caller that every id it invents already exists; GL_FALSE
// is both truthful and what the spec requires for a name that was never generated.
MOBILEGL_GL_API GLboolean glIsTransformFeedback(GLuint id) {
MGLOG_W("Stub function: %s(...)", __FUNCTION__);
return GL_FALSE;
}
DECLARE_GL_FUNCTION_STUB_HEAD(void, PauseTransformFeedback) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PauseTransformFeedback)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ResumeTransformFeedback) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ResumeTransformFeedback)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramBinary, GLuint program, GLsizei bufSize, GLsizei* length, GLenum* binaryFormat, void* binary) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetProgramBinary, program, bufSize, length, binaryFormat, binary)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramBinary, GLuint program, GLenum binaryFormat, const void* binary, GLsizei length) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramBinary, program, binaryFormat, binary, length)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramParameteri, GLuint program, GLenum pname, GLint value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramParameteri, program, pname, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, InvalidateFramebuffer, GLenum target, GLsizei numAttachments, const GLenum* attachments) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, InvalidateFramebuffer, target, numAttachments, attachments)
DECLARE_GL_FUNCTION_STUB_HEAD(void, InvalidateSubFramebuffer, GLenum target, GLsizei numAttachments, const GLenum* attachments, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, InvalidateSubFramebuffer, target, numAttachments, attachments, x, y, width, height)
DECLARE_GL_FUNCTION_HEAD(void, BindTransformFeedback, GLenum target, GLuint id) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindTransformFeedback, target, id)
DECLARE_GL_FUNCTION_HEAD(void, DeleteTransformFeedbacks, GLsizei n, const GLuint* ids) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DeleteTransformFeedbacks, n, ids)
DECLARE_GL_FUNCTION_HEAD(void, GenTransformFeedbacks, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GenTransformFeedbacks, n, ids)
DECLARE_GL_FUNCTION_HEAD(GLboolean, IsTransformFeedback, GLuint id) DECLARE_GL_FUNCTION_END(GLboolean, IsTransformFeedback, id)
DECLARE_GL_FUNCTION_HEAD(void, PauseTransformFeedback) DECLARE_GL_FUNCTION_END_NO_RETURN(void, PauseTransformFeedback)
DECLARE_GL_FUNCTION_HEAD(void, ResumeTransformFeedback) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ResumeTransformFeedback)
DECLARE_GL_FUNCTION_HEAD(void, GetProgramBinary, GLuint program, GLsizei bufSize, GLsizei* length, GLenum* binaryFormat, void* binary) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetProgramBinary, program, bufSize, length, binaryFormat, binary)
DECLARE_GL_FUNCTION_HEAD(void, ProgramBinary, GLuint program, GLenum binaryFormat, const void* binary, GLsizei length) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramBinary, program, binaryFormat, binary, length)
DECLARE_GL_FUNCTION_HEAD(void, ProgramParameteri, GLuint program, GLenum pname, GLint value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramParameteri, program, pname, value)
DECLARE_GL_FUNCTION_HEAD(void, InvalidateFramebuffer, GLenum target, GLsizei numAttachments, const GLenum* attachments) DECLARE_GL_FUNCTION_END_NO_RETURN(void, InvalidateFramebuffer, target, numAttachments, attachments)
DECLARE_GL_FUNCTION_HEAD(void, InvalidateSubFramebuffer, GLenum target, GLsizei numAttachments, const GLenum* attachments, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, InvalidateSubFramebuffer, target, numAttachments, attachments, x, y, width, height)
DECLARE_GL_FUNCTION_HEAD(void, TexStorage2D, GLenum target, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexStorage2D, target, levels, internalformat, width, height)
DECLARE_GL_FUNCTION_HEAD(void, TexStorage3D, GLenum target, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexStorage3D, target, levels, internalformat, width, height, depth)
DECLARE_GL_FUNCTION_HEAD(void, GetInternalformativ, GLenum target, GLenum internalformat, GLenum pname, GLsizei bufSize, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetInternalformativ, target, internalformat, pname, bufSize, params)
@@ -316,21 +311,21 @@ DECLARE_GL_FUNCTION_HEAD(void, DispatchCompute, GLuint num_groups_x, GLuint num_
DECLARE_GL_FUNCTION_HEAD(void, DispatchComputeIndirect, GLintptr indirect) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DispatchComputeIndirect, indirect)
DECLARE_GL_FUNCTION_HEAD(void, DrawArraysIndirect, GLenum mode, const void* indirect) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawArraysIndirect, mode, indirect)
DECLARE_GL_FUNCTION_HEAD(void, DrawElementsIndirect, GLenum mode, GLenum type, const void* indirect) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawElementsIndirect, mode, type, indirect)
DECLARE_GL_FUNCTION_STUB_HEAD(void, FramebufferParameteri, GLenum target, GLenum pname, GLint param) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, FramebufferParameteri, target, pname, param)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetFramebufferParameteriv, GLenum target, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetFramebufferParameteriv, target, pname, params)
DECLARE_GL_FUNCTION_HEAD(void, FramebufferParameteri, GLenum target, GLenum pname, GLint param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, FramebufferParameteri, target, pname, param)
DECLARE_GL_FUNCTION_HEAD(void, GetFramebufferParameteriv, GLenum target, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetFramebufferParameteriv, target, pname, params)
DECLARE_GL_FUNCTION_HEAD(void, GetProgramInterfaceiv, GLuint program, GLenum programInterface, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetProgramInterfaceiv, program, programInterface, pname, params)
DECLARE_GL_FUNCTION_HEAD(GLuint, GetProgramResourceIndex, GLuint program, GLenum programInterface, const GLchar* name) DECLARE_GL_FUNCTION_END(GLuint, GetProgramResourceIndex, program, programInterface, name)
DECLARE_GL_FUNCTION_HEAD(void, GetProgramResourceName, GLuint program, GLenum programInterface, GLuint index, GLsizei bufSize, GLsizei* length, GLchar* name) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetProgramResourceName, program, programInterface, index, bufSize, length, name)
DECLARE_GL_FUNCTION_HEAD(void, GetProgramResourceiv, GLuint program, GLenum programInterface, GLuint index, GLsizei propCount, const GLenum* props, GLsizei bufSize, GLsizei* length, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetProgramResourceiv, program, programInterface, index, propCount, props, bufSize, length, params)
DECLARE_GL_FUNCTION_HEAD(GLint, GetProgramResourceLocation, GLuint program, GLenum programInterface, const GLchar* name) DECLARE_GL_FUNCTION_END(GLint, GetProgramResourceLocation, program, programInterface, name)
DECLARE_GL_FUNCTION_STUB_HEAD(void, UseProgramStages, GLuint pipeline, GLbitfield stages, GLuint program) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UseProgramStages, pipeline, stages, program)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ActiveShaderProgram, GLuint pipeline, GLuint program) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ActiveShaderProgram, pipeline, program)
DECLARE_GL_FUNCTION_HEAD(void, UseProgramStages, GLuint pipeline, GLbitfield stages, GLuint program) DECLARE_GL_FUNCTION_END_NO_RETURN(void, UseProgramStages, pipeline, stages, program)
DECLARE_GL_FUNCTION_HEAD(void, ActiveShaderProgram, GLuint pipeline, GLuint program) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ActiveShaderProgram, pipeline, program)
DECLARE_GL_FUNCTION_STUB_HEAD(GLuint, CreateShaderProgramv, GLenum type, GLsizei count, const GLchar* const* strings) DECLARE_GL_FUNCTION_STUB_END(GLuint, CreateShaderProgramv, type, count, strings)
DECLARE_GL_FUNCTION_STUB_HEAD(void, BindProgramPipeline, GLuint pipeline) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BindProgramPipeline, pipeline)
DECLARE_GL_FUNCTION_STUB_HEAD(void, DeleteProgramPipelines, GLsizei n, const GLuint* pipelines) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DeleteProgramPipelines, n, pipelines)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GenProgramPipelines, GLsizei n, GLuint* pipelines) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GenProgramPipelines, n, pipelines)
DECLARE_GL_FUNCTION_STUB_HEAD(GLboolean, IsProgramPipeline, GLuint pipeline) DECLARE_GL_FUNCTION_STUB_END(GLboolean, IsProgramPipeline, pipeline)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramPipelineiv, GLuint pipeline, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetProgramPipelineiv, pipeline, pname, params)
DECLARE_GL_FUNCTION_HEAD(void, BindProgramPipeline, GLuint pipeline) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindProgramPipeline, pipeline)
DECLARE_GL_FUNCTION_HEAD(void, DeleteProgramPipelines, GLsizei n, const GLuint* pipelines) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DeleteProgramPipelines, n, pipelines)
DECLARE_GL_FUNCTION_HEAD(void, GenProgramPipelines, GLsizei n, GLuint* pipelines) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GenProgramPipelines, n, pipelines)
DECLARE_GL_FUNCTION_HEAD(GLboolean, IsProgramPipeline, GLuint pipeline) DECLARE_GL_FUNCTION_END(GLboolean, IsProgramPipeline, pipeline)
DECLARE_GL_FUNCTION_HEAD(void, GetProgramPipelineiv, GLuint pipeline, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetProgramPipelineiv, pipeline, pname, params)
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniform1i, GLuint program, GLint location, GLint v0) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniform1i, program, location, v0)
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniform2i, GLuint program, GLint location, GLint v0, GLint v1) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniform2i, program, location, v0, v1)
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniform3i, GLuint program, GLint location, GLint v0, GLint v1, GLint v2) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniform3i, program, location, v0, v1, v2)
@@ -364,8 +359,8 @@ DECLARE_GL_FUNCTION_HEAD(void, ProgramUniformMatrix2x4fv, GLuint program, GLint
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniformMatrix4x2fv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniformMatrix4x2fv, program, location, count, transpose, value)
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniformMatrix3x4fv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniformMatrix3x4fv, program, location, count, transpose, value)
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniformMatrix4x3fv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniformMatrix4x3fv, program, location, count, transpose, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ValidateProgramPipeline, GLuint pipeline) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ValidateProgramPipeline, pipeline)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramPipelineInfoLog, GLuint pipeline, GLsizei bufSize, GLsizei* length, GLchar* infoLog) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetProgramPipelineInfoLog, pipeline, bufSize, length, infoLog)
DECLARE_GL_FUNCTION_HEAD(void, ValidateProgramPipeline, GLuint pipeline) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ValidateProgramPipeline, pipeline)
DECLARE_GL_FUNCTION_HEAD(void, GetProgramPipelineInfoLog, GLuint pipeline, GLsizei bufSize, GLsizei* length, GLchar* infoLog) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetProgramPipelineInfoLog, pipeline, bufSize, length, infoLog)
DECLARE_GL_FUNCTION_HEAD(void, BindImageTexture, GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access, GLenum format) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindImageTexture, unit, texture, level, layered, layer, access, format)
DECLARE_GL_FUNCTION_HEAD(void, GetBooleani_v, GLenum target, GLuint index, GLboolean* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetBooleani_v, target, index, data)
DECLARE_GL_FUNCTION_HEAD(void, MemoryBarrier, GLbitfield barriers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, MemoryBarrier, barriers)
@@ -425,12 +420,12 @@ 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_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_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, 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, MinSampleShading, GLfloat value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, MinSampleShading, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, PatchParameteri, GLenum pname, GLint value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PatchParameteri, pname, value)
DECLARE_GL_FUNCTION_HEAD(void, PatchParameteri, GLenum pname, GLint value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, PatchParameteri, pname, value)
DECLARE_GL_FUNCTION_HEAD(void, TexParameterIiv, GLenum target, GLenum pname, const GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexParameterIiv, target, pname, params)
DECLARE_GL_FUNCTION_HEAD(void, TexParameterIuiv, GLenum target, GLenum pname, const GLuint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexParameterIuiv, target, pname, params)
DECLARE_GL_FUNCTION_HEAD(void, GetTexParameterIiv, GLenum target, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetTexParameterIiv, target, pname, params)
@@ -440,7 +435,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, 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_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*, 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)
@@ -915,24 +910,24 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, ColorP4ui, GLenum type, GLuint color) DECLAR
DECLARE_GL_FUNCTION_STUB_HEAD(void, ColorP4uiv, GLenum type, const GLuint* color) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ColorP4uiv, type, color)
DECLARE_GL_FUNCTION_STUB_HEAD(void, SecondaryColorP3ui, GLenum type, GLuint color) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, SecondaryColorP3ui, type, color)
DECLARE_GL_FUNCTION_STUB_HEAD(void, SecondaryColorP3uiv, GLenum type, const GLuint* color) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, SecondaryColorP3uiv, type, color)
DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform1d, GLint location, GLdouble x) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform1d, location, x)
DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform2d, GLint location, GLdouble x, GLdouble y) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform2d, location, x, y)
DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform3d, GLint location, GLdouble x, GLdouble y, GLdouble z) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform3d, location, x, y, z)
DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform4d, GLint location, GLdouble x, GLdouble y, GLdouble z, GLdouble w) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform4d, location, x, y, z, w)
DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform1dv, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform1dv, location, count, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform2dv, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform2dv, location, count, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform3dv, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform3dv, location, count, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform4dv, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform4dv, location, count, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, UniformMatrix2dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UniformMatrix2dv, location, count, transpose, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, UniformMatrix3dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UniformMatrix3dv, location, count, transpose, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, UniformMatrix4dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UniformMatrix4dv, location, count, transpose, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, UniformMatrix2x3dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UniformMatrix2x3dv, location, count, transpose, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, UniformMatrix2x4dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UniformMatrix2x4dv, location, count, transpose, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, UniformMatrix3x2dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UniformMatrix3x2dv, location, count, transpose, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, UniformMatrix3x4dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UniformMatrix3x4dv, location, count, transpose, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, UniformMatrix4x2dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UniformMatrix4x2dv, location, count, transpose, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, UniformMatrix4x3dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UniformMatrix4x3dv, location, count, transpose, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetUniformdv, GLuint program, GLint location, GLdouble* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetUniformdv, program, location, params)
DECLARE_GL_FUNCTION_HEAD(void, Uniform1d, GLint location, GLdouble x) DECLARE_GL_FUNCTION_END_NO_RETURN(void, Uniform1d, location, x)
DECLARE_GL_FUNCTION_HEAD(void, Uniform2d, GLint location, GLdouble x, GLdouble y) DECLARE_GL_FUNCTION_END_NO_RETURN(void, Uniform2d, location, x, y)
DECLARE_GL_FUNCTION_HEAD(void, Uniform3d, GLint location, GLdouble x, GLdouble y, GLdouble z) DECLARE_GL_FUNCTION_END_NO_RETURN(void, Uniform3d, location, x, y, z)
DECLARE_GL_FUNCTION_HEAD(void, Uniform4d, GLint location, GLdouble x, GLdouble y, GLdouble z, GLdouble w) DECLARE_GL_FUNCTION_END_NO_RETURN(void, Uniform4d, location, x, y, z, w)
DECLARE_GL_FUNCTION_HEAD(void, Uniform1dv, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, Uniform1dv, location, count, value)
DECLARE_GL_FUNCTION_HEAD(void, Uniform2dv, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, Uniform2dv, location, count, value)
DECLARE_GL_FUNCTION_HEAD(void, Uniform3dv, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, Uniform3dv, location, count, value)
DECLARE_GL_FUNCTION_HEAD(void, Uniform4dv, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, Uniform4dv, location, count, value)
DECLARE_GL_FUNCTION_HEAD(void, UniformMatrix2dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, UniformMatrix2dv, location, count, transpose, value)
DECLARE_GL_FUNCTION_HEAD(void, UniformMatrix3dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, UniformMatrix3dv, location, count, transpose, value)
DECLARE_GL_FUNCTION_HEAD(void, UniformMatrix4dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, UniformMatrix4dv, location, count, transpose, value)
DECLARE_GL_FUNCTION_HEAD(void, UniformMatrix2x3dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, UniformMatrix2x3dv, location, count, transpose, value)
DECLARE_GL_FUNCTION_HEAD(void, UniformMatrix2x4dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, UniformMatrix2x4dv, location, count, transpose, value)
DECLARE_GL_FUNCTION_HEAD(void, UniformMatrix3x2dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, UniformMatrix3x2dv, location, count, transpose, value)
DECLARE_GL_FUNCTION_HEAD(void, UniformMatrix3x4dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, UniformMatrix3x4dv, location, count, transpose, value)
DECLARE_GL_FUNCTION_HEAD(void, UniformMatrix4x2dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, UniformMatrix4x2dv, location, count, transpose, value)
DECLARE_GL_FUNCTION_HEAD(void, UniformMatrix4x3dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, UniformMatrix4x3dv, location, count, transpose, value)
DECLARE_GL_FUNCTION_HEAD(void, GetUniformdv, GLuint program, GLint location, GLdouble* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetUniformdv, program, location, params)
DECLARE_GL_FUNCTION_STUB_HEAD(GLint, GetSubroutineUniformLocation, GLuint program, GLenum shadertype, const GLchar* name) DECLARE_GL_FUNCTION_STUB_END(GLint, GetSubroutineUniformLocation, program, shadertype, name)
DECLARE_GL_FUNCTION_STUB_HEAD(GLuint, GetSubroutineIndex, GLuint program, GLenum shadertype, const GLchar* name) DECLARE_GL_FUNCTION_STUB_END(GLuint, GetSubroutineIndex, program, shadertype, name)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetActiveSubroutineUniformiv, GLuint program, GLenum shadertype, GLuint index, GLenum pname, GLint* values) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetActiveSubroutineUniformiv, program, shadertype, index, pname, values)
@@ -942,28 +937,28 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, UniformSubroutinesuiv, GLenum shadertype, GL
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetUniformSubroutineuiv, GLenum shadertype, GLint location, GLuint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetUniformSubroutineuiv, shadertype, location, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramStageiv, GLuint program, GLenum shadertype, GLenum pname, GLint* values) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetProgramStageiv, program, shadertype, pname, values)
DECLARE_GL_FUNCTION_STUB_HEAD(void, PatchParameterfv, GLenum pname, const GLfloat* values) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PatchParameterfv, pname, values)
DECLARE_GL_FUNCTION_STUB_HEAD(void, DrawTransformFeedback, GLenum mode, GLuint id) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DrawTransformFeedback, mode, id)
DECLARE_GL_FUNCTION_STUB_HEAD(void, DrawTransformFeedbackStream, GLenum mode, GLuint id, GLuint stream) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DrawTransformFeedbackStream, mode, id, stream)
DECLARE_GL_FUNCTION_STUB_HEAD(void, BeginQueryIndexed, GLenum target, GLuint index, GLuint id) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BeginQueryIndexed, target, index, id)
DECLARE_GL_FUNCTION_STUB_HEAD(void, EndQueryIndexed, GLenum target, GLuint index) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, EndQueryIndexed, target, index)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetQueryIndexediv, GLenum target, GLuint index, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetQueryIndexediv, target, index, pname, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniform1d, GLuint program, GLint location, GLdouble v0) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniform1d, program, location, v0)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniform1dv, GLuint program, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniform1dv, program, location, count, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniform2d, GLuint program, GLint location, GLdouble v0, GLdouble v1) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniform2d, program, location, v0, v1)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniform2dv, GLuint program, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniform2dv, program, location, count, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniform3d, GLuint program, GLint location, GLdouble v0, GLdouble v1, GLdouble v2) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniform3d, program, location, v0, v1, v2)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniform3dv, GLuint program, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniform3dv, program, location, count, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniform4d, GLuint program, GLint location, GLdouble v0, GLdouble v1, GLdouble v2, GLdouble v3) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniform4d, program, location, v0, v1, v2, v3)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniform4dv, GLuint program, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniform4dv, program, location, count, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniformMatrix2dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniformMatrix2dv, program, location, count, transpose, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniformMatrix3dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniformMatrix3dv, program, location, count, transpose, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniformMatrix4dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniformMatrix4dv, program, location, count, transpose, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniformMatrix2x3dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniformMatrix2x3dv, program, location, count, transpose, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniformMatrix3x2dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniformMatrix3x2dv, program, location, count, transpose, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniformMatrix2x4dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniformMatrix2x4dv, program, location, count, transpose, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniformMatrix4x2dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniformMatrix4x2dv, program, location, count, transpose, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniformMatrix3x4dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniformMatrix3x4dv, program, location, count, transpose, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniformMatrix4x3dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniformMatrix4x3dv, program, location, count, transpose, value)
DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedback, GLenum mode, GLuint id) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedback, mode, id)
DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedbackStream, GLenum mode, GLuint id, GLuint stream) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedbackStream, mode, id, stream)
DECLARE_GL_FUNCTION_HEAD(void, BeginQueryIndexed, GLenum target, GLuint index, GLuint id) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BeginQueryIndexed, target, index, id)
DECLARE_GL_FUNCTION_HEAD(void, EndQueryIndexed, GLenum target, GLuint index) DECLARE_GL_FUNCTION_END_NO_RETURN(void, EndQueryIndexed, target, index)
DECLARE_GL_FUNCTION_HEAD(void, GetQueryIndexediv, GLenum target, GLuint index, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetQueryIndexediv, target, index, pname, params)
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniform1d, GLuint program, GLint location, GLdouble v0) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniform1d, program, location, v0)
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniform1dv, GLuint program, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniform1dv, program, location, count, value)
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniform2d, GLuint program, GLint location, GLdouble v0, GLdouble v1) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniform2d, program, location, v0, v1)
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniform2dv, GLuint program, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniform2dv, program, location, count, value)
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniform3d, GLuint program, GLint location, GLdouble v0, GLdouble v1, GLdouble v2) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniform3d, program, location, v0, v1, v2)
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniform3dv, GLuint program, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniform3dv, program, location, count, value)
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniform4d, GLuint program, GLint location, GLdouble v0, GLdouble v1, GLdouble v2, GLdouble v3) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniform4d, program, location, v0, v1, v2, v3)
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniform4dv, GLuint program, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniform4dv, program, location, count, value)
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniformMatrix2dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniformMatrix2dv, program, location, count, transpose, value)
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniformMatrix3dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniformMatrix3dv, program, location, count, transpose, value)
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniformMatrix4dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniformMatrix4dv, program, location, count, transpose, value)
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniformMatrix2x3dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniformMatrix2x3dv, program, location, count, transpose, value)
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniformMatrix3x2dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniformMatrix3x2dv, program, location, count, transpose, value)
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniformMatrix2x4dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniformMatrix2x4dv, program, location, count, transpose, value)
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniformMatrix4x2dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniformMatrix4x2dv, program, location, count, transpose, value)
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniformMatrix3x4dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniformMatrix3x4dv, program, location, count, transpose, value)
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniformMatrix4x3dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniformMatrix4x3dv, program, location, count, transpose, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribL1d, GLuint index, GLdouble x) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribL1d, index, x)
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribL2d, GLuint index, GLdouble x, GLdouble y) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribL2d, index, x, y)
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribL3d, GLuint index, GLdouble x, GLdouble y, GLdouble z) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribL3d, index, x, y, z)
@@ -988,8 +983,8 @@ DECLARE_GL_FUNCTION_HEAD(void, DrawArraysInstancedBaseInstance, GLenum mode, GLi
DECLARE_GL_FUNCTION_HEAD(void, DrawElementsInstancedBaseInstance, GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawElementsInstancedBaseInstance, mode, count, type, indices, instancecount, baseinstance)
DECLARE_GL_FUNCTION_HEAD(void, DrawElementsInstancedBaseVertexBaseInstance, GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLint basevertex, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawElementsInstancedBaseVertexBaseInstance, mode, count, type, indices, instancecount, basevertex, baseinstance)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetActiveAtomicCounterBufferiv, GLuint program, GLuint bufferIndex, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetActiveAtomicCounterBufferiv, program, bufferIndex, pname, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, DrawTransformFeedbackInstanced, GLenum mode, GLuint id, GLsizei instancecount) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DrawTransformFeedbackInstanced, mode, id, instancecount)
DECLARE_GL_FUNCTION_STUB_HEAD(void, DrawTransformFeedbackStreamInstanced, GLenum mode, GLuint id, GLuint stream, GLsizei instancecount) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DrawTransformFeedbackStreamInstanced, mode, id, stream, instancecount)
DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedbackInstanced, GLenum mode, GLuint id, GLsizei instancecount) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedbackInstanced, mode, id, instancecount)
DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedbackStreamInstanced, GLenum mode, GLuint id, GLuint stream, GLsizei instancecount) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedbackStreamInstanced, mode, id, stream, instancecount)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ClearBufferData, GLenum target, GLenum internalformat, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClearBufferData, target, internalformat, format, type, data)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ClearBufferSubData, GLenum target, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClearBufferSubData, target, internalformat, offset, size, format, type, data)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetInternalformati64v, GLenum target, GLenum internalformat, GLenum pname, GLsizei count, GLint64* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetInternalformati64v, target, internalformat, pname, count, params)
@@ -1002,7 +997,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(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, 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, 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)
@@ -1013,12 +1008,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_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, CreateTransformFeedbacks, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_STUB_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_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_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_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_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, CreateTransformFeedbacks, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CreateTransformFeedbacks, n, ids)
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_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_HEAD(void, GetTransformFeedbackiv, GLuint xfb, GLenum pname, GLint* param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetTransformFeedbackiv, xfb, pname, 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_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, 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)
@@ -1031,32 +1026,32 @@ DECLARE_GL_FUNCTION_HEAD(void, FlushMappedNamedBufferRange, GLuint buffer, GLint
DECLARE_GL_FUNCTION_HEAD(void, GetNamedBufferParameteriv, GLuint buffer, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetNamedBufferParameteriv, buffer, pname, params)
DECLARE_GL_FUNCTION_HEAD(void, GetNamedBufferParameteri64v, GLuint buffer, GLenum pname, GLint64* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetNamedBufferParameteri64v, buffer, pname, params)
DECLARE_GL_FUNCTION_HEAD(void, GetNamedBufferPointerv, GLuint buffer, GLenum pname, void** params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetNamedBufferPointerv, buffer, pname, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetNamedBufferSubData, GLuint buffer, GLintptr offset, GLsizeiptr size, void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetNamedBufferSubData, buffer, offset, size, data)
DECLARE_GL_FUNCTION_HEAD(void, GetNamedBufferSubData, GLuint buffer, GLintptr offset, GLsizeiptr size, void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetNamedBufferSubData, buffer, offset, size, data)
DECLARE_GL_FUNCTION_HEAD(void, CreateFramebuffers, GLsizei n, GLuint* framebuffers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CreateFramebuffers, n, framebuffers)
DECLARE_GL_FUNCTION_HEAD(void, NamedFramebufferRenderbuffer, GLuint framebuffer, GLenum attachment, GLenum renderbuffertarget, GLuint renderbuffer) DECLARE_GL_FUNCTION_END_NO_RETURN(void, NamedFramebufferRenderbuffer, framebuffer, attachment, renderbuffertarget, renderbuffer)
DECLARE_GL_FUNCTION_STUB_HEAD(void, NamedFramebufferParameteri, GLuint framebuffer, GLenum pname, GLint param) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, NamedFramebufferParameteri, framebuffer, pname, param)
DECLARE_GL_FUNCTION_HEAD(void, NamedFramebufferParameteri, GLuint framebuffer, GLenum pname, GLint param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, NamedFramebufferParameteri, framebuffer, pname, param)
DECLARE_GL_FUNCTION_HEAD(void, NamedFramebufferTexture, GLuint framebuffer, GLenum attachment, GLuint texture, GLint level) DECLARE_GL_FUNCTION_END_NO_RETURN(void, NamedFramebufferTexture, framebuffer, attachment, texture, level)
DECLARE_GL_FUNCTION_HEAD(void, NamedFramebufferTextureLayer, GLuint framebuffer, GLenum attachment, GLuint texture, GLint level, GLint layer) DECLARE_GL_FUNCTION_END_NO_RETURN(void, NamedFramebufferTextureLayer, framebuffer, attachment, texture, level, layer)
DECLARE_GL_FUNCTION_HEAD(void, NamedFramebufferDrawBuffer, GLuint framebuffer, GLenum buf) DECLARE_GL_FUNCTION_END_NO_RETURN(void, NamedFramebufferDrawBuffer, framebuffer, buf)
DECLARE_GL_FUNCTION_HEAD(void, NamedFramebufferDrawBuffers, GLuint framebuffer, GLsizei n, const GLenum* bufs) DECLARE_GL_FUNCTION_END_NO_RETURN(void, NamedFramebufferDrawBuffers, framebuffer, n, bufs)
DECLARE_GL_FUNCTION_HEAD(void, NamedFramebufferReadBuffer, GLuint framebuffer, GLenum src) DECLARE_GL_FUNCTION_END_NO_RETURN(void, NamedFramebufferReadBuffer, framebuffer, src)
DECLARE_GL_FUNCTION_STUB_HEAD(void, InvalidateNamedFramebufferData, GLuint framebuffer, GLsizei numAttachments, const GLenum* attachments) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, InvalidateNamedFramebufferData, framebuffer, numAttachments, attachments)
DECLARE_GL_FUNCTION_STUB_HEAD(void, InvalidateNamedFramebufferSubData, GLuint framebuffer, GLsizei numAttachments, const GLenum* attachments, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, InvalidateNamedFramebufferSubData, framebuffer, numAttachments, attachments, x, y, width, height)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ClearNamedFramebufferiv, GLuint framebuffer, GLenum buffer, GLint drawbuffer, const GLint* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClearNamedFramebufferiv, framebuffer, buffer, drawbuffer, value)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ClearNamedFramebufferuiv, GLuint framebuffer, GLenum buffer, GLint drawbuffer, const GLuint* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClearNamedFramebufferuiv, framebuffer, buffer, drawbuffer, value)
DECLARE_GL_FUNCTION_HEAD(void, InvalidateNamedFramebufferData, GLuint framebuffer, GLsizei numAttachments, const GLenum* attachments) DECLARE_GL_FUNCTION_END_NO_RETURN(void, InvalidateNamedFramebufferData, framebuffer, numAttachments, attachments)
DECLARE_GL_FUNCTION_HEAD(void, InvalidateNamedFramebufferSubData, GLuint framebuffer, GLsizei numAttachments, const GLenum* attachments, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, InvalidateNamedFramebufferSubData, framebuffer, numAttachments, attachments, x, y, width, height)
DECLARE_GL_FUNCTION_HEAD(void, ClearNamedFramebufferiv, GLuint framebuffer, GLenum buffer, GLint drawbuffer, const GLint* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearNamedFramebufferiv, framebuffer, buffer, drawbuffer, value)
DECLARE_GL_FUNCTION_HEAD(void, ClearNamedFramebufferuiv, GLuint framebuffer, GLenum buffer, GLint drawbuffer, const GLuint* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearNamedFramebufferuiv, framebuffer, buffer, drawbuffer, value)
DECLARE_GL_FUNCTION_HEAD(void, ClearNamedFramebufferfv, GLuint framebuffer, GLenum buffer, GLint drawbuffer, const GLfloat* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearNamedFramebufferfv, framebuffer, buffer, drawbuffer, value)
DECLARE_GL_FUNCTION_HEAD(void, ClearNamedFramebufferfi, GLuint framebuffer, GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearNamedFramebufferfi, framebuffer, buffer, drawbuffer, depth, stencil)
DECLARE_GL_FUNCTION_HEAD(void, BlitNamedFramebuffer, GLuint readFramebuffer, GLuint drawFramebuffer, GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BlitNamedFramebuffer, readFramebuffer, drawFramebuffer, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter)
DECLARE_GL_FUNCTION_HEAD(GLenum, CheckNamedFramebufferStatus, GLuint framebuffer, GLenum target) DECLARE_GL_FUNCTION_END(GLenum, CheckNamedFramebufferStatus, framebuffer, target)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetNamedFramebufferParameteriv, GLuint framebuffer, GLenum pname, GLint* param) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetNamedFramebufferParameteriv, framebuffer, pname, param)
DECLARE_GL_FUNCTION_HEAD(void, GetNamedFramebufferParameteriv, GLuint framebuffer, GLenum pname, GLint* param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetNamedFramebufferParameteriv, framebuffer, pname, param)
DECLARE_GL_FUNCTION_HEAD(void, GetNamedFramebufferAttachmentParameteriv, GLuint framebuffer, GLenum attachment, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetNamedFramebufferAttachmentParameteriv, framebuffer, attachment, pname, params)
DECLARE_GL_FUNCTION_HEAD(void, CreateRenderbuffers, GLsizei n, GLuint* renderbuffers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CreateRenderbuffers, n, renderbuffers)
DECLARE_GL_FUNCTION_HEAD(void, NamedRenderbufferStorage, GLuint renderbuffer, GLenum internalformat, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, NamedRenderbufferStorage, renderbuffer, 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, 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_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, TextureBuffer, GLuint texture, GLenum internalformat, GLuint buffer) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureBuffer, texture, internalformat, buffer)
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, 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)
@@ -1068,9 +1063,9 @@ DECLARE_GL_FUNCTION_HEAD(void, TextureSubImage3D, GLuint texture, GLint level, G
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLsizei width, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage1D, texture, level, xoffset, width, format, imageSize, data)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, imageSize, data)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, data)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CopyTextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CopyTextureSubImage1D, texture, level, xoffset, x, y, width)
DECLARE_GL_FUNCTION_HEAD(void, CopyTextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyTextureSubImage1D, texture, level, xoffset, x, y, width)
DECLARE_GL_FUNCTION_HEAD(void, CopyTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyTextureSubImage2D, texture, level, xoffset, yoffset, x, y, width, height)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CopyTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CopyTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, x, y, width, height)
DECLARE_GL_FUNCTION_HEAD(void, CopyTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, x, y, width, height)
DECLARE_GL_FUNCTION_HEAD(void, TextureParameterf, GLuint texture, GLenum pname, GLfloat param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureParameterf, texture, pname, param)
DECLARE_GL_FUNCTION_HEAD(void, TextureParameterfv, GLuint texture, GLenum pname, const GLfloat* param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureParameterfv, texture, pname, param)
DECLARE_GL_FUNCTION_HEAD(void, TextureParameteri, GLuint texture, GLenum pname, GLint param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureParameteri, texture, pname, param)
@@ -1080,7 +1075,7 @@ DECLARE_GL_FUNCTION_HEAD(void, TextureParameteriv, GLuint texture, GLenum pname,
DECLARE_GL_FUNCTION_HEAD(void, GenerateTextureMipmap, GLuint texture) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GenerateTextureMipmap, texture)
DECLARE_GL_FUNCTION_HEAD(void, BindTextureUnit, GLuint unit, GLuint texture) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindTextureUnit, unit, texture)
DECLARE_GL_FUNCTION_HEAD(void, GetTextureImage, GLuint texture, GLint level, GLenum format, GLenum type, GLsizei bufSize, void* pixels) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetTextureImage, texture, level, format, type, bufSize, pixels)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetCompressedTextureImage, GLuint texture, GLint level, GLsizei bufSize, void* pixels) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetCompressedTextureImage, texture, level, bufSize, pixels)
DECLARE_GL_FUNCTION_HEAD(void, GetCompressedTextureImage, GLuint texture, GLint level, GLsizei bufSize, void* pixels) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetCompressedTextureImage, texture, level, bufSize, pixels)
DECLARE_GL_FUNCTION_HEAD(void, GetTextureLevelParameterfv, GLuint texture, GLint level, GLenum pname, GLfloat* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetTextureLevelParameterfv, texture, level, pname, params)
DECLARE_GL_FUNCTION_HEAD(void, GetTextureLevelParameteriv, GLuint texture, GLint level, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetTextureLevelParameteriv, texture, level, pname, params)
DECLARE_GL_FUNCTION_HEAD(void, GetTextureParameterfv, GLuint texture, GLenum pname, GLfloat* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetTextureParameterfv, texture, pname, params)
@@ -1096,18 +1091,18 @@ 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, 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_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_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_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_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, GetVertexArrayiv, GLuint vaobj, GLenum pname, GLint* param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetVertexArrayiv, vaobj, 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_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_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_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, 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, GetQueryBufferObjectuiv, GLuint id, GLuint buffer, GLenum pname, GLintptr offset) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetQueryBufferObjectuiv, id, buffer, pname, offset)
DECLARE_GL_FUNCTION_HEAD(void, CreateProgramPipelines, GLsizei n, GLuint* pipelines) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CreateProgramPipelines, n, pipelines)
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_HEAD(void, GetQueryBufferObjecti64v, GLuint id, GLuint buffer, GLenum pname, GLintptr offset) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetQueryBufferObjecti64v, id, buffer, pname, offset)
DECLARE_GL_FUNCTION_HEAD(void, GetQueryBufferObjectiv, GLuint id, GLuint buffer, GLenum pname, GLintptr offset) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetQueryBufferObjectiv, id, buffer, pname, offset)
DECLARE_GL_FUNCTION_HEAD(void, GetQueryBufferObjectui64v, GLuint id, GLuint buffer, GLenum pname, GLintptr offset) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetQueryBufferObjectui64v, id, buffer, pname, offset)
DECLARE_GL_FUNCTION_HEAD(void, GetQueryBufferObjectuiv, GLuint id, GLuint buffer, GLenum pname, GLintptr offset) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetQueryBufferObjectuiv, id, buffer, pname, offset)
DECLARE_GL_FUNCTION_HEAD(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) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetTextureSubImage, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, type, bufSize, pixels)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetCompressedTextureSubImage, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLsizei bufSize, void* pixels) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetCompressedTextureSubImage, texture, level, xoffset, yoffset, zoffset, width, height, depth, bufSize, pixels)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnCompressedTexImage, GLenum target, GLint lod, GLsizei bufSize, void* pixels) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnCompressedTexImage, target, lod, bufSize, pixels)
File diff suppressed because it is too large Load Diff
@@ -14,6 +14,8 @@
namespace MobileGL::MG_Impl::GLImpl {
/* @INSERTION_POINT:FUNCTION_DECLARATION@ */
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 ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value);
void ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value);
@@ -56,7 +58,19 @@ namespace MobileGL::MG_Impl::GLImpl {
void NamedFramebufferReadBuffer(GLuint framebuffer, GLenum src);
void ClearNamedFramebufferfv(GLuint framebuffer, GLenum buffer, GLint drawbuffer, const GLfloat* value);
void ClearNamedFramebufferfi(GLuint framebuffer, GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
void InvalidateNamedFramebufferData(GLuint framebuffer, GLsizei numAttachments, const GLenum* attachments);
void InvalidateNamedFramebufferSubData(GLuint framebuffer, GLsizei numAttachments, const GLenum* attachments,
GLint x, GLint y, GLsizei width, GLsizei height);
void InvalidateFramebuffer(GLenum target, GLsizei numAttachments, const GLenum* attachments);
void InvalidateSubFramebuffer(GLenum target, GLsizei numAttachments, const GLenum* attachments, GLint x, GLint y,
GLsizei width, GLsizei height);
void ClearNamedFramebufferiv(GLuint framebuffer, GLenum buffer, GLint drawbuffer, const GLint* value);
void ClearNamedFramebufferuiv(GLuint framebuffer, GLenum buffer, GLint drawbuffer, const GLuint* value);
GLenum CheckNamedFramebufferStatus(GLuint framebuffer, GLenum target);
void GetFramebufferParameteriv(GLenum target, GLenum pname, GLint* params);
void FramebufferParameteri(GLenum target, GLenum pname, GLint param);
void GetNamedFramebufferParameteriv(GLuint framebuffer, GLenum pname, GLint* params);
void NamedFramebufferParameteri(GLuint framebuffer, GLenum pname, GLint param);
void GetNamedFramebufferAttachmentParameteriv(GLuint framebuffer, GLenum attachment, GLenum pname, GLint* params);
void BlitNamedFramebuffer(GLuint readFramebuffer, GLuint drawFramebuffer, GLint srcX0, GLint srcY0, GLint srcX1,
GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask,
@@ -7,6 +7,7 @@
// End of Source File Header
#include "Validators.h"
#include <MG_Backend/BackendObjects.h>
#include <MG_State/GLState/Core.h>
#include <MG_State/GLState/ErrorState/Error.h>
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
@@ -60,6 +61,26 @@ namespace MobileGL::MG_Impl::GLImpl::FramebufferImpl {
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) {
if (target == RenderbufferTarget::Unknown) {
using namespace MG_Util;
@@ -97,4 +118,100 @@ namespace MobileGL::MG_Impl::GLImpl::FramebufferImpl {
std::format("Renderbuffer name {} is not valid.", index)));
return false;
}
Bool ValidateFramebufferParameterPname(GLenum pname, Bool isDefaultFramebuffer, Bool forSetter,
const char* caller) {
Bool isDefaultParameter = false;
switch (pname) {
case GL_FRAMEBUFFER_DEFAULT_WIDTH:
case GL_FRAMEBUFFER_DEFAULT_HEIGHT:
case GL_FRAMEBUFFER_DEFAULT_LAYERS:
case GL_FRAMEBUFFER_DEFAULT_SAMPLES:
case GL_FRAMEBUFFER_DEFAULT_FIXED_SAMPLE_LOCATIONS:
isDefaultParameter = true;
break;
case GL_DOUBLEBUFFER:
case GL_IMPLEMENTATION_COLOR_READ_FORMAT:
case GL_IMPLEMENTATION_COLOR_READ_TYPE:
case GL_SAMPLES:
case GL_SAMPLE_BUFFERS:
case GL_STEREO:
// Queryable only; glFramebufferParameteri sets none of these.
if (forSetter) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl/FramebufferImpl", caller,
std::format("pname {} is not settable on a framebuffer.",
MG_Util::ConvertGLEnumToString(pname))));
return false;
}
break;
default:
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl/FramebufferImpl", caller,
std::format("pname {} is not a framebuffer parameter.",
MG_Util::ConvertGLEnumToString(pname))));
return false;
}
// The default framebuffer has no DEFAULT_* state of its own - its shape comes from the
// surface - so those names are accepted enums it simply cannot answer or accept.
if (isDefaultFramebuffer && isDefaultParameter) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl/FramebufferImpl", caller,
std::format("pname {} does not apply to the default framebuffer.",
MG_Util::ConvertGLEnumToString(pname))));
return false;
}
return true;
}
Bool ValidateReadFramebufferForCopy(const char* caller) {
auto& framebufferObject =
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();
if (!framebufferObject || !framebufferObject->CheckCompleteness()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidFramebufferOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl/FramebufferImpl", caller,
"Read framebuffer is not framebuffer complete."));
return false;
}
const FramebufferAttachmentType readBuffer = framebufferObject->GetReadBuffer();
if (readBuffer == FramebufferAttachmentType::None ||
!framebufferObject->GetAttachment(readBuffer).IsValid()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl/FramebufferImpl", caller,
"Read buffer names no attachment of the read framebuffer."));
return false;
}
// SAMPLE_BUFFERS is one whenever the read buffer resolves to multisample storage. A
// multisample texture says so by its target - its sample count can legally be one - while a
// renderbuffer says so by having been given a non-zero sample count.
const auto& readAttachment = framebufferObject->GetAttachment(readBuffer);
Bool isMultisampled = false;
if (readAttachment.IsRenderbuffer() && readAttachment.GetRenderbuffer()) {
isMultisampled = readAttachment.GetRenderbuffer()->GetSamples() > 0;
} else if (readAttachment.IsTexture() && readAttachment.GetTexture()) {
const auto target = readAttachment.GetTexture()->GetTarget();
isMultisampled = target == TextureTarget::Texture2DMultisample ||
target == TextureTarget::Texture2DMultisampleArray;
}
if (isMultisampled) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl/FramebufferImpl", caller,
"Cannot copy from a multisampled read framebuffer."));
return false;
}
return true;
}
} // namespace MobileGL::MG_Impl::GLImpl::FramebufferImpl
@@ -14,6 +14,21 @@ namespace MobileGL::MG_Impl::GLImpl::FramebufferImpl {
Bool ValidateFramebufferTarget(FramebufferTarget target);
Bool ValidateFramebufferName(Uint index, Bool allowZero = true);
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 ValidateRenderbufferName(Uint index, Bool allowZero = true);
// The read-framebuffer preconditions the CopyTexSubImage family shares (GL 4.6 core 8.6): the
// read framebuffer must be complete, its read buffer must name a real attachment, and it must
// not be multisampled. Incompleteness is INVALID_FRAMEBUFFER_OPERATION, the other two are
// INVALID_OPERATION.
Bool ValidateReadFramebufferForCopy(const char* caller);
// The pname sets of glGet/FramebufferParameteri (GL 4.6 core 9.2.3). Order matters and is part
// of the contract: a name outside the table is INVALID_ENUM, and only then is a name that the
// DEFAULT framebuffer does not answer INVALID_OPERATION. Testing the framebuffer kind first
// would turn GL_FRAMEBUFFER_DEFAULT_WIDTH on framebuffer zero into the wrong error.
Bool ValidateFramebufferParameterPname(GLenum pname, Bool isDefaultFramebuffer, Bool forSetter,
const char* caller);
} // namespace MobileGL::MG_Impl::GLImpl::FramebufferImpl
+121 -11
View File
@@ -213,8 +213,13 @@ namespace MobileGL::MG_Impl::GLImpl {
GLint maxSamples = 0;
for (const auto& attachment : drawFbo->GetAllAttachmentObjects()) {
if (!attachment.IsRenderbuffer() || !attachment.GetRenderbuffer()) continue;
maxSamples = std::max(maxSamples, static_cast<GLint>(attachment.GetRenderbuffer()->GetSamples()));
if (attachment.IsRenderbuffer() && attachment.GetRenderbuffer()) {
maxSamples = std::max(maxSamples, static_cast<GLint>(attachment.GetRenderbuffer()->GetSamples()));
} else if (attachment.IsTexture() && attachment.GetTexture()) {
// Multisample texture attachments count too (GL_SAMPLE_BUFFERS must
// report 1 for any multisampled draw framebuffer).
maxSamples = std::max(maxSamples, static_cast<GLint>(attachment.GetTexture()->GetSamples()));
}
}
return maxSamples;
}
@@ -465,6 +470,14 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_STENCIL_TEST:
*params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::StencilTest) ? GL_TRUE : GL_FALSE;
return;
case GL_MIN_FRAGMENT_INTERPOLATION_OFFSET:
case GL_MAX_FRAGMENT_INTERPOLATION_OFFSET:
case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS: {
GLfloat value = 0.0f;
GetFloatv(pname, &value);
*params = value != 0.0f ? GL_TRUE : GL_FALSE;
return;
}
default:
break;
}
@@ -520,6 +533,19 @@ namespace MobileGL::MG_Impl::GLImpl {
params[1] = dynamicParameters.ViewportBoundsRangeMax;
return;
}
case GL_MIN_FRAGMENT_INTERPOLATION_OFFSET:
case GL_MAX_FRAGMENT_INTERPOLATION_OFFSET:
case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS: {
const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
if (pname == GL_MIN_FRAGMENT_INTERPOLATION_OFFSET) {
params[0] = dynamicParameters.MinFragmentInterpolationOffset;
} else if (pname == GL_MAX_FRAGMENT_INTERPOLATION_OFFSET) {
params[0] = dynamicParameters.MaxFragmentInterpolationOffset;
} else {
params[0] = static_cast<GLfloat>(dynamicParameters.FragmentInterpolationOffsetBits);
}
return;
}
case GL_DEPTH_CLEAR_VALUE:
params[0] = MG_State::pGLContext->GetClearDepth();
return;
@@ -645,6 +671,40 @@ namespace MobileGL::MG_Impl::GLImpl {
}
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_LEVEL:
case GL_IMAGE_BINDING_LAYERED:
@@ -1004,6 +1064,11 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
return;
}
case GL_DRAW_INDIRECT_BUFFER_BINDING: {
auto& obj = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
*params = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
return;
}
case GL_MAX_DEBUG_GROUP_STACK_DEPTH:
*params = 0; // debug-group entrypoints are stubbed
return;
@@ -1367,7 +1432,7 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = 0; // program-binary entrypoints are stubbed
return;
case GL_PROGRAM_PIPELINE_BINDING:
*params = 0; // program-pipeline entrypoints are stubbed
*params = static_cast<GLint>(MG_State::pGLContext->GetBoundProgramPipelineName());
return;
case GL_PROGRAM_POINT_SIZE:
*params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::ProgramPointSize) ? GL_TRUE : GL_FALSE;
@@ -1638,7 +1703,7 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = static_cast<GLint>(MG_State::pGLContext->GetHint(pname));
return;
case GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT:
*params = 0; // texture-buffer range entrypoints are stubbed
*params = MG_Backend::pActiveBackendObject->GetDynamicParameters().TextureBufferOffsetAlignment;
return;
case GL_TIMESTAMP: {
Int64 timestamp = 0;
@@ -1707,20 +1772,22 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = vao ? static_cast<GLint>(vao->GetExternalIndex()) : 0;
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:
*params = 0; // vertex-binding entrypoints are stubbed
return;
case GL_VERTEX_BINDING_OFFSET:
*params = 0; // vertex-binding entrypoints are stubbed
return;
case GL_VERTEX_BINDING_STRIDE:
*params = 0; // vertex-binding entrypoints are stubbed
RecordIndexedOnlyGetterError(__func__, pname);
return;
case GL_MAX_VERTEX_ATTRIB_RELATIVE_OFFSET:
*params = 0; // vertex-binding entrypoints are stubbed
*params = static_cast<GLint>(VertexArrayImpl::GetMaxVertexAttribRelativeOffset());
return;
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;
case GL_VIEWPORT: {
const auto& vp = MG_State::pGLContext->GetViewport();
@@ -1886,6 +1953,21 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_MAX_SAMPLE_MASK_WORDS:
*params = dynamicParameters.MaxSampleMaskWords;
break;
case GL_PATCH_VERTICES:
*params = static_cast<GLint>(MG_State::pGLContext->GetPatchVertices());
break;
case GL_MAX_PATCH_VERTICES:
*params = dynamicParameters.MaxPatchVertices;
break;
case GL_MAX_TESS_GEN_LEVEL:
*params = dynamicParameters.MaxTessGenLevel;
break;
case GL_MIN_PROGRAM_TEXTURE_GATHER_OFFSET:
*params = dynamicParameters.MinProgramTextureGatherOffset;
break;
case GL_MAX_PROGRAM_TEXTURE_GATHER_OFFSET:
*params = dynamicParameters.MaxProgramTextureGatherOffset;
break;
case GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS:
*params = static_cast<GLint>(GetIndexedBufferQueryPointCount(BufferTarget::ShaderStorage));
break;
@@ -1901,6 +1983,25 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_COMPONENTS:
*params = kFrontendMaxTransformFeedbackSeparateComponents;
break;
// ARB_transform_feedback3 limits. The GL CTS queries these before checking
// whether the extension is advertised and requires no GL error; desktop
// drivers all accept them, so answer with the separate-attrib capacity and
// the single vertex stream the backends provide.
case GL_MAX_TRANSFORM_FEEDBACK_BUFFERS:
*params = kFrontendMaxTransformFeedbackSeparateAttribs;
break;
case GL_MAX_VERTEX_STREAMS:
*params = 1;
break;
case GL_TRANSFORM_FEEDBACK_ACTIVE:
*params = MG_State::pGLContext->IsTransformFeedbackActive() ? 1 : 0;
break;
case GL_TRANSFORM_FEEDBACK_PAUSED:
*params = MG_State::pGLContext->IsTransformFeedbackPaused() ? 1 : 0;
break;
case GL_TRANSFORM_FEEDBACK_BINDING:
*params = static_cast<GLint>(MG_State::pGLContext->GetBoundTransformFeedbackName());
break;
case GL_MAX_TEXTURE_IMAGE_UNITS:
*params = dynamicParameters.MaxTextureImageUnits;
break;
@@ -1957,6 +2058,15 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_SUBPIXEL_BITS:
*params = std::max(dynamicParameters.ViewportSubpixelBits, kFrontendSubpixelBits);
break;
case GL_MIN_FRAGMENT_INTERPOLATION_OFFSET:
*params = static_cast<GLint>(std::lround(dynamicParameters.MinFragmentInterpolationOffset));
break;
case GL_MAX_FRAGMENT_INTERPOLATION_OFFSET:
*params = static_cast<GLint>(std::lround(dynamicParameters.MaxFragmentInterpolationOffset));
break;
case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS:
*params = dynamicParameters.FragmentInterpolationOffsetBits;
break;
case GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT:
*params = static_cast<Int>(dynamicParameters.UniformBufferOffsetAlignment);
break;
+681 -14
View File
@@ -8,6 +8,8 @@
#include "GL_Program.h"
#include "Config.h"
#include <cmath>
#include <limits>
#include <MG_Impl/GLImpl/VertexArray/Validators.h>
#include <MG_State/GLState/Core.h>
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
@@ -49,10 +51,18 @@ namespace MobileGL::MG_Impl::GLImpl {
static bool CheckProgramNameValidity(GLuint program) {
if (!MG_State::pGLContext->ValidateProgramName(program)) {
// Programs and shaders share one name space: a name that exists but
// belongs to a shader is INVALID_OPERATION, a name GL never handed
// out is INVALID_VALUE (GL 3.3 core 2.11.x).
const ErrorCode error = MG_State::pGLContext->ValidateShaderName(program)
? ErrorCode::InvalidOperation
: ErrorCode::InvalidValue;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
error,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
std::to_string(program) + " is not a valid name."));
std::to_string(program) +
(error == ErrorCode::InvalidOperation ? " is not a program object."
: " is not a valid name.")));
return false;
}
return true;
@@ -195,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) {
if (bufSize <= 0) {
if (length) *length = 0;
@@ -317,11 +371,16 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void DeleteProgram_State(GLuint program) {
// "If program is zero, it is silently ignored" (GL 4.6 core 7.3) - unlike every
// other program entry point, where 0 is a name GL never handed out.
if (program == 0) return;
if (!CheckProgramNameValidity(program)) return;
MG_State::pGLContext->MarkProgramForDeletion(program);
}
void DeleteShader_State(GLuint shader) {
// Same silent-zero rule as glDeleteProgram (GL 4.6 core 7.1).
if (shader == 0) return;
if (!CheckShaderNameValidity(shader)) return;
MG_State::pGLContext->MarkShaderForDeletion(shader);
}
@@ -605,6 +664,18 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = programObject->GetActiveUniformBlocksMaxNameLength() + 1;
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
break;
case GL_TRANSFORM_FEEDBACK_VARYINGS:
*params = static_cast<GLint>(programObject->GetTransformFeedbackVaryingCount());
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
break;
case GL_TRANSFORM_FEEDBACK_BUFFER_MODE:
*params = static_cast<GLint>(programObject->GetTransformFeedbackBufferMode());
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
break;
case GL_TRANSFORM_FEEDBACK_VARYING_MAX_LENGTH:
*params = programObject->GetTransformFeedbackVaryingMaxLength();
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
break;
case GL_COMPUTE_WORK_GROUP_SIZE: { // GL >= 4.3
if (!programObject->GetLinkStatus() || programObject->GetShaderIndexByStage(ShaderStage::Compute) < 0) {
MG_State::pGLContext->RecordError(
@@ -623,10 +694,14 @@ namespace MobileGL::MG_Impl::GLImpl {
}
case GL_PROGRAM_BINARY_LENGTH:
// No program binary format is exposed, so a program never has a retrievable
// binary and its length is zero (ARB_get_program_binary).
*params = 0;
break;
case GL_PROGRAM_BINARY_RETRIEVABLE_HINT:
*params = programObject->GetBinaryRetrievableHint() ? GL_TRUE : GL_FALSE;
break;
case GL_TRANSFORM_FEEDBACK_BUFFER_MODE:
case GL_TRANSFORM_FEEDBACK_VARYINGS:
case GL_TRANSFORM_FEEDBACK_VARYING_MAX_LENGTH:
case GL_GEOMETRY_VERTICES_OUT:
case GL_GEOMETRY_INPUT_TYPE:
case GL_GEOMETRY_OUTPUT_TYPE:
@@ -801,7 +876,8 @@ namespace MobileGL::MG_Impl::GLImpl {
}
if constexpr (std::is_same_v<T, GLfloat>) {
if (ttype->isMatrix() && ttype->getMatrixCols() == 3) {
if (ttype->getBasicType() != glslang::EbtDouble && ttype->isMatrix() &&
ttype->getMatrixCols() == 3) {
auto* pBase = pUBO + offset;
for (int i = 0; i < ttype->getMatrixRows(); i++) {
Memcpy(reinterpret_cast<char*>(params) + ttype->getMatrixCols() * sizeof(GLfloat) * i,
@@ -811,9 +887,46 @@ namespace MobileGL::MG_Impl::GLImpl {
}
}
// A double-precision uniform is the one case where the stored component type can
// differ from the queried one for a non-opaque uniform, and the difference is not
// just a reinterpretation: it is twice as wide, so a raw copy would overrun the
// caller's buffer as well as return nonsense. Read component by component and let
// GL's conversion rules (7.6: round to nearest for the integer queries) apply.
if (ttype->getBasicType() == glslang::EbtDouble) {
const Int columns = ttype->isMatrix() ? ttype->getMatrixCols() : 1;
const Int rows = ttype->isMatrix() ? ttype->getMatrixRows()
: (ttype->isVector() ? ttype->getVectorSize() : 1);
// The slot the linker handed out is exactly `columns` columns wide, so it also
// states the column stride - which for a double matrix is not a float's 16 bytes.
const SizeT columnStride = columns > 0 ? size / static_cast<SizeT>(columns) : size;
for (Int column = 0; column < columns; ++column) {
for (Int row = 0; row < rows; ++row) {
GLdouble component = 0.0;
Memcpy(&component, pUBO + offset + column * columnStride + row * sizeof(GLdouble),
sizeof(component));
if constexpr (std::is_integral_v<T>) {
// Rounded to the nearest integer and clamped into the queried type's
// range, so a negative double read through glGetUniformuiv is 0
// rather than its two's complement.
const GLdouble rounded = std::nearbyint(component);
const GLdouble lowest = static_cast<GLdouble>(std::numeric_limits<T>::lowest());
const GLdouble highest = static_cast<GLdouble>(std::numeric_limits<T>::max());
params[column * rows + row] = static_cast<T>(std::clamp(rounded, lowest, highest));
} else {
params[column * rows + row] = static_cast<T>(component);
}
}
}
return;
}
Memcpy(params, pUBO + offset, size);
}
void GetUniformdv_State(GLuint program, GLint location, GLdouble* params) {
GetUniformScalar_State(program, location, params);
}
void GetUniformfv_State(GLuint program, GLint location, GLfloat* params) {
GetUniformScalar_State(program, location, params);
}
@@ -827,19 +940,13 @@ namespace MobileGL::MG_Impl::GLImpl {
}
GLboolean IsProgram_State(GLuint program) {
/* FIXME: Handle situations that:
* A program object marked for deletion with glDeleteProgram but still in use as part of current
* rendering state is still considered a program object and glIsProgram will return GL_TRUE.
*/
// Deletion-flagged names stay valid while the object is still GL-visible (program in
// use, shader attached), so name validity is exactly the Is* answer.
if (program == 0) return GL_FALSE;
return MG_State::pGLContext->ValidateProgramName(program) ? GL_TRUE : GL_FALSE;
}
GLboolean IsShader_State(GLuint shader) {
/* FIXME: Handle situations that:
* A shader object marked for deletion with glDeleteShader but still attached to a program object is still
* considered a shader object and glIsShader will return GL_TRUE.
*/
if (shader == 0) return GL_FALSE;
return MG_State::pGLContext->ValidateShaderName(shader) ? GL_TRUE : GL_FALSE;
}
@@ -849,6 +956,18 @@ namespace MobileGL::MG_Impl::GLImpl {
if (!programObject) return;
MGLOG_D("%s: linking program %d", __func__, program);
// Relinking the program an active transform feedback captures from would
// invalidate its varyings mid-capture (GL 3.3 core 2.11.3).
if (MG_State::pGLContext->IsTransformFeedbackActive() &&
MG_State::pGLContext->GetTransformFeedbackProgram().get() == programObject.get()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", __func__,
"The program used by active transform feedback cannot be relinked."));
return;
}
static Bool allowVSOnlyPrograms;
static Bool initialized = false;
if (!initialized) {
@@ -892,6 +1011,18 @@ namespace MobileGL::MG_Impl::GLImpl {
void UseProgram_State(GLuint program) {
MGLOG_D("UseProgram_State: program=%u", program);
// The program in use may not change while transform feedback is active - unless
// the capture is paused, which is exactly what ARB_transform_feedback2 added the
// pause for (GL 4.6 core 7.3).
if (MG_State::pGLContext->IsTransformFeedbackActive() &&
!MG_State::pGLContext->IsTransformFeedbackPaused()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"The current program cannot change while transform feedback is active."));
return;
}
if (program == 0) {
MG_State::pGLContext->UseProgram(0);
return;
@@ -1014,6 +1145,38 @@ namespace MobileGL::MG_Impl::GLImpl {
}
}
// glUniform*d / glUniformMatrix*dv. The vector forms need nothing beyond the shared
// upload template - it is already typed on the component - but a matrix does: the
// column stride the linker used for a double matrix is not the 16 bytes a float one
// gets. It is not guessed here; the slot the uniform was given is exactly `columns`
// columns wide, so dividing states the stride the rest of the pipeline agreed on.
template <typename Program>
void UniformMatrixdv_Object(Program& programObject, GLint location, GLsizei count, GLboolean transpose,
const GLdouble* value, Int columns, Int rows) {
const SizeT slotSize = programObject.GetUniformSizesInBytes(location);
const SizeT columnStride = columns > 0 ? slotSize / static_cast<SizeT>(columns) : slotSize;
const SizeT componentCount = static_cast<SizeT>(columns) * static_cast<SizeT>(rows);
Vector<GLdouble> column(static_cast<SizeT>(rows));
for (GLint matrix = 0; matrix < count; ++matrix) {
if (matrix > 0 && !programObject.UniformLocationsAliasSameUniform(location, location + matrix)) break;
if (!programObject.IsValidUniformLocation(location + matrix)) {
RecordInvalidUniformLocationError(__func__, location + matrix, "the current program object");
return;
}
const GLdouble* source = value + matrix * componentCount;
for (Int c = 0; c < columns; ++c) {
for (Int r = 0; r < rows; ++r) {
column[r] = transpose == GL_TRUE ? source[r * columns + c] : source[c * rows + r];
}
Uniform_State<1>(programObject, location + matrix, column.data(), c * columnStride);
for (Int r = 1; r < rows; ++r) {
Uniform_State<1>(programObject, location + matrix, column.data() + r,
c * columnStride + r * sizeof(GLdouble));
}
}
}
}
// Helper function to transpose a 2x2 matrix
void TransposeMatrix2x2(const GLfloat* input, GLfloat* output) {
// Input matrix is in column-major order (OpenGL default)
@@ -1822,6 +1985,312 @@ namespace MobileGL::MG_Impl::GLImpl {
GLuint v[] = {v0, v1, v2, v3};
Uniform4uiv(location, 1, v);
}
void Uniform1d(GLint location, GLdouble v0) {
const GLdouble v[] = {v0};
Uniformv_State<1>(location, 1, v);
}
void Uniform1dv(GLint location, GLsizei count, const GLdouble* value) {
Uniformv_State<1>(location, count, value);
}
void ProgramUniform1d(GLuint program, GLint location, GLdouble v0) {
const GLdouble v[] = {v0};
ProgramUniformv_State<1>(program, location, 1, v);
}
void ProgramUniform1dv(GLuint program, GLint location, GLsizei count, const GLdouble* value) {
ProgramUniformv_State<1>(program, location, count, value);
}
void Uniform2d(GLint location, GLdouble v0, GLdouble v1) {
const GLdouble v[] = {v0, v1};
Uniformv_State<2>(location, 1, v);
}
void Uniform2dv(GLint location, GLsizei count, const GLdouble* value) {
Uniformv_State<2>(location, count, value);
}
void ProgramUniform2d(GLuint program, GLint location, GLdouble v0, GLdouble v1) {
const GLdouble v[] = {v0, v1};
ProgramUniformv_State<2>(program, location, 1, v);
}
void ProgramUniform2dv(GLuint program, GLint location, GLsizei count, const GLdouble* value) {
ProgramUniformv_State<2>(program, location, count, value);
}
void Uniform3d(GLint location, GLdouble v0, GLdouble v1, GLdouble v2) {
const GLdouble v[] = {v0, v1, v2};
Uniformv_State<3>(location, 1, v);
}
void Uniform3dv(GLint location, GLsizei count, const GLdouble* value) {
Uniformv_State<3>(location, count, value);
}
void ProgramUniform3d(GLuint program, GLint location, GLdouble v0, GLdouble v1, GLdouble v2) {
const GLdouble v[] = {v0, v1, v2};
ProgramUniformv_State<3>(program, location, 1, v);
}
void ProgramUniform3dv(GLuint program, GLint location, GLsizei count, const GLdouble* value) {
ProgramUniformv_State<3>(program, location, count, value);
}
void Uniform4d(GLint location, GLdouble v0, GLdouble v1, GLdouble v2, GLdouble v3) {
const GLdouble v[] = {v0, v1, v2, v3};
Uniformv_State<4>(location, 1, v);
}
void Uniform4dv(GLint location, GLsizei count, const GLdouble* value) {
Uniformv_State<4>(location, count, value);
}
void ProgramUniform4d(GLuint program, GLint location, GLdouble v0, GLdouble v1, GLdouble v2, GLdouble v3) {
const GLdouble v[] = {v0, v1, v2, v3};
ProgramUniformv_State<4>(program, location, 1, v);
}
void ProgramUniform4dv(GLuint program, GLint location, GLsizei count, const GLdouble* value) {
ProgramUniformv_State<4>(program, location, count, value);
}
void UniformMatrix2dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
if (location == -1) return;
auto& programObject = MG_State::pGLContext->GetCurrentProgram();
if (programObject == nullptr) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "There is no current program object."));
return;
}
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 2, 2);
}
void ProgramUniformMatrix2dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLdouble* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program);
if (!programObject) return;
if (!programObject->GetLinkStatus()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"program " + std::to_string(program) + " is not linked."));
return;
}
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 2, 2);
}
void UniformMatrix3dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
if (location == -1) return;
auto& programObject = MG_State::pGLContext->GetCurrentProgram();
if (programObject == nullptr) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "There is no current program object."));
return;
}
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 3, 3);
}
void ProgramUniformMatrix3dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLdouble* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program);
if (!programObject) return;
if (!programObject->GetLinkStatus()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"program " + std::to_string(program) + " is not linked."));
return;
}
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 3, 3);
}
void UniformMatrix4dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
if (location == -1) return;
auto& programObject = MG_State::pGLContext->GetCurrentProgram();
if (programObject == nullptr) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "There is no current program object."));
return;
}
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 4, 4);
}
void ProgramUniformMatrix4dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLdouble* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program);
if (!programObject) return;
if (!programObject->GetLinkStatus()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"program " + std::to_string(program) + " is not linked."));
return;
}
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 4, 4);
}
void UniformMatrix2x3dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
if (location == -1) return;
auto& programObject = MG_State::pGLContext->GetCurrentProgram();
if (programObject == nullptr) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "There is no current program object."));
return;
}
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 2, 3);
}
void ProgramUniformMatrix2x3dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLdouble* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program);
if (!programObject) return;
if (!programObject->GetLinkStatus()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"program " + std::to_string(program) + " is not linked."));
return;
}
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 2, 3);
}
void UniformMatrix2x4dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
if (location == -1) return;
auto& programObject = MG_State::pGLContext->GetCurrentProgram();
if (programObject == nullptr) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "There is no current program object."));
return;
}
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 2, 4);
}
void ProgramUniformMatrix2x4dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLdouble* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program);
if (!programObject) return;
if (!programObject->GetLinkStatus()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"program " + std::to_string(program) + " is not linked."));
return;
}
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 2, 4);
}
void UniformMatrix3x2dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
if (location == -1) return;
auto& programObject = MG_State::pGLContext->GetCurrentProgram();
if (programObject == nullptr) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "There is no current program object."));
return;
}
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 3, 2);
}
void ProgramUniformMatrix3x2dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLdouble* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program);
if (!programObject) return;
if (!programObject->GetLinkStatus()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"program " + std::to_string(program) + " is not linked."));
return;
}
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 3, 2);
}
void UniformMatrix3x4dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
if (location == -1) return;
auto& programObject = MG_State::pGLContext->GetCurrentProgram();
if (programObject == nullptr) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "There is no current program object."));
return;
}
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 3, 4);
}
void ProgramUniformMatrix3x4dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLdouble* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program);
if (!programObject) return;
if (!programObject->GetLinkStatus()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"program " + std::to_string(program) + " is not linked."));
return;
}
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 3, 4);
}
void UniformMatrix4x2dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
if (location == -1) return;
auto& programObject = MG_State::pGLContext->GetCurrentProgram();
if (programObject == nullptr) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "There is no current program object."));
return;
}
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 4, 2);
}
void ProgramUniformMatrix4x2dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLdouble* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program);
if (!programObject) return;
if (!programObject->GetLinkStatus()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"program " + std::to_string(program) + " is not linked."));
return;
}
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 4, 2);
}
void UniformMatrix4x3dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
if (location == -1) return;
auto& programObject = MG_State::pGLContext->GetCurrentProgram();
if (programObject == nullptr) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "There is no current program object."));
return;
}
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 4, 3);
}
void ProgramUniformMatrix4x3dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
const GLdouble* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program);
if (!programObject) return;
if (!programObject->GetLinkStatus()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"program " + std::to_string(program) + " is not linked."));
return;
}
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 4, 3);
}
void GetUniformdv(GLuint program, GLint location, GLdouble* params) {
GetUniformdv_State(program, location, params);
}
void Uniform1fv(GLint location, GLsizei count, const GLfloat* value) {
Uniform1fv_State(location, count, value);
}
@@ -2101,6 +2570,17 @@ namespace MobileGL::MG_Impl::GLImpl {
"Backend does not support program interface queries."));
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);
}
@@ -2109,6 +2589,10 @@ namespace MobileGL::MG_Impl::GLImpl {
if (!programObject) return GL_INVALID_INDEX;
if (!ValidateNamedProgramResourceInterface(programInterface, __func__)) 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;
if (!getProgramResourceIndex) {
MG_State::pGLContext->RecordError(
@@ -2145,6 +2629,13 @@ namespace MobileGL::MG_Impl::GLImpl {
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "bufSize must be non-negative."));
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;
if (!getProgramResourceName) {
MG_State::pGLContext->RecordError(
@@ -2166,6 +2657,36 @@ namespace MobileGL::MG_Impl::GLImpl {
"propCount and bufSize must be non-negative."));
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;
if (!getProgramResourceiv) {
MG_State::pGLContext->RecordError(
@@ -2217,4 +2738,150 @@ namespace MobileGL::MG_Impl::GLImpl {
void ValidateProgram(GLuint program) {
ValidateProgram_State(program);
}
// ARB_get_program_binary with no supported binary format (GL_NUM_PROGRAM_BINARY_FORMATS
// is 0, which the extension explicitly allows). The three entry points below are what an
// application - and dEQP's function loader - reach through the extension; without it
// glProgramParameteri is not exposed in a 4.0 context at all.
void ProgramParameteri(GLuint program, GLenum pname, GLint value) {
auto& programObject = TryToGetProgramObject(program);
if (!programObject) return;
if (pname != GL_PROGRAM_BINARY_RETRIEVABLE_HINT) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "pname is not an accepted value."));
return;
}
if (value != GL_TRUE && value != GL_FALSE) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "value must be GL_TRUE or GL_FALSE."));
return;
}
programObject->SetBinaryRetrievableHint(value == GL_TRUE);
}
void GetProgramBinary(GLuint program, GLsizei bufSize, GLsizei* length, GLenum* binaryFormat, void* binary) {
(void)binaryFormat;
(void)binary;
auto& programObject = TryToGetProgramObject(program);
if (!programObject) return;
if (bufSize < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "bufSize must be non-negative."));
return;
}
if (length) *length = 0;
// GL_PROGRAM_BINARY_LENGTH is always zero here, which the spec makes an error to ask for.
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "The program has no retrievable binary."));
}
void ProgramBinary(GLuint program, GLenum binaryFormat, const void* binary, GLsizei length) {
(void)binaryFormat;
(void)binary;
auto& programObject = TryToGetProgramObject(program);
if (!programObject) return;
if (length < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "length must be non-negative."));
return;
}
// No format is supported, so every binary is rejected - and the program's link status
// has to read FALSE afterwards.
programObject->MarkLinkFailedByProgramBinary();
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "binaryFormat is not a supported format."));
}
void TransformFeedbackVaryings(GLuint program, GLsizei count, const GLchar* const* varyings, GLenum bufferMode) {
auto& programObject = TryToGetProgramObject(program);
if (!programObject) return;
if (bufferMode != GL_INTERLEAVED_ATTRIBS && bufferMode != GL_SEPARATE_ATTRIBS) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "bufferMode is not a valid capture mode."));
return;
}
if (count < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "count must be non-negative."));
return;
}
// GL 3.3 core: SEPARATE_ATTRIBS count may not exceed the separate-attrib limit.
if (bufferMode == GL_SEPARATE_ATTRIBS && count > 4) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"count exceeds GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS."));
return;
}
Vector<String> names;
names.reserve(static_cast<SizeT>(count));
for (GLsizei i = 0; i < count; ++i) {
names.emplace_back(varyings != nullptr && varyings[i] != nullptr ? varyings[i] : "");
}
// ARB_transform_feedback3's special names only mean anything in an interleaved
// capture, and gl_NextBuffer cannot advance past the last capture buffer.
constexpr Uint maxTransformFeedbackBuffers = 4;
Uint nextBufferCount = 0;
for (const String& name : names) {
const Bool isNextBuffer = name == "gl_NextBuffer";
const Bool isSkipComponents = name.size() == 18 && name.compare(0, 17, "gl_SkipComponents") == 0 &&
name[17] >= '1' && name[17] <= '4';
if (!isNextBuffer && !isSkipComponents) continue;
if (bufferMode != GL_INTERLEAVED_ATTRIBS) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"'" + name + "' requires GL_INTERLEAVED_ATTRIBS."));
return;
}
if (isNextBuffer && ++nextBufferCount >= maxTransformFeedbackBuffers) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"More gl_NextBuffer entries than "
"GL_MAX_TRANSFORM_FEEDBACK_BUFFERS allows."));
return;
}
}
programObject->SetTransformFeedbackVaryings(Move(names), bufferMode);
}
void GetTransformFeedbackVarying(GLuint program, GLuint index, GLsizei bufSize, GLsizei* length, GLsizei* size,
GLenum* type, GLchar* name) {
auto& programObject = TryToGetProgramObject(program);
if (!programObject) return;
if (!programObject->GetLinkStatus()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
std::to_string(program) + " has not been successfully linked."));
return;
}
const auto* varying = programObject->GetTransformFeedbackVarying(index);
if (varying == nullptr) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", __func__,
"index is not an active transform feedback varying of the program."));
return;
}
if (size != nullptr) *size = varying->size;
if (type != nullptr) *type = varying->type;
GLsizei written = 0;
if (name != nullptr && bufSize > 0) {
written = std::min<GLsizei>(bufSize - 1, static_cast<GLsizei>(varying->name.size()));
Memcpy(name, varying->name.data(), static_cast<SizeT>(written));
name[written] = '\0';
}
if (length != nullptr) *length = written;
}
} // namespace MobileGL::MG_Impl::GLImpl
@@ -137,5 +137,46 @@ namespace MobileGL::MG_Impl::GLImpl {
GLint GetProgramResourceLocation(GLuint program, GLenum programInterface, const GLchar* name);
GLint GetProgramResourceLocationIndex(GLuint program, GLenum programInterface, const GLchar* name);
void ShaderStorageBlockBinding(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding);
void Uniform1d(GLint location, GLdouble v0);
void Uniform1dv(GLint location, GLsizei count, const GLdouble* value);
void ProgramUniform1d(GLuint program, GLint location, GLdouble v0);
void ProgramUniform1dv(GLuint program, GLint location, GLsizei count, const GLdouble* value);
void Uniform2d(GLint location, GLdouble v0, GLdouble v1);
void Uniform2dv(GLint location, GLsizei count, const GLdouble* value);
void ProgramUniform2d(GLuint program, GLint location, GLdouble v0, GLdouble v1);
void ProgramUniform2dv(GLuint program, GLint location, GLsizei count, const GLdouble* value);
void Uniform3d(GLint location, GLdouble v0, GLdouble v1, GLdouble v2);
void Uniform3dv(GLint location, GLsizei count, const GLdouble* value);
void ProgramUniform3d(GLuint program, GLint location, GLdouble v0, GLdouble v1, GLdouble v2);
void ProgramUniform3dv(GLuint program, GLint location, GLsizei count, const GLdouble* value);
void Uniform4d(GLint location, GLdouble v0, GLdouble v1, GLdouble v2, GLdouble v3);
void Uniform4dv(GLint location, GLsizei count, const GLdouble* value);
void ProgramUniform4d(GLuint program, GLint location, GLdouble v0, GLdouble v1, GLdouble v2, GLdouble v3);
void ProgramUniform4dv(GLuint program, GLint location, GLsizei count, const GLdouble* value);
void UniformMatrix2dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
void ProgramUniformMatrix2dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
void UniformMatrix3dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
void ProgramUniformMatrix3dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
void UniformMatrix4dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
void ProgramUniformMatrix4dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
void UniformMatrix2x3dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
void ProgramUniformMatrix2x3dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
void UniformMatrix2x4dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
void ProgramUniformMatrix2x4dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
void UniformMatrix3x2dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
void ProgramUniformMatrix3x2dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
void UniformMatrix3x4dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
void ProgramUniformMatrix3x4dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
void UniformMatrix4x2dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
void ProgramUniformMatrix4x2dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
void UniformMatrix4x3dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
void ProgramUniformMatrix4x3dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
void GetUniformdv(GLuint program, GLint location, GLdouble* params);
void ValidateProgram(GLuint program);
void ProgramParameteri(GLuint program, GLenum pname, GLint value);
void GetProgramBinary(GLuint program, GLsizei bufSize, GLsizei* length, GLenum* binaryFormat, void* binary);
void ProgramBinary(GLuint program, GLenum binaryFormat, const void* binary, GLsizei length);
void TransformFeedbackVaryings(GLuint program, GLsizei count, const GLchar* const* varyings, GLenum bufferMode);
void GetTransformFeedbackVarying(GLuint program, GLuint index, GLsizei bufSize, GLsizei* length, GLsizei* size,
GLenum* type, GLchar* name);
} // namespace MobileGL::MG_Impl::GLImpl
@@ -0,0 +1,231 @@
// MobileGL - MobileGL/MG_Impl/GLImpl/Program/GL_ProgramPipeline.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 "GL_ProgramPipeline.h"
#include <MG_State/GLState/Core.h>
#include <MG_State/GLState/ErrorState/ErrorInfo.h>
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
namespace MobileGL::MG_Impl::GLImpl {
namespace {
void RecordPipelineError(ErrorCode code, const char* function, String message) {
MG_State::pGLContext->RecordError(
code, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", function, Move(message)));
}
// A pipeline name only names an object once it has been bound or created; querying a
// reserved-but-unmaterialised name is INVALID_OPERATION (GL 4.6 core 7.4).
const SharedPtr<MG_State::GLState::ProgramPipelineObject>* TryGetPipeline(GLuint pipeline,
const char* function) {
if (!MG_State::pGLContext->IsProgramPipelineObject(pipeline)) {
RecordPipelineError(ErrorCode::InvalidOperation, function,
std::format("Program pipeline {} does not exist.", pipeline));
return nullptr;
}
return &MG_State::pGLContext->GetProgramPipelineObject(pipeline);
}
Bool ValidatePipelineCount(GLsizei n, const char* function) {
if (n < 0) {
RecordPipelineError(ErrorCode::InvalidValue, function, "n must be non-negative.");
return false;
}
return true;
}
// GL 4.6 core table 7.1 maps each stage bit onto a shader stage.
Bool TryResolveStageBit(GLbitfield bit, ShaderStage& outStage) {
switch (bit) {
case GL_VERTEX_SHADER_BIT: outStage = ShaderStage::Vertex; return true;
case GL_TESS_CONTROL_SHADER_BIT: outStage = ShaderStage::TessControl; return true;
case GL_TESS_EVALUATION_SHADER_BIT: outStage = ShaderStage::TessEval; return true;
case GL_GEOMETRY_SHADER_BIT: outStage = ShaderStage::Geometry; return true;
case GL_FRAGMENT_SHADER_BIT: outStage = ShaderStage::Fragment; return true;
case GL_COMPUTE_SHADER_BIT: outStage = ShaderStage::Compute; return true;
default: return false;
}
}
constexpr GLbitfield kAllStageBits = GL_VERTEX_SHADER_BIT | GL_TESS_CONTROL_SHADER_BIT |
GL_TESS_EVALUATION_SHADER_BIT | GL_GEOMETRY_SHADER_BIT |
GL_FRAGMENT_SHADER_BIT | GL_COMPUTE_SHADER_BIT;
} // namespace
void GenProgramPipelines(GLsizei n, GLuint* pipelines) {
if (!ValidatePipelineCount(n, __func__)) return;
if (n == 0 || !pipelines) return;
static thread_local Vector<GLuint> names;
MG_State::pGLContext->GenProgramPipelineNames(static_cast<Uint>(n), names);
Memcpy(pipelines, names.data(), static_cast<SizeT>(n) * sizeof(GLuint));
}
void CreateProgramPipelines(GLsizei n, GLuint* pipelines) {
if (!ValidatePipelineCount(n, __func__)) return;
if (n == 0 || !pipelines) return;
static thread_local Vector<GLuint> names;
MG_State::pGLContext->GenProgramPipelineNames(static_cast<Uint>(n), names);
for (GLsizei i = 0; i < n; ++i) {
pipelines[i] = names[static_cast<SizeT>(i)];
MG_State::pGLContext->CreateProgramPipelineObject(names[static_cast<SizeT>(i)]);
}
}
void DeleteProgramPipelines(GLsizei n, const GLuint* pipelines) {
if (!ValidatePipelineCount(n, __func__)) return;
if (!pipelines) return;
for (GLsizei i = 0; i < n; ++i) {
// Deleting zero, an unknown name, or a name that was only reserved is silently ignored.
MG_State::pGLContext->MarkProgramPipelineForDeletion(pipelines[i]);
}
}
void BindProgramPipeline(GLuint pipeline) {
if (pipeline != 0 && !MG_State::pGLContext->ValidateProgramPipelineName(pipeline)) {
RecordPipelineError(ErrorCode::InvalidOperation, __func__,
std::format("Program pipeline name {} is not valid.", pipeline));
return;
}
MG_State::pGLContext->BindProgramPipelineObject(pipeline);
}
GLboolean IsProgramPipeline(GLuint pipeline) {
return MG_State::pGLContext->IsProgramPipelineObject(pipeline) ? GL_TRUE : GL_FALSE;
}
void GetProgramPipelineiv(GLuint pipeline, GLenum pname, GLint* params) {
const auto* pipelineObject = TryGetPipeline(pipeline, __func__);
if (!pipelineObject || !params) return;
const auto stageProgramName = [&](ShaderStage stage) -> GLint {
const auto& program = (*pipelineObject)->GetStageProgram(stage);
return program ? static_cast<GLint>(program->GetExternalIndex()) : 0;
};
switch (pname) {
case GL_ACTIVE_PROGRAM: {
const auto& active = (*pipelineObject)->GetActiveProgram();
*params = active ? static_cast<GLint>(active->GetExternalIndex()) : 0;
break;
}
case GL_VERTEX_SHADER: *params = stageProgramName(ShaderStage::Vertex); break;
case GL_TESS_CONTROL_SHADER: *params = stageProgramName(ShaderStage::TessControl); break;
case GL_TESS_EVALUATION_SHADER: *params = stageProgramName(ShaderStage::TessEval); break;
case GL_GEOMETRY_SHADER: *params = stageProgramName(ShaderStage::Geometry); break;
case GL_FRAGMENT_SHADER: *params = stageProgramName(ShaderStage::Fragment); break;
case GL_COMPUTE_SHADER: *params = stageProgramName(ShaderStage::Compute); break;
case GL_VALIDATE_STATUS: *params = (*pipelineObject)->GetValidateStatus() ? GL_TRUE : GL_FALSE; break;
case GL_INFO_LOG_LENGTH: {
// GL counts the null terminator, and reports 0 rather than 1 for an empty log.
const auto& log = (*pipelineObject)->GetInfoLog();
*params = log.empty() ? 0 : static_cast<GLint>(log.length()) + 1;
break;
}
default:
RecordPipelineError(ErrorCode::InvalidEnum, __func__,
std::format("pname {} is not a program pipeline parameter.",
MG_Util::ConvertGLEnumToString(pname)));
break;
}
}
void GetProgramPipelineInfoLog(GLuint pipeline, GLsizei bufSize, GLsizei* length, GLchar* infoLog) {
const auto* pipelineObject = TryGetPipeline(pipeline, __func__);
if (!pipelineObject) return;
if (bufSize < 0) {
RecordPipelineError(ErrorCode::InvalidValue, __func__, "bufSize must be non-negative.");
return;
}
if (bufSize == 0 || !infoLog) {
if (length) *length = 0;
return;
}
const auto& log = (*pipelineObject)->GetInfoLog();
const auto copied = std::min<GLsizei>(bufSize - 1, static_cast<GLsizei>(log.length()));
if (copied > 0) Memcpy(infoLog, log.data(), static_cast<SizeT>(copied));
infoLog[copied] = '\0';
if (length) *length = copied;
}
void UseProgramStages(GLuint pipeline, GLbitfield stages, GLuint program) {
if (stages != GL_ALL_SHADER_BITS && (stages & ~kAllStageBits) != 0) {
RecordPipelineError(ErrorCode::InvalidValue, __func__, "stages names a bit that is not a shader stage.");
return;
}
const auto* pipelineObject = TryGetPipeline(pipeline, __func__);
if (!pipelineObject) return;
SharedPtr<MG_State::GLState::ProgramObject> programObject;
if (program != 0) {
if (!MG_State::pGLContext->ValidateProgramName(program)) {
RecordPipelineError(ErrorCode::InvalidValue, __func__,
std::format("{} is not the name of a program object.", program));
return;
}
programObject = MG_State::pGLContext->GetProgramObject(program);
if (!programObject) {
RecordPipelineError(ErrorCode::InvalidValue, __func__,
std::format("{} is not the name of a program object.", program));
return;
}
if (!programObject->GetLinkStatus()) {
RecordPipelineError(ErrorCode::InvalidOperation, __func__,
std::format("Program {} has not been linked successfully.", program));
return;
}
}
const GLbitfield selected = stages == GL_ALL_SHADER_BITS ? kAllStageBits : stages;
for (GLbitfield bit = 1; bit != 0 && bit <= kAllStageBits; bit <<= 1) {
if ((selected & bit) == 0) continue;
ShaderStage stage = ShaderStage::Unknown;
if (!TryResolveStageBit(bit, stage)) continue;
// program == 0 clears the stage, which is what a null program reference means here.
(*pipelineObject)->SetStageProgram(stage, programObject);
}
}
void ActiveShaderProgram(GLuint pipeline, GLuint program) {
const auto* pipelineObject = TryGetPipeline(pipeline, __func__);
if (!pipelineObject) return;
if (program == 0) {
(*pipelineObject)->SetActiveProgram(nullptr);
return;
}
if (!MG_State::pGLContext->ValidateProgramName(program)) {
RecordPipelineError(ErrorCode::InvalidValue, __func__,
std::format("{} is not the name of a program object.", program));
return;
}
auto programObject = MG_State::pGLContext->GetProgramObject(program);
if (!programObject) {
RecordPipelineError(ErrorCode::InvalidValue, __func__,
std::format("{} is not the name of a program object.", program));
return;
}
if (!programObject->GetLinkStatus()) {
RecordPipelineError(ErrorCode::InvalidOperation, __func__,
std::format("Program {} has not been linked successfully.", program));
return;
}
(*pipelineObject)->SetActiveProgram(programObject);
}
void ValidateProgramPipeline(GLuint pipeline) {
const auto* pipelineObject = TryGetPipeline(pipeline, __func__);
if (!pipelineObject) return;
// Nothing here can fail today: MobileGL links each stage program on its own, so there is no
// cross-stage interface to re-check at validation time. The log stays empty, which GL allows.
(*pipelineObject)->SetValidateStatus(true);
}
} // namespace MobileGL::MG_Impl::GLImpl
@@ -0,0 +1,23 @@
// MobileGL - MobileGL/MG_Impl/GLImpl/Program/GL_ProgramPipeline.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 <Includes.h>
namespace MobileGL::MG_Impl::GLImpl {
void GenProgramPipelines(GLsizei n, GLuint* pipelines);
void CreateProgramPipelines(GLsizei n, GLuint* pipelines);
void DeleteProgramPipelines(GLsizei n, const GLuint* pipelines);
void BindProgramPipeline(GLuint pipeline);
GLboolean IsProgramPipeline(GLuint pipeline);
void GetProgramPipelineiv(GLuint pipeline, GLenum pname, GLint* params);
void GetProgramPipelineInfoLog(GLuint pipeline, GLsizei bufSize, GLsizei* length, GLchar* infoLog);
void UseProgramStages(GLuint pipeline, GLbitfield stages, GLuint program);
void ActiveShaderProgram(GLuint pipeline, GLuint program);
void ValidateProgramPipeline(GLuint pipeline);
} // namespace MobileGL::MG_Impl::GLImpl
+318 -29
View File
@@ -7,6 +7,7 @@
// End of Source File Header
#include "GL_Query.h"
#include "../Getter/GL_Getter.h"
#include <Config.h>
#include <MG_Backend/BackendObjects.h>
#include <MG_State/GLState/Core.h>
@@ -22,11 +23,16 @@ namespace MobileGL::MG_Impl::GLImpl {
struct QueryObject {
GLuint id = 0;
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;
Bool active = false;
Bool ended = false;
Bool resultCached = false;
Uint64 cachedResult = 0;
// Transform feedback primitive counter at BeginQuery time.
Uint64 counterSnapshot = 0;
};
// Query calls may arrive from any thread (launchers migrate the context
@@ -41,6 +47,11 @@ namespace MobileGL::MG_Impl::GLImpl {
GLuint g_nextQueryId = 1;
// Id of the query currently active on GL_TIME_ELAPSED (0 = none).
GLuint g_activeTimeElapsedQueryId = 0;
// Ids of the queries active on the transform feedback targets (0 = none).
GLuint g_activePrimitivesWrittenQueryId = 0;
GLuint g_activePrimitivesGeneratedQueryId = 0;
// Id of the query active on GL_SAMPLES_PASSED (0 = none).
GLuint g_activeSamplesPassedQueryId = 0;
Bool TimerQueryDisabled() {
return MG_Config::Features.DisableTimerQuery;
@@ -51,6 +62,34 @@ namespace MobileGL::MG_Impl::GLImpl {
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", function, message));
}
// The by-buffer query getters write the result into a buffer object instead of client
// memory. Everything about the query itself - the name, whether it is still active, the
// parameter - is checked by GetQueryObjectValue; what is left is the destination, so this
// resolves the buffer and confirms the write lands inside it (GL 4.6 core 4.2.1).
Bool ResolveQueryResultDestination(GLuint buffer, GLintptr offset, SizeT writeSize, const char* function,
SharedPtr<MG_State::GLState::BufferObject>& outBuffer) {
if (offset < 0) {
RecordQueryError(ErrorCode::InvalidValue, function, "Offset cannot be negative.");
return false;
}
if (!MG_State::pGLContext->ValidateBufferObject(buffer)) {
RecordQueryError(ErrorCode::InvalidOperation, function, "Buffer object does not exist.");
return false;
}
auto bufferObject = MG_State::pGLContext->GetBufferObject(buffer);
if (!bufferObject) {
RecordQueryError(ErrorCode::InvalidOperation, function, "Buffer object does not exist.");
return false;
}
if (static_cast<SizeT>(offset) + writeSize > bufferObject->GetSize()) {
RecordQueryError(ErrorCode::InvalidOperation, function,
"The query result does not fit in the buffer object at this offset.");
return false;
}
outBuffer = bufferObject;
return true;
}
// Callers must hold g_queryObjectsMutex.
QueryObject* FindQueryObjectLocked(GLuint id) {
const auto it = g_liveQueryObjects.find(id);
@@ -84,8 +123,13 @@ namespace MobileGL::MG_Impl::GLImpl {
}
// Shared GetQueryObject* implementation. Returns false when an error
// was recorded and no value should be written back.
Bool GetQueryObjectValue(GLuint id, GLenum pname, const char* function, Uint64& outValue) {
// was recorded and no value should be written back. `outValueProduced`, when given,
// additionally distinguishes "succeeded with a value" from "succeeded but the result is not
// ready" - the GL_QUERY_RESULT_NO_WAIT case, where GL_ARB_query_buffer_object says the
// destination is left alone rather than written with a placeholder.
Bool GetQueryObjectValue(GLuint id, GLenum pname, const char* function, Uint64& outValue,
Bool* outValueProduced = nullptr) {
if (outValueProduced) *outValueProduced = true;
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
auto* queryObject = FindQueryObjectLocked(id);
if (!queryObject) {
@@ -98,6 +142,41 @@ namespace MobileGL::MG_Impl::GLImpl {
}
switch (pname) {
case GL_QUERY_TARGET:
// The target a query was begun with (or created with, for glCreateQueries) - state
// the object has carried all along, GL 4.6 core table 23.35.
outValue = queryObject->target;
return true;
case GL_QUERY_RESULT_NO_WAIT: {
if (queryObject->resultCached) {
outValue = queryObject->cachedResult;
return true;
}
Uint64 result = 0;
const auto getQueryResult64 = MG_Backend::gBackendFunctionsTable.GL.GetQueryResult64;
if (queryObject->backendHandle && getQueryResult64 &&
!getQueryResult64(queryObject->backendHandle, /*wait=*/false, &result)) {
// Not ready. The whole point of the no-wait form is that the caller's
// destination keeps whatever it already held.
if (outValueProduced) *outValueProduced = false;
outValue = 0;
return true;
}
if (queryObject->target == GL_ANY_SAMPLES_PASSED ||
queryObject->target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) {
result = result != 0 ? 1 : 0;
}
if (queryObject->backendHandle) {
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
deleteBackendQuery(queryObject->backendHandle);
}
queryObject->backendHandle = nullptr;
}
queryObject->cachedResult = result;
queryObject->resultCached = true;
outValue = result;
return true;
}
case GL_QUERY_RESULT_AVAILABLE: {
if (queryObject->resultCached || !queryObject->backendHandle) {
outValue = 1;
@@ -126,6 +205,11 @@ namespace MobileGL::MG_Impl::GLImpl {
outValue = 0;
return true;
}
// ANY_SAMPLES_PASSED* report a boolean.
if (queryObject->target == GL_ANY_SAMPLES_PASSED ||
queryObject->target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) {
result = result != 0 ? 1 : 0;
}
// Final value produced (or no GetQueryResult64 hook: the
// query degrades to a zero result); the backend handle is
// consumed and the value cached for later reads.
@@ -144,6 +228,21 @@ namespace MobileGL::MG_Impl::GLImpl {
return false;
}
}
template <typename T>
void GetQueryBufferObject(GLuint id, GLuint buffer, GLenum pname, GLintptr offset, const char* function) {
SharedPtr<MG_State::GLState::BufferObject> bufferObject;
if (!ResolveQueryResultDestination(buffer, offset, sizeof(T), function, bufferObject)) return;
Uint64 value = 0;
Bool valueProduced = false;
if (!GetQueryObjectValue(id, pname, function, value, &valueProduced)) return;
// GL_QUERY_RESULT_NO_WAIT on a result that has not landed writes nothing at all.
if (!valueProduced) return;
const T narrowed = static_cast<T>(value);
bufferObject->UploadSubData({const_cast<T*>(&narrowed), sizeof(T)}, static_cast<SizeT>(offset));
}
} // namespace
void GenQueries(GLsizei n, GLuint* ids) {
@@ -164,6 +263,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) {
if (n < 0) {
RecordQueryError(ErrorCode::InvalidValue, __FUNCTION__, "n cannot be negative.");
@@ -180,7 +314,24 @@ namespace MobileGL::MG_Impl::GLImpl {
}
QueryObject* queryObject = it->second;
if (queryObject->active) {
EndTimeElapsedQueryLocked(queryObject); // implicitly end before deletion
// Implicitly end before deletion, releasing the matching active slot.
if (queryObject->target == GL_SAMPLES_PASSED || queryObject->target == GL_ANY_SAMPLES_PASSED ||
queryObject->target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) {
if (const auto endOcclusionQuery = MG_Backend::gBackendFunctionsTable.GL.EndOcclusionQuery;
endOcclusionQuery && queryObject->backendHandle) {
endOcclusionQuery(queryObject->backendHandle);
}
queryObject->active = false;
g_activeSamplesPassedQueryId = 0;
} else if (queryObject->target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ||
queryObject->target == GL_PRIMITIVES_GENERATED) {
queryObject->active = false;
(queryObject->target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN
? g_activePrimitivesWrittenQueryId
: g_activePrimitivesGeneratedQueryId) = 0;
} else {
EndTimeElapsedQueryLocked(queryObject);
}
}
if (queryObject->backendHandle) {
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
@@ -198,16 +349,23 @@ namespace MobileGL::MG_Impl::GLImpl {
return GL_FALSE;
}
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
// Gen'd ids count as query objects here: the registry creates live
// objects at GenQueries time.
return FindQueryObjectLocked(id) != nullptr ? GL_TRUE : GL_FALSE;
// A name from glGenQueries is not yet a query object: it becomes one when it is first
// used with BeginQuery/QueryCounter (which is what a non-zero target records), or
// 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) {
if (target != GL_TIME_ELAPSED) {
// Only GL_TIME_ELAPSED timer queries are implemented (occlusion and
// primitive queries remain stubs); GL_TIMESTAMP is not a valid
// BeginQuery target either.
const Bool isTransformFeedbackQuery =
target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN || target == GL_PRIMITIVES_GENERATED;
const Bool isOcclusionQuery =
(target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED ||
target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) &&
MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr;
if (target != GL_TIME_ELAPSED && !isTransformFeedbackQuery && !isOcclusionQuery) {
// GL_TIMESTAMP is not a valid BeginQuery target; the occlusion targets
// need backend support.
RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "Query target is not supported.");
return;
}
@@ -221,9 +379,13 @@ namespace MobileGL::MG_Impl::GLImpl {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "Query object does not exist.");
return;
}
if (g_activeTimeElapsedQueryId != 0) {
GLuint& activeQueryId = isTransformFeedbackQuery
? (target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ? g_activePrimitivesWrittenQueryId
: g_activePrimitivesGeneratedQueryId)
: (isOcclusionQuery ? g_activeSamplesPassedQueryId : g_activeTimeElapsedQueryId);
if (activeQueryId != 0) {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__,
"A query is already active on GL_TIME_ELAPSED.");
"A query is already active on this target.");
return;
}
if (queryObject->active) {
@@ -239,25 +401,72 @@ namespace MobileGL::MG_Impl::GLImpl {
ResetQueryObjectLocked(queryObject); // discard any previous result
queryObject->target = target;
queryObject->active = true;
const auto beginTimeElapsedQuery = MG_Backend::gBackendFunctionsTable.GL.BeginTimeElapsedQuery;
queryObject->backendHandle =
(!TimerQueryDisabled() && beginTimeElapsedQuery) ? beginTimeElapsedQuery() : nullptr;
g_activeTimeElapsedQueryId = id;
if (isTransformFeedbackQuery) {
// Prefer real GPU transform-feedback queries (exact with geometry shaders);
// the CPU accounting delta stays as the fallback when the backend lacks them.
const auto beginXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.BeginXfbPrimitivesQuery;
queryObject->backendHandle =
beginXfbPrimitivesQuery ? beginXfbPrimitivesQuery(target == GL_PRIMITIVES_GENERATED) : nullptr;
queryObject->counterSnapshot = MG_State::pGLContext->GetTransformFeedbackPrimitiveCounter();
} else if (isOcclusionQuery) {
queryObject->backendHandle = MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery();
} else {
const auto beginTimeElapsedQuery = MG_Backend::gBackendFunctionsTable.GL.BeginTimeElapsedQuery;
queryObject->backendHandle =
(!TimerQueryDisabled() && beginTimeElapsedQuery) ? beginTimeElapsedQuery() : nullptr;
}
activeQueryId = id;
}
void EndQuery(GLenum target) {
if (target != GL_TIME_ELAPSED) {
const Bool isTransformFeedbackQuery =
target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN || target == GL_PRIMITIVES_GENERATED;
const Bool isOcclusionQuery =
(target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED ||
target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) &&
MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr;
if (target != GL_TIME_ELAPSED && !isTransformFeedbackQuery && !isOcclusionQuery) {
RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "Query target is not supported.");
return;
}
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
if (g_activeTimeElapsedQueryId == 0) {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "No query is active on GL_TIME_ELAPSED.");
GLuint& activeQueryId = isTransformFeedbackQuery
? (target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ? g_activePrimitivesWrittenQueryId
: g_activePrimitivesGeneratedQueryId)
: (isOcclusionQuery ? g_activeSamplesPassedQueryId : g_activeTimeElapsedQueryId);
if (activeQueryId == 0) {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "No query is active on this target.");
return;
}
auto* queryObject = FindQueryObjectLocked(g_activeTimeElapsedQueryId);
auto* queryObject = FindQueryObjectLocked(activeQueryId);
if (!queryObject) {
g_activeTimeElapsedQueryId = 0; // should not happen; keep state consistent
activeQueryId = 0; // should not happen; keep state consistent
return;
}
if (isTransformFeedbackQuery) {
if (queryObject->backendHandle) {
if (const auto endXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.EndXfbPrimitivesQuery) {
endXfbPrimitivesQuery(queryObject->backendHandle);
}
// Result comes from the GPU query at read time.
} else {
queryObject->cachedResult =
MG_State::pGLContext->GetTransformFeedbackPrimitiveCounter() - queryObject->counterSnapshot;
queryObject->resultCached = true;
}
queryObject->active = false;
queryObject->ended = true;
activeQueryId = 0;
return;
}
if (isOcclusionQuery) {
if (const auto endOcclusionQuery = MG_Backend::gBackendFunctionsTable.GL.EndOcclusionQuery;
endOcclusionQuery && queryObject->backendHandle) {
endOcclusionQuery(queryObject->backendHandle);
}
queryObject->active = false;
queryObject->ended = true;
activeQueryId = 0;
return;
}
EndTimeElapsedQueryLocked(queryObject);
@@ -303,9 +512,25 @@ namespace MobileGL::MG_Impl::GLImpl {
switch (pname) {
case GL_CURRENT_QUERY: {
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
// Only GL_TIME_ELAPSED queries can be active; GL_TIMESTAMP queries
// never are, and other targets remain unimplemented.
*params = target == GL_TIME_ELAPSED ? static_cast<GLint>(g_activeTimeElapsedQueryId) : 0;
switch (target) {
case GL_TIME_ELAPSED:
*params = static_cast<GLint>(g_activeTimeElapsedQueryId);
break;
case GL_SAMPLES_PASSED:
case GL_ANY_SAMPLES_PASSED:
case GL_ANY_SAMPLES_PASSED_CONSERVATIVE:
*params = static_cast<GLint>(g_activeSamplesPassedQueryId);
break;
case GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN:
*params = static_cast<GLint>(g_activePrimitivesWrittenQueryId);
break;
case GL_PRIMITIVES_GENERATED:
*params = static_cast<GLint>(g_activePrimitivesGeneratedQueryId);
break;
default:
*params = 0;
break;
}
return;
}
case GL_QUERY_COUNTER_BITS: {
@@ -313,7 +538,13 @@ namespace MobileGL::MG_Impl::GLImpl {
// time: IsTimerQuerySupported is the dynamic truth (extension /
// entry points / timestamp valid bits at call time, not at table
// init), and the MOBILEGL_DISABLE_TIMERQUERY kill switch always
// wins. Non-timer targets remain unimplemented and report 0.
// wins.
if (target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED ||
target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) {
const Bool occlusionSupported = MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr;
*params = occlusionSupported ? (target == GL_SAMPLES_PASSED ? 32 : 1) : 0;
return;
}
const Bool timerTarget = target == GL_TIME_ELAPSED || target == GL_TIMESTAMP;
const auto isTimerQuerySupported = MG_Backend::gBackendFunctionsTable.GL.IsTimerQuerySupported;
const Bool supported =
@@ -327,9 +558,26 @@ namespace MobileGL::MG_Impl::GLImpl {
}
}
void GetQueryBufferObjectiv(GLuint id, GLuint buffer, GLenum pname, GLintptr offset) {
GetQueryBufferObject<GLint>(id, buffer, pname, offset, __FUNCTION__);
}
void GetQueryBufferObjectuiv(GLuint id, GLuint buffer, GLenum pname, GLintptr offset) {
GetQueryBufferObject<GLuint>(id, buffer, pname, offset, __FUNCTION__);
}
void GetQueryBufferObjecti64v(GLuint id, GLuint buffer, GLenum pname, GLintptr offset) {
GetQueryBufferObject<GLint64>(id, buffer, pname, offset, __FUNCTION__);
}
void GetQueryBufferObjectui64v(GLuint id, GLuint buffer, GLenum pname, GLintptr offset) {
GetQueryBufferObject<GLuint64>(id, buffer, pname, offset, __FUNCTION__);
}
void GetQueryObjectiv(GLuint id, GLenum pname, GLint* params) {
Uint64 value = 0;
if (!GetQueryObjectValue(id, pname, __FUNCTION__, value) || !params) {
Bool valueProduced = false;
if (!GetQueryObjectValue(id, pname, __FUNCTION__, value, &valueProduced) || !valueProduced || !params) {
return;
}
constexpr Uint64 kMaxInt = static_cast<Uint64>(INT_MAX);
@@ -338,7 +586,8 @@ namespace MobileGL::MG_Impl::GLImpl {
void GetQueryObjectuiv(GLuint id, GLenum pname, GLuint* params) {
Uint64 value = 0;
if (!GetQueryObjectValue(id, pname, __FUNCTION__, value) || !params) {
Bool valueProduced = false;
if (!GetQueryObjectValue(id, pname, __FUNCTION__, value, &valueProduced) || !valueProduced || !params) {
return;
}
*params = static_cast<GLuint>(value & 0xFFFFFFFFull);
@@ -346,7 +595,8 @@ namespace MobileGL::MG_Impl::GLImpl {
void GetQueryObjecti64v(GLuint id, GLenum pname, GLint64* params) {
Uint64 value = 0;
if (!GetQueryObjectValue(id, pname, __FUNCTION__, value) || !params) {
Bool valueProduced = false;
if (!GetQueryObjectValue(id, pname, __FUNCTION__, value, &valueProduced) || !valueProduced || !params) {
return;
}
*params = static_cast<GLint64>(value);
@@ -354,9 +604,48 @@ namespace MobileGL::MG_Impl::GLImpl {
void GetQueryObjectui64v(GLuint id, GLenum pname, GLuint64* params) {
Uint64 value = 0;
if (!GetQueryObjectValue(id, pname, __FUNCTION__, value) || !params) {
Bool valueProduced = false;
if (!GetQueryObjectValue(id, pname, __FUNCTION__, value, &valueProduced) || !valueProduced || !params) {
return;
}
*params = static_cast<GLuint64>(value);
}
namespace {
// The indexed query entry points differ from the plain ones only in the vertex
// stream they address (GL 4.6 core 4.2.1): index must be below GL_MAX_VERTEX_STREAMS
// for the two transform feedback targets and zero for every other target. With a
// single vertex stream both bounds are 1, so a valid call is always index 0 and
// forwards to the unindexed implementation.
Bool ValidateQueryStreamIndex(const char* function, GLenum target, GLuint index) {
const Bool perStreamTarget =
target == GL_PRIMITIVES_GENERATED || target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN;
GLint maxVertexStreams = 1;
if (perStreamTarget) {
GetIntegerv(GL_MAX_VERTEX_STREAMS, &maxVertexStreams);
}
if (index < static_cast<GLuint>(std::max(maxVertexStreams, 1))) {
return true;
}
RecordQueryError(ErrorCode::InvalidValue, function,
perStreamTarget ? "index is not less than GL_MAX_VERTEX_STREAMS."
: "index must be zero for this query target.");
return false;
}
} // namespace
void BeginQueryIndexed(GLenum target, GLuint index, GLuint id) {
if (!ValidateQueryStreamIndex(__FUNCTION__, target, index)) return;
BeginQuery(target, id);
}
void EndQueryIndexed(GLenum target, GLuint index) {
if (!ValidateQueryStreamIndex(__FUNCTION__, target, index)) return;
EndQuery(target);
}
void GetQueryIndexediv(GLenum target, GLuint index, GLenum pname, GLint* params) {
if (!ValidateQueryStreamIndex(__FUNCTION__, target, index)) return;
GetQueryiv(target, pname, params);
}
} // namespace MobileGL::MG_Impl::GLImpl
+8
View File
@@ -11,14 +11,22 @@
namespace MobileGL::MG_Impl::GLImpl {
void GenQueries(GLsizei n, GLuint* ids);
void CreateQueries(GLenum target, GLsizei n, GLuint* ids);
void DeleteQueries(GLsizei n, const GLuint* ids);
GLboolean IsQuery(GLuint id);
void BeginQuery(GLenum target, GLuint id);
void EndQuery(GLenum target);
void GetQueryiv(GLenum target, GLenum pname, GLint* params);
void BeginQueryIndexed(GLenum target, GLuint index, GLuint id);
void EndQueryIndexed(GLenum target, GLuint index);
void GetQueryIndexediv(GLenum target, GLuint index, GLenum pname, GLint* params);
void GetQueryObjectiv(GLuint id, GLenum pname, GLint* params);
void GetQueryObjectuiv(GLuint id, GLenum pname, GLuint* params);
void GetQueryObjecti64v(GLuint id, GLenum pname, GLint64* params);
void GetQueryObjectui64v(GLuint id, GLenum pname, GLuint64* params);
void GetQueryBufferObjectiv(GLuint id, GLuint buffer, GLenum pname, GLintptr offset);
void GetQueryBufferObjectuiv(GLuint id, GLuint buffer, GLenum pname, GLintptr offset);
void GetQueryBufferObjecti64v(GLuint id, GLuint buffer, GLenum pname, GLintptr offset);
void GetQueryBufferObjectui64v(GLuint id, GLuint buffer, GLenum pname, GLintptr offset);
void QueryCounter(GLuint id, GLenum target);
} // namespace MobileGL::MG_Impl::GLImpl
@@ -28,6 +28,11 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_TEXTURE_MAX_LOD:
case GL_TEXTURE_LOD_BIAS:
return true;
// Four components, and GL puts no range on them - a border colour outside [0,1] is
// clamped when a fixed-point format is sampled, not rejected here. The scalar readers
// below would look at one component and invent an error.
case GL_TEXTURE_BORDER_COLOR:
return true;
case GL_TEXTURE_MAX_ANISOTROPY_EXT:
if (ReadSamplerScalar(param, isFloat, isUnsignedInteger) >= 1.0f) return true;
MG_State::pGLContext->RecordError(
@@ -99,6 +104,20 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_TEXTURE_COMPARE_FUNC:
samplerObj->SetSamplerCompareFunc(MG_Util::ConvertGLEnumToSamplerCompareFunc(*(const GLint*)param));
break;
case GL_TEXTURE_BORDER_COLOR:
// The only four-component sampler parameter: the caller's form decides which
// representation is authoritative, and SamplerObject keeps the other two in step.
if (isFloat) {
const auto* values = (const GLfloat*)param;
samplerObj->SetBorderColor(FloatVec4(values[0], values[1], values[2], values[3]));
} else if (isUnsignedInteger) {
const auto* values = (const GLuint*)param;
samplerObj->SetBorderColorUI(UintVec4(values[0], values[1], values[2], values[3]));
} else {
const auto* values = (const GLint*)param;
samplerObj->SetBorderColorI(IntVec4(values[0], values[1], values[2], values[3]));
}
break;
default:
MG_State::pGLContext->RecordError(ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "SetSamplerParam_State",
@@ -162,6 +181,31 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_TEXTURE_COMPARE_FUNC:
*(GLuint*)params = MG_Util::ConvertSamplerCompareFuncToGLEnum(samplerObj->GetSamplerCompareFunc());
break;
case GL_TEXTURE_BORDER_COLOR: {
if (isFloat) {
const auto& color = samplerObj->GetBorderColor();
auto* out = (GLfloat*)params;
out[0] = color.x();
out[1] = color.y();
out[2] = color.z();
out[3] = color.w();
} else if (isUnsignedInteger) {
const auto& color = samplerObj->GetBorderColorUI();
auto* out = (GLuint*)params;
out[0] = color.x();
out[1] = color.y();
out[2] = color.z();
out[3] = color.w();
} else {
const auto& color = samplerObj->GetBorderColorI();
auto* out = (GLint*)params;
out[0] = color.x();
out[1] = color.y();
out[2] = color.z();
out[3] = color.w();
}
break;
}
default:
MG_State::pGLContext->RecordError(ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetSamplerParam_State",
@@ -185,6 +229,11 @@ namespace MobileGL::MG_Impl::GLImpl {
static thread_local Vector<GLuint> names;
MG_State::pGLContext->GenSamplerNames(count, names);
Memcpy(samplers, names.data(), count * sizeof(GLuint));
// Unlike textures/buffers, glGenSamplers CREATES the sampler objects: each name
// is immediately a sampler (glIsSampler == GL_TRUE before any bind).
for (GLsizei i = 0; i < count; ++i) {
MG_State::pGLContext->CreateSamplerObject(names[i]);
}
}
void DeleteSamplers_State(GLsizei count, const GLuint* samplers) {
File diff suppressed because it is too large Load Diff
@@ -11,6 +11,10 @@
namespace MobileGL::MG_Impl::GLImpl {
/* @INSERTION_POINT:FUNCTION_DECLARATION@ */
// The sized internal formats a buffer texture accepts (GL 4.6 core table 8.16). The buffer
// clears take the same list, so it is shared rather than written out twice.
Bool IsBufferTextureInternalFormat(GLenum internalformat);
void ClearTexImage(GLuint texture, GLint level, GLenum format, GLenum type, const void* data);
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);
@@ -42,6 +46,10 @@ namespace MobileGL::MG_Impl::GLImpl {
void GenerateTextureMipmap(GLuint texture);
void BindTextureUnit(GLuint unit, GLuint texture);
void GetTextureImage(GLuint texture, GLint level, GLenum format, GLenum type, GLsizei bufSize, void* pixels);
void GetCompressedTextureImage(GLuint texture, GLint level, 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,
GLsizei height, GLsizei depth, GLenum format, GLenum type, GLsizei bufSize, void* pixels);
void GetTextureParameterfv(GLuint texture, GLenum pname, GLfloat* params);
@@ -98,6 +106,9 @@ namespace MobileGL::MG_Impl::GLImpl {
GLsizei width, GLsizei height);
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
GLsizei height);
void CopyTextureSubImage1D(GLuint texture, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width);
void CopyTextureSubImage3D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x,
GLint y, GLsizei width, GLsizei height);
void CopyTextureSubImage2D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y,
GLsizei width, GLsizei height);
void CopyTexSubImage1D(GLenum target, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width);
@@ -226,6 +226,9 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
case TextureInternalFormat::Depth24Stencil8:
case TextureInternalFormat::Depth32FStencil8:
case TextureInternalFormat::DepthStencil:
// Stencil-only is not a colour format either: a colour client format read against a
// STENCIL_INDEX8 texture has to be the same INVALID_OPERATION as against a depth one.
case TextureInternalFormat::StencilIndex8:
return true;
default:
return false;
@@ -105,12 +105,45 @@ namespace MobileGL::MG_Impl::GLImpl {
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) {
// Bound by the same dynamic limit as attribute indices: the default attribute -> binding
// 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()) {
if (bindingindex >= VertexArrayImpl::GetMaxVertexAttribBindings()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
@@ -155,6 +188,17 @@ namespace MobileGL::MG_Impl::GLImpl {
SharedPtr<MG_State::GLState::VertexArrayObject> GetNamedVertexArrayObject_State(GLuint vaobj,
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::ValidateVertexArrayObject(vaobj)) return nullptr;
return MG_State::pGLContext->GetVertexArrayObject(vaobj);
@@ -210,7 +254,7 @@ namespace MobileGL::MG_Impl::GLImpl {
DataType dataType = MG_Util::ConvertGLEnumToDataType(type);
// 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();
if (!vao) {
@@ -227,6 +271,7 @@ namespace MobileGL::MG_Impl::GLImpl {
vao->SetAttributeFormat(index, size, dataType, false, stride, offset, true, false);
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,
@@ -234,7 +279,7 @@ namespace MobileGL::MG_Impl::GLImpl {
if (!VertexArrayImpl::ValidateVertexAttributeIndex(index)) return;
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;
auto& vao = MG_State::pGLContext->GetBoundVertexArray();
@@ -256,6 +301,7 @@ namespace MobileGL::MG_Impl::GLImpl {
const int effectiveSize = isBgra ? 4 : size;
vao->SetAttributeFormat(index, effectiveSize, dataType, normalized, stride, offset, false, isBgra);
vao->BindAttributeBuffer(index, vbo);
vao->MirrorPointerIntoBinding(index, vbo, offset, EffectiveVertexStride(stride, effectiveSize, type));
}
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."));
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);
if (buffer != 0 && !bufferObject) return;
@@ -376,6 +429,7 @@ namespace MobileGL::MG_Impl::GLImpl {
const GLintptr* offsets, const GLsizei* strides) {
auto vao = GetNamedVertexArrayObject_State(vaobj, "VertexArrayVertexBuffers_State");
if (!vao) return;
if (!ValidateVertexBindingRange(first, count, "VertexArrayVertexBuffers_State")) return;
for (GLsizei i = 0; i < count; ++i) {
if (!buffers) {
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,
GLuint attribindex, GLint size, GLenum type, GLboolean normalized,
GLuint relativeoffset, Bool isInteger, const char* caller) {
static_cast<void>(caller);
if (!VertexArrayImpl::ValidateVertexAttributeIndex(attribindex)) return;
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,
@@ -1044,6 +1122,83 @@ namespace MobileGL::MG_Impl::GLImpl {
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,
GLuint relativeoffset) {
VertexArrayAttribFormat_State(vaobj, attribindex, size, type, normalized, relativeoffset);
@@ -1076,6 +1231,7 @@ namespace MobileGL::MG_Impl::GLImpl {
const GLsizei* strides) {
auto vao = GetBoundVertexArrayOrError("BindVertexBuffers");
if (!vao) return;
if (!ValidateVertexBindingRange(first, count, "BindVertexBuffers")) return;
for (GLsizei i = 0; i < count; ++i) {
if (!buffers) {
VertexBufferBinding_State(vao, first + i, 0, 0, 16, "BindVertexBuffers");
@@ -1100,6 +1256,18 @@ namespace MobileGL::MG_Impl::GLImpl {
"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) {
auto vao = GetBoundVertexArrayOrError("VertexAttribBinding");
if (!vao) return;
@@ -92,9 +92,13 @@ namespace MobileGL::MG_Impl::GLImpl {
void EnableVertexArrayAttrib(GLuint vaobj, GLuint index);
void VertexArrayElementBuffer(GLuint vaobj, GLuint buffer);
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,
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 VertexArrayBindingDivisor(GLuint vaobj, GLuint bindingindex, GLuint divisor);
void VertexArrayVertexBuffers(GLuint vaobj, GLuint first, GLsizei count, const GLuint* buffers,
@@ -104,6 +108,7 @@ namespace MobileGL::MG_Impl::GLImpl {
const GLsizei* strides);
void VertexAttribFormat(GLuint attribindex, GLint size, GLenum type, GLboolean normalized, 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 VertexBindingDivisor(GLuint bindingindex, 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);
}
Uint GetMaxVertexAttribBindings() {
return GetMaxVertexAttribs();
}
Uint GetMaxVertexAttribRelativeOffset() {
return 2047;
}
Uint GetMaxVertexAttribStride() {
return 2048;
}
Bool ValidateVertexArrayName(Uint index) {
Bool isValid = MG_State::pGLContext->ValidateVertexArrayName(index);
if (!isValid) {
@@ -90,9 +102,31 @@ namespace MobileGL::MG_Impl::GLImpl::VertexArrayImpl {
return true;
}
Bool ValidateVertexAttribFormat(Uint index, GLint sizeRaw, DataType type, Bool normalized, Int stride,
Bool integerPath) {
Bool ValidateVertexAttribFormat(Uint index, GLint sizeRaw, GLenum glType, DataType type, Bool normalized,
Int stride, Bool integerPath) {
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) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
@@ -170,4 +204,40 @@ namespace MobileGL::MG_Impl::GLImpl::VertexArrayImpl {
}
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
@@ -15,6 +15,20 @@ namespace MobileGL::MG_Impl::GLImpl::VertexArrayImpl {
// capacity). Falls back to the capacity when no backend is active (unit tests).
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 ValidateVertexArrayObject(Uint index);
Bool ValidateVertexAttributeIndex(Uint index);
@@ -22,6 +36,13 @@ namespace MobileGL::MG_Impl::GLImpl::VertexArrayImpl {
// 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);
// integerPath selects the glVertexAttribIPointer rules.
Bool ValidateVertexAttribFormat(Uint index, GLint sizeRaw, DataType type, Bool normalized, Int stride,
Bool integerPath);
Bool ValidateVertexAttribFormat(Uint index, GLint sizeRaw, GLenum glType, DataType type, Bool normalized,
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
@@ -174,6 +174,21 @@ namespace MobileGL::MG_State::GLState {
++m_changeSerial;
}
void BufferObject::MarkGpuWritten() {
m_gpuWritePending = true;
}
void BufferObject::SyncGpuWrites() {
if (!m_gpuWritePending) return;
// Cleared unconditionally: without a readback op the shadow can never catch up,
// and retrying on every subsequent read would only repeat the same no-op.
m_gpuWritePending = false;
if (m_size == 0 || g_bufferBackendOps == nullptr || g_bufferBackendOps->ReadbackFromGpu == nullptr) {
return;
}
g_bufferBackendOps->ReadbackFromGpu(*this);
}
void BufferObject::UploadSubData(DataPtr data, SizeT atOffset) {
MOBILEGL_ASSERT(!m_isMapped || (m_mappingAccess & BufferMappingAccessBit::Persistent),
"Cannot upload sub data while buffer is non-persistently mapped.");
@@ -204,11 +219,13 @@ namespace MobileGL::MG_State::GLState {
"Destination buffer copy out of bounds: dstOffset (%zu) + size (%zu) > m_size (%zu)", dstOffset,
size, m_size);
src->SyncGpuWrites();
Memcpy(m_resource.Bytes() + dstOffset, src->m_resource.Bytes() + srcOffset, size);
NotifyContentWrite(dstOffset, size);
}
void* BufferObject::AcquireMemory(Bool markMapped, Bool read, Bool write) {
SyncGpuWrites();
if (markMapped) {
m_isMapped = true;
m_mappingAccess = (read ? BufferMappingAccessBit::Read : BufferMappingAccessBit::Null) |
@@ -231,10 +248,29 @@ namespace MobileGL::MG_State::GLState {
return m_resource.Bytes();
}
Bool BufferObject::EnsureGpuResidentStorage() {
if (m_resource.IsGpuResident()) {
return true;
}
if (m_size == 0 || g_bufferBackendOps == nullptr || g_bufferBackendOps->AcquirePersistentMap == nullptr) {
return false;
}
void* base = g_bufferBackendOps->AcquirePersistentMap(*this);
if (base == nullptr) {
return false;
}
m_resource.AdoptPersistentMap(base);
return true;
}
void* BufferObject::AcquireMemoryRange(Range1D range, Flags<BufferMappingAccessBit> access) {
MOBILEGL_ASSERT(range.end <= m_size && range.start <= range.end,
"AcquireMemoryRange out of bounds: range (%zu, %zu) exceeds m_size (%zu)", range.start,
range.end, m_size);
// The app is about to look at the bytes; a shader may have rewritten them since
// the shadow was last authoritative. Also needed for a write map without an
// invalidate bit, whose staging copy is seeded from the shadow.
SyncGpuWrites();
m_isMapped = true;
m_mappingAccess = access;
m_mappedRange = range;
@@ -99,6 +99,13 @@ namespace MobileGL {
// Must be idempotent: a second call for an already-backed buffer returns the
// same base pointer.
void* (*AcquirePersistentMap)(BufferObject& bufferObject) = nullptr;
// Pulls the backend's current contents for the whole buffer into the shadow
// (through WritebackFromBackend). Only ever called for a buffer the GPU may
// have written behind the frontend's back - a shader storage or atomic counter
// binding of a draw or dispatch - because nothing else can desynchronise the
// shadow. Backends that cannot read their storage back leave this null; the
// shadow then keeps its pre-dispatch bytes, which is the old behaviour.
void (*ReadbackFromGpu)(BufferObject& bufferObject) = nullptr;
};
// Registered by the active backend at init, cleared at shutdown.
@@ -133,6 +140,11 @@ namespace MobileGL {
void* AcquireMemory(Bool markMapped, Bool read, Bool write);
void* AcquireMemoryRange(Range1D range, Flags<BufferMappingAccessBit> access);
// Adopt backend host-visible coherent GPU storage as the source of truth
// (used for GPU-written targets like transform feedback capture, so
// MapBuffer/GetBufferSubData read real GPU results). No-op when already
// resident or when the backend declines.
Bool EnsureGpuResidentStorage();
void ReleaseMemory();
void FlushMemoryRange(SizeT offset, SizeT length);
@@ -144,6 +156,16 @@ namespace MobileGL {
// backend op: the backend storage already holds these bytes.
void WritebackFromBackend(DataPtr data, SizeT atOffset);
// A draw or dispatch just ran with this buffer bound where a shader can write
// it (shader storage / atomic counter). The next read has to reconcile with
// that: pull the bytes back, or - when the shadow already IS coherent GPU
// memory - wait for the work that wrote them to retire. Which of the two is
// the backend's business; the flag only says a GPU write is outstanding.
void MarkGpuWritten();
// Refreshes the shadow from the backend when a GPU write is outstanding. Called
// from every path that reads the shadow on the app's behalf.
void SyncGpuWrites();
Bool IsMapped() const;
Bool IsImmutableStorage() const;
SizeT GetSize() const;
@@ -191,6 +213,8 @@ namespace MobileGL {
Bool m_isImmutableStorage = false;
GLbitfield m_storageFlags = 0;
Uint64 m_changeSerial = 0;
// Set by MarkGpuWritten, cleared by SyncGpuWrites once the shadow is refreshed.
Bool m_gpuWritePending = false;
Range1D m_mappedRange;
Vector<Uint8> m_stagingData;
Bool m_ownsStagingData;
@@ -31,6 +31,9 @@ namespace MobileGL::MG_State::GLState {
BindingSlot<BufferObject>& GetBindingSlot(BufferTarget target);
// For glBindBufferBase / glBindBufferRange
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 {
auto it = std::find(BufferBindPointTargets.begin(), BufferBindPointTargets.end(), target);
auto index = std::distance(BufferBindPointTargets.begin(), it);
+228
View File
@@ -241,6 +241,32 @@ namespace MobileGL::MG_State {
}
void GLContext::MarkTextureObjectForDeletion(Uint index) {
// GL 3.3 core 4.4.2: deleting a texture whose image is attached to the framebuffer
// that is currently bound acts as if FramebufferTexture* had been called with texture
// zero for every attachment point it occupied there. Framebuffers that are NOT bound
// keep the orphaned attachment, so only the bound ones are touched.
//
// Without this the framebuffer object goes on holding the deleted texture alive as its
// attachment, and a later read through that framebuffer returns the dead texture's
// contents rather than those of whatever the application put in its place - the name
// it deleted usually comes straight back from the next glGenTextures, so the two are
// indistinguishable from the outside (KHR-GL32.packed_pixels read a stale gradient).
if (const auto& textureObject = m_textureState.GetTextureObject(index)) {
for (SizeT targetIndex = 0; targetIndex < SizeT(FramebufferTarget::FramebufferTargetCount);
++targetIndex) {
const auto& framebuffer =
GetFramebufferBindingSlot(static_cast<FramebufferTarget>(targetIndex)).GetBoundObject();
if (!framebuffer || framebuffer->IsDefaultFramebuffer()) {
continue;
}
const auto& attachments = framebuffer->GetAllAttachmentObjects();
for (SizeT i = 0; i < attachments.size(); ++i) {
if (attachments[i].IsTexture() && attachments[i].GetTexture() == textureObject) {
framebuffer->Detach(static_cast<FramebufferAttachmentType>(i));
}
}
}
}
m_textureState.MarkTextureObjectForDeletion(index, IsRelaxedSemanticsActive());
}
@@ -398,6 +424,14 @@ namespace MobileGL::MG_State {
m_renderState.SetPointSize(size);
}
void GLContext::SetPatchVertices(Uint vertices) {
m_renderState.SetPatchVertices(vertices);
}
Uint GLContext::GetPatchVertices() const {
return m_renderState.GetPatchVertices();
}
Float GLContext::GetPointSize() const {
return m_renderState.GetPointSize();
}
@@ -719,6 +753,200 @@ namespace MobileGL::MG_State {
Bool GLContext::ValidateRenderbufferObject(Uint index) const {
return m_renderbufferState.ValidateRenderbufferObject(index);
}
void GLContext::SaveBoundTransformFeedbackState() {
auto& object = m_transformFeedbackObjects[m_boundTransformFeedback];
for (Uint i = 0; i < MAX_TRANSFORM_FEEDBACK_BUFFERS; ++i) {
const auto& point = m_bufferState.GetBindingPoint(BufferTarget::TransformFeedback, i);
object.bindings[i] = {point.GetBoundObject(), point.GetRange(), point.HasExplicitRange()};
}
object.active = m_transformFeedbackActive;
object.paused = m_transformFeedbackPaused;
object.primitiveMode = m_transformFeedbackPrimitiveMode;
object.program = m_transformFeedbackProgram;
object.generation = m_transformFeedbackGeneration;
object.capturedVertices = m_transformFeedbackCapturedVertices;
object.inputPrimitives = m_transformFeedbackInputPrimitives;
}
void GLContext::RestoreBoundTransformFeedbackState() {
const auto& object = m_transformFeedbackObjects[m_boundTransformFeedback];
for (Uint i = 0; i < MAX_TRANSFORM_FEEDBACK_BUFFERS; ++i) {
auto& point = m_bufferState.GetBindingPoint(BufferTarget::TransformFeedback, i);
point.Bind(object.bindings[i].buffer);
if (object.bindings[i].buffer) {
point.SetRange(object.bindings[i].range, object.bindings[i].hasExplicitRange);
} else {
point.ClearRange();
}
}
m_transformFeedbackActive = object.active;
m_transformFeedbackPaused = object.paused;
m_transformFeedbackPrimitiveMode = object.primitiveMode;
m_transformFeedbackProgram = object.program;
// The generation identifies one capture span, and a span belongs to the object
// that opened it - a backend keys its append state on it, so switching objects
// has to bring the right one back.
m_transformFeedbackGeneration = object.generation;
m_transformFeedbackCapturedVertices = object.capturedVertices;
m_transformFeedbackInputPrimitives = object.inputPrimitives;
}
void GLContext::GenTransformFeedbackNames(Uint number, Vector<Uint>& ids) {
ids.resize(number);
if (number == 0) return;
m_transformFeedbackNames.Generate(number, ids.data());
// A generated name already denotes an object with the default state, so that a
// bind never has to distinguish "first use" from any later one.
for (const Uint id : ids) {
m_transformFeedbackObjects[id] = {};
}
}
// Program pipeline
void GLContext::GenProgramPipelineNames(Uint number, Vector<Uint>& pipelines) {
pipelines.resize(number);
// Names only: glIsProgramPipeline must answer GL_FALSE until one is bound or created.
m_programPipelineNames.Generate(number, pipelines.data());
}
void GLContext::CreateProgramPipelineObject(Uint index) {
m_programPipelines[index] = MakeShared<ProgramPipelineObject>(index);
}
Bool GLContext::ValidateProgramPipelineName(Uint index) const {
return index == 0 || m_programPipelineNames.IsValid(index);
}
Bool GLContext::IsProgramPipelineObject(Uint index) const {
if (index == 0 || !m_programPipelineNames.IsValid(index)) return false;
return m_programPipelines.find(index) != m_programPipelines.end();
}
void GLContext::BindProgramPipelineObject(Uint index) {
if (index != 0 && m_programPipelines.find(index) == m_programPipelines.end()) {
// First bind is what turns a reserved name into an object.
m_programPipelines[index] = MakeShared<ProgramPipelineObject>(index);
}
m_boundProgramPipeline = index;
}
void GLContext::MarkProgramPipelineForDeletion(Uint index) {
if (index == 0 || !m_programPipelineNames.IsValid(index)) return;
if (index == m_boundProgramPipeline) {
m_boundProgramPipeline = 0;
}
m_programPipelines.erase(index);
m_programPipelineNames.Delete(index);
}
const SharedPtr<ProgramPipelineObject>& GLContext::GetProgramPipelineObject(Uint index) const {
static const SharedPtr<ProgramPipelineObject> kNone;
const auto it = m_programPipelines.find(index);
return it == m_programPipelines.end() ? kNone : it->second;
}
const SharedPtr<ProgramPipelineObject>& GLContext::GetBoundProgramPipeline() const {
return GetProgramPipelineObject(m_boundProgramPipeline);
}
Bool GLContext::ValidateTransformFeedbackName(Uint index) const {
return index == 0 || m_transformFeedbackNames.IsValid(index);
}
void GLContext::BindTransformFeedbackObject(Uint index) {
if (index == m_boundTransformFeedback) return;
SaveBoundTransformFeedbackState();
m_boundTransformFeedback = index;
m_transformFeedbackObjects[index].everBound = true;
RestoreBoundTransformFeedbackState();
}
Bool GLContext::IsTransformFeedbackObject(Uint index) const {
if (index == 0 || !m_transformFeedbackNames.IsValid(index)) return false;
const auto it = m_transformFeedbackObjects.find(index);
return it != m_transformFeedbackObjects.end() && it->second.everBound;
}
void GLContext::MarkTransformFeedbackObjectForDeletion(Uint index) {
if (index == 0 || !m_transformFeedbackNames.IsValid(index)) return;
// Deleting the bound object reverts to the default one (GL 4.6 core 13.2.1);
// its state is dropped rather than saved back into the dying object.
if (index == m_boundTransformFeedback) {
m_boundTransformFeedback = 0;
RestoreBoundTransformFeedbackState();
}
m_transformFeedbackObjects.erase(index);
m_transformFeedbackNames.Delete(index);
}
Uint64 GLContext::GetTransformFeedbackRecordedVertices(Uint index) const {
const auto it = m_transformFeedbackObjects.find(index);
return it == m_transformFeedbackObjects.end() ? 0 : it->second.recordedVertices;
}
Bool GLContext::HasTransformFeedbackCompletedSpan(Uint index) const {
const auto it = m_transformFeedbackObjects.find(index);
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
// Leak-at-exit storage; see GlobalObjects.cpp.
+166
View File
@@ -14,6 +14,7 @@
#include "MG_State/GLState/RenderbufferState/RenderbufferState.h"
#include "RenderState/RenderState.h"
#include "ProgramState/ProgramState.h"
#include "ProgramState/ProgramPipelineObject.h"
#include "SamplerState/SamplerState.h"
#include "TextureState/TextureState.h"
#include "FramebufferState/FramebufferState.h"
@@ -45,6 +46,14 @@ namespace MobileGL {
// translates the result into its own API call.
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 {
public:
GLContext() = default;
@@ -129,6 +138,19 @@ namespace MobileGL {
void UseProgram(Uint program);
const SharedPtr<ProgramObject>& GetCurrentProgram();
// Program pipeline (GL_ARB_separate_shader_objects, GL 4.6 core 7.4). Like queries
// and transform feedbacks, glGenProgramPipelines only RESERVES a name - the object
// appears on first bind - while glCreateProgramPipelines makes it immediately.
void GenProgramPipelineNames(Uint number, Vector<Uint>& pipelines);
void CreateProgramPipelineObject(Uint index);
Bool ValidateProgramPipelineName(Uint index) const;
Bool IsProgramPipelineObject(Uint index) const;
void BindProgramPipelineObject(Uint index);
void MarkProgramPipelineForDeletion(Uint index);
const SharedPtr<ProgramPipelineObject>& GetProgramPipelineObject(Uint index) const;
Uint GetBoundProgramPipelineName() const { return m_boundProgramPipeline; }
const SharedPtr<ProgramPipelineObject>& GetBoundProgramPipeline() const;
// RenderState
Uint GetRenderStateParametersVersion() const;
const RenderStateParameters& GetRenderStateParameters() const;
@@ -138,6 +160,8 @@ namespace MobileGL {
Float GetLineWidth() const;
void SetPointSize(Float size);
Float GetPointSize() const;
void SetPatchVertices(Uint vertices);
Uint GetPatchVertices() const;
void SetPolygonOffset(Float factor, Float units);
Float GetPolygonOffsetFactor() const;
Float GetPolygonOffsetUnits() const;
@@ -211,6 +235,103 @@ namespace MobileGL {
void SetScissorBox(IntVec4 box); // x, y, width, height
const IntVec4& GetScissorBox() const; // x, y, width, height
// Transform feedback. The fields below are the state of the transform
// feedback object currently bound to GL_TRANSFORM_FEEDBACK; see the object
// block further down for how a bind swaps them.
void BeginTransformFeedback(GLenum primitiveMode, const SharedPtr<ProgramObject>& program) {
m_transformFeedbackActive = true;
m_transformFeedbackPaused = false;
m_transformFeedbackPrimitiveMode = primitiveMode;
m_transformFeedbackProgram = program;
m_transformFeedbackGeneration = ++m_transformFeedbackNextGeneration;
m_transformFeedbackCapturedVertices = 0;
m_transformFeedbackInputPrimitives = 0;
}
void EndTransformFeedback() {
m_transformFeedbackActive = false;
m_transformFeedbackPaused = false;
m_transformFeedbackProgram.reset();
// What glDrawTransformFeedback on this object replays from now on.
auto& object = m_transformFeedbackObjects[m_boundTransformFeedback];
object.recordedVertices = m_transformFeedbackCapturedVertices;
object.hasCompletedSpan = true;
}
Bool IsTransformFeedbackActive() const { return m_transformFeedbackActive; }
Bool IsTransformFeedbackPaused() const { return m_transformFeedbackPaused; }
void SetTransformFeedbackPaused(Bool paused) { m_transformFeedbackPaused = paused; }
GLenum GetTransformFeedbackPrimitiveMode() const { return m_transformFeedbackPrimitiveMode; }
const SharedPtr<ProgramObject>& GetTransformFeedbackProgram() const {
return m_transformFeedbackProgram;
}
// Bumped on every BeginTransformFeedback; the backend uses it to
// distinguish "resume appending" from "fresh capture".
Uint64 GetTransformFeedbackGeneration() const { return m_transformFeedbackGeneration; }
// CPU-side primitive accounting for the transform feedback queries:
// every captured draw adds its primitive count (draws without a
// geometry stage write exactly what they generate).
void AddTransformFeedbackPrimitives(Uint64 primitives) {
m_transformFeedbackPrimitiveCounter += primitives;
}
Uint64 GetTransformFeedbackPrimitiveCounter() const { return m_transformFeedbackPrimitiveCounter; }
// Primitives a draw assembled while the capture was paused. GL counts those in
// PRIMITIVES_GENERATED, but a backend that answers the query with its own
// transform feedback counter cannot see them - nothing was being captured.
void AddTransformFeedbackPausedPrimitives(Uint64 primitives) {
m_transformFeedbackPausedPrimitiveCounter += primitives;
}
Uint64 GetTransformFeedbackPausedPrimitiveCounter() const {
return m_transformFeedbackPausedPrimitiveCounter;
}
// Vertices already captured since BeginTransformFeedback (drives the
// buffer-capacity clamp on the primitives-written accounting).
void AddTransformFeedbackCapturedVertices(Uint64 vertices) {
m_transformFeedbackCapturedVertices += vertices;
}
Uint64 GetTransformFeedbackCapturedVertices() const { return m_transformFeedbackCapturedVertices; }
// Raw assembled input primitives fed to the capture stage since Begin
// (pre-clamp; drives the GS strip capture-order fixup at EndTF).
void AddTransformFeedbackInputPrimitives(Uint64 primitives) {
m_transformFeedbackInputPrimitives += primitives;
}
Uint64 GetTransformFeedbackInputPrimitives() const { return m_transformFeedbackInputPrimitives; }
// Transform feedback objects (ARB_transform_feedback2 / GL 4.0 core).
// The capture state above and the indexed GL_TRANSFORM_FEEDBACK_BUFFER
// binding points are object state, but the context keeps exactly one live
// copy of both so that every existing reader - the backends' per-draw sync,
// the drawing and getter paths - needs no notion of which object owns them.
// A bind therefore saves the live copy into the outgoing object and restores
// the incoming one's. Object 0 is the default object and always exists.
static constexpr Uint MAX_TRANSFORM_FEEDBACK_BUFFERS = 4;
void GenTransformFeedbackNames(Uint number, Vector<Uint>& ids);
// A name glGenTransformFeedbacks handed out and glDeleteTransformFeedbacks
// has not taken back. Name 0 is always valid.
Bool ValidateTransformFeedbackName(Uint index) const;
// What glIsTransformFeedback reports: a generated name only becomes the name
// of an object once it has been bound at least once (GL 4.6 core 13.2.1).
Bool IsTransformFeedbackObject(Uint index) const;
void BindTransformFeedbackObject(Uint index);
void MarkTransformFeedbackObjectForDeletion(Uint index);
Uint GetBoundTransformFeedbackName() const { return m_boundTransformFeedback; }
// Vertices the object captured in its last completed span; the vertex count
// glDrawTransformFeedback replays.
Uint64 GetTransformFeedbackRecordedVertices(Uint index) const;
// Whether the object has ever completed a capture span. glDrawTransformFeedback
// on an object that has not is INVALID_OPERATION, which a zero vertex count
// cannot express: an empty completed span is legal and draws nothing.
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
void GenFramebufferNames(Uint number, Vector<Uint>& framebuffers);
const SharedPtr<FramebufferObject>& GetFramebufferObject(Uint index);
@@ -243,6 +364,51 @@ namespace MobileGL {
BufferState m_bufferState;
VertexArrayState m_vertexArrayState;
Array<CurrentVertexAttributeValue, VertexArrayObject::MAX_VERTEX_ATTRIBS> m_currentVertexAttributes{};
Bool m_transformFeedbackActive = false;
Bool m_transformFeedbackPaused = false;
GLenum m_transformFeedbackPrimitiveMode = GL_POINTS;
SharedPtr<ProgramObject> m_transformFeedbackProgram;
Uint64 m_transformFeedbackGeneration = 0;
// Source of the per-span ids above; never rolls back with an object switch.
Uint64 m_transformFeedbackNextGeneration = 0;
// Not object state: the transform feedback queries snapshot it at BeginQuery
// and take the delta at EndQuery, which spans whatever objects were used.
Uint64 m_transformFeedbackPrimitiveCounter = 0;
Uint64 m_transformFeedbackPausedPrimitiveCounter = 0;
Uint64 m_transformFeedbackCapturedVertices = 0;
Uint64 m_transformFeedbackInputPrimitives = 0;
// Everything a transform feedback object owns while it is NOT the bound one.
struct TransformFeedbackObjectState {
struct SavedBufferBinding {
SharedPtr<BufferObject> buffer;
Range1D range;
Bool hasExplicitRange = false;
};
Array<SavedBufferBinding, MAX_TRANSFORM_FEEDBACK_BUFFERS> bindings;
Bool active = false;
Bool paused = false;
GLenum primitiveMode = GL_POINTS;
SharedPtr<ProgramObject> program;
Uint64 generation = 0;
Uint64 capturedVertices = 0;
Uint64 inputPrimitives = 0;
Uint64 recordedVertices = 0;
Bool hasCompletedSpan = false;
Bool everBound = false;
};
void SaveBoundTransformFeedbackState();
void RestoreBoundTransformFeedbackState();
// operator[] materialises an entry with the default state on first touch, so
// the default object (name 0) needs no seeding here.
UnorderedMap<Uint, TransformFeedbackObjectState> m_transformFeedbackObjects;
IndexGenerator<Uint> m_transformFeedbackNames;
Uint m_boundTransformFeedback = 0;
// Map membership IS object existence here: a pipeline has no stateful default
// object 0, so no everBound flag is needed.
UnorderedMap<Uint, SharedPtr<ProgramPipelineObject>> m_programPipelines;
IndexGenerator<Uint> m_programPipelineNames;
Uint m_boundProgramPipeline = 0;
TextureState m_textureState;
ProgramState m_programState;
RenderState m_renderState;
@@ -185,6 +185,20 @@ namespace MobileGL::MG_State::GLState {
return m_externalIndex;
}
#define MOBILEGL_DEFINE_FRAMEBUFFER_DEFAULT_SETTER(name, member, type) \
void FramebufferObject::Set##name(type value) { \
if (member == value) return; \
member = value; \
++m_objectVersion; \
}
MOBILEGL_DEFINE_FRAMEBUFFER_DEFAULT_SETTER(DefaultWidth, m_defaultWidth, Int)
MOBILEGL_DEFINE_FRAMEBUFFER_DEFAULT_SETTER(DefaultHeight, m_defaultHeight, Int)
MOBILEGL_DEFINE_FRAMEBUFFER_DEFAULT_SETTER(DefaultLayers, m_defaultLayers, Int)
MOBILEGL_DEFINE_FRAMEBUFFER_DEFAULT_SETTER(DefaultSamples, m_defaultSamples, Int)
MOBILEGL_DEFINE_FRAMEBUFFER_DEFAULT_SETTER(DefaultFixedSampleLocations, m_defaultFixedSampleLocations, Bool)
#undef MOBILEGL_DEFINE_FRAMEBUFFER_DEFAULT_SETTER
void FramebufferObject::BumpAttachmentVersion(FramebufferAttachmentType type) {
++m_attachmentVersions[static_cast<SizeT>(type)];
++m_objectVersion;
@@ -129,6 +129,19 @@ namespace MobileGL {
void SetReadBuffer(FramebufferAttachmentType buf);
FramebufferAttachmentType GetReadBuffer() const { return m_readBuffer; }
// GL_ARB_framebuffer_no_attachments state (GL 4.6 core table 23.24). The shape a
// framebuffer with no attachments would rasterize at; all zero / FALSE until set.
Int GetDefaultWidth() const { return m_defaultWidth; }
Int GetDefaultHeight() const { return m_defaultHeight; }
Int GetDefaultLayers() const { return m_defaultLayers; }
Int GetDefaultSamples() const { return m_defaultSamples; }
Bool GetDefaultFixedSampleLocations() const { return m_defaultFixedSampleLocations; }
void SetDefaultWidth(Int value);
void SetDefaultHeight(Int value);
void SetDefaultLayers(Int value);
void SetDefaultSamples(Int value);
void SetDefaultFixedSampleLocations(Bool value);
FramebufferAttachmentVersionArray GetAllFramebufferAttachmentVersions() const {
return m_attachmentVersions;
}
@@ -148,6 +161,12 @@ namespace MobileGL {
FramebufferAttachmentArray m_drawBuffers; // Probably no versioning needed for this, just check equality
FramebufferAttachmentType m_readBuffer = FramebufferAttachmentType::None;
Int m_defaultWidth = 0;
Int m_defaultHeight = 0;
Int m_defaultLayers = 0;
Int m_defaultSamples = 0;
Bool m_defaultFixedSampleLocations = false;
// This version will bump when draw/read buffer changes (by `glDrawBuffer(s)`/`glReadBuffer`)
Uint16 m_objectVersion = 0;
};
@@ -170,9 +170,280 @@ namespace MobileGL::MG_State::GLState {
m_uniformNameMaxLength = 0;
m_attribInNameMaxLength = 0;
m_uniformBlockNameMaxLength = 0;
m_xfbVaryings.clear();
m_xfbStrides.clear();
m_xfbBufferMode = GL_INTERLEAVED_ATTRIBS;
m_xfbVaryingNameMaxLength = 0;
m_xfbNeedsScatteredCapture = false;
m_xfbPackedStride = 0;
m_gsInputPrimitive = GL_NONE;
m_linkStatus = false;
}
namespace {
// GL type enum for a vertex-stage output symbol captured by transform
// feedback. Covers the scalar/vector/matrix float+integer types transform
// feedback may legally capture in GL 3.3.
Bool ResolveXfbSymbolType(const glslang::TType& type, GLenum& outType, GLint& outArraySize,
Uint32& outBytesPerElement) {
outArraySize = type.isArray() ? type.getOuterArraySize() : 1;
const Int columns = type.isMatrix() ? type.getMatrixCols() : 1;
const Int components = type.isMatrix() ? type.getMatrixRows()
: (type.isVector() ? type.getVectorSize() : 1);
const glslang::TBasicType basic = type.getBasicType();
static constexpr GLenum kFloatTypes[5] = {0, GL_FLOAT, GL_FLOAT_VEC2, GL_FLOAT_VEC3, GL_FLOAT_VEC4};
static constexpr GLenum kIntTypes[5] = {0, GL_INT, GL_INT_VEC2, GL_INT_VEC3, GL_INT_VEC4};
static constexpr GLenum kUintTypes[5] = {0, GL_UNSIGNED_INT, GL_UNSIGNED_INT_VEC2, GL_UNSIGNED_INT_VEC3,
GL_UNSIGNED_INT_VEC4};
static constexpr GLenum kDoubleTypes[5] = {0, GL_DOUBLE, GL_DOUBLE_VEC2, GL_DOUBLE_VEC3,
GL_DOUBLE_VEC4};
if (type.isMatrix()) {
if (basic != glslang::EbtFloat && basic != glslang::EbtDouble) return false;
static constexpr GLenum kMatTypes[5][5] = {
{}, {},
{0, 0, GL_FLOAT_MAT2, GL_FLOAT_MAT2x3, GL_FLOAT_MAT2x4},
{0, 0, GL_FLOAT_MAT3x2, GL_FLOAT_MAT3, GL_FLOAT_MAT3x4},
{0, 0, GL_FLOAT_MAT4x2, GL_FLOAT_MAT4x3, GL_FLOAT_MAT4},
};
static constexpr GLenum kDoubleMatTypes[5][5] = {
{}, {},
{0, 0, GL_DOUBLE_MAT2, GL_DOUBLE_MAT2x3, GL_DOUBLE_MAT2x4},
{0, 0, GL_DOUBLE_MAT3x2, GL_DOUBLE_MAT3, GL_DOUBLE_MAT3x4},
{0, 0, GL_DOUBLE_MAT4x2, GL_DOUBLE_MAT4x3, GL_DOUBLE_MAT4},
};
if (columns < 2 || columns > 4 || components < 2 || components > 4) return false;
outType = basic == glslang::EbtDouble ? kDoubleMatTypes[columns][components]
: kMatTypes[columns][components];
} else if (components >= 1 && components <= 4) {
switch (basic) {
case glslang::EbtFloat: outType = kFloatTypes[components]; break;
case glslang::EbtInt: outType = kIntTypes[components]; break;
case glslang::EbtUint: outType = kUintTypes[components]; break;
// A double-typed varying is capturable like any other; rejecting it here reported
// the varying as "not an output of the vertex stage", which it plainly was.
case glslang::EbtDouble: outType = kDoubleTypes[components]; break;
default: return false;
}
} else {
return false;
}
// GL 4.6 core 11.1.2.1: a double component occupies eight basic machine units, and
// counts as two components against the transform feedback limits.
const Uint32 bytesPerComponent = basic == glslang::EbtDouble ? 8u : 4u;
outBytesPerElement = static_cast<Uint32>(columns * components) * bytesPerComponent;
return true;
}
} // namespace
Bool ProgramObject::ResolveTransformFeedbackVaryings() {
m_xfbVaryings.clear();
m_xfbStrides.clear();
m_xfbBufferMode = m_requestedXfbBufferMode;
m_xfbVaryingNameMaxLength = 0;
m_xfbNeedsScatteredCapture = false;
m_xfbPackedStride = 0;
if (m_requestedXfbVaryings.empty()) {
return true;
}
// Capture happens at the last vertex-processing stage (geometry, then
// tessellation evaluation, then vertex).
const glslang::TIntermediate* captureIntermediate = nullptr;
for (EShLanguage stage : {EShLangGeometry, EShLangTessEvaluation, EShLangVertex}) {
captureIntermediate = m_program->getIntermediate(stage);
if (captureIntermediate != nullptr) {
break;
}
}
if (captureIntermediate == nullptr) {
m_infoLog = "Transform feedback varyings requested but the program has no vertex-processing stage.";
return false;
}
const glslang::TIntermAggregate* linkerObjects = captureIntermediate->findLinkerObjects();
const Bool interleaved = m_xfbBufferMode == GL_INTERLEAVED_ATTRIBS;
Uint32 interleavedOffset = 0;
// ARB_transform_feedback3 lets an interleaved capture leave holes (gl_SkipComponents1..4)
// and move on to the next buffer (gl_NextBuffer). Both only affect where the following
// varyings land, so they are consumed here and never become XfbVaryings of their own -
// which also keeps them out of the name list a backend declares on its own driver.
Uint32 interleavedBufferIndex = 0;
Vector<Uint32> interleavedStrides;
for (SizeT i = 0; i < m_requestedXfbVaryings.size(); ++i) {
const String& name = m_requestedXfbVaryings[i];
if (interleaved && name == "gl_NextBuffer") {
interleavedStrides.push_back(interleavedOffset);
interleavedOffset = 0;
++interleavedBufferIndex;
m_xfbNeedsScatteredCapture = true;
continue;
}
if (interleaved && name.size() == 18 && name.compare(0, 17, "gl_SkipComponents") == 0 &&
name[17] >= '1' && name[17] <= '4') {
interleavedOffset += static_cast<Uint32>(name[17] - '0') * 4;
m_xfbNeedsScatteredCapture = true;
continue;
}
for (SizeT j = 0; j < i; ++j) {
if (m_requestedXfbVaryings[j] == name) {
m_infoLog = "Transform feedback varying '" + name + "' is specified more than once.";
return false;
}
}
XfbVarying varying;
varying.name = name;
Uint32 bytesPerElement = 0;
Bool resolved = false;
if (name == "gl_Position") {
varying.type = GL_FLOAT_VEC4;
varying.size = 1;
bytesPerElement = 16;
resolved = true;
} else if (name == "gl_PointSize") {
varying.type = GL_FLOAT;
varying.size = 1;
bytesPerElement = 4;
resolved = true;
} else if (linkerObjects != nullptr) {
for (const auto* node : linkerObjects->getSequence()) {
const glslang::TIntermSymbol* symbol = node->getAsSymbolNode();
if (symbol == nullptr || symbol->getType().getQualifier().storage != glslang::EvqVaryingOut) {
continue;
}
if (symbol->getName() != name.c_str()) {
continue;
}
resolved = ResolveXfbSymbolType(symbol->getType(), varying.type, varying.size, bytesPerElement);
break;
}
}
if (!resolved) {
m_infoLog = "Transform feedback varying '" + name + "' is not an output of the vertex stage.";
return false;
}
varying.byteSize = bytesPerElement * static_cast<Uint32>(varying.size);
varying.packedOffsetBytes = m_xfbPackedStride;
m_xfbPackedStride += varying.byteSize;
if (interleaved) {
varying.bufferIndex = interleavedBufferIndex;
varying.offsetBytes = interleavedOffset;
interleavedOffset += varying.byteSize;
} else {
varying.bufferIndex = static_cast<Uint32>(m_xfbVaryings.size());
varying.offsetBytes = 0;
}
m_xfbVaryingNameMaxLength =
std::max(m_xfbVaryingNameMaxLength, static_cast<Int>(name.size()) + 1);
m_xfbVaryings.push_back(Move(varying));
}
constexpr Uint32 kMaxSeparateAttribs = 4;
constexpr Uint32 kMaxSeparateComponents = 4;
constexpr Uint32 kMaxInterleavedComponents = 64;
constexpr Uint32 kMaxTransformFeedbackBuffers = 4;
if (interleaved) {
interleavedStrides.push_back(interleavedOffset);
if (interleavedStrides.size() > kMaxTransformFeedbackBuffers) {
m_infoLog = "Transform feedback capture uses more buffers than "
"GL_MAX_TRANSFORM_FEEDBACK_BUFFERS.";
return false;
}
for (const Uint32 stride : interleavedStrides) {
if (stride > kMaxInterleavedComponents * 4) {
m_infoLog = "Transform feedback interleaved capture exceeds "
"GL_MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS.";
return false;
}
}
m_xfbStrides = Move(interleavedStrides);
} else {
if (m_xfbVaryings.size() > kMaxSeparateAttribs) {
m_infoLog = "Transform feedback separate capture exceeds "
"GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS.";
return false;
}
m_xfbStrides.resize(m_xfbVaryings.size());
for (SizeT i = 0; i < m_xfbVaryings.size(); ++i) {
if (m_xfbVaryings[i].byteSize > kMaxSeparateComponents * 4) {
m_infoLog = "Transform feedback varying '" + m_xfbVaryings[i].name +
"' exceeds GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_COMPONENTS.";
return false;
}
m_xfbStrides[i] = m_xfbVaryings[i].byteSize;
}
}
ResolveGsTriangleStripCapture(captureIntermediate);
return true;
}
namespace {
// Extracts a geometry shader's per-invocation EmitVertex/EndPrimitive sequence
// when it is statically knowable (no emit inside selection/loop/switch). Vulkan
// transform feedback captures triangle strips in plain (i, i+1, i+2) order while
// GL decomposes odd strip triangles as (i+1, i, i+2) (GL 4.6 table 10.1); with
// the static strip lengths the capture buffer can be reordered after EndTF.
class GsEmitSequenceTraverser final : public glslang::TIntermTraverser {
public:
bool visitAggregate(glslang::TVisit, glslang::TIntermAggregate* node) override {
if (node->getOp() == glslang::EOpEmitVertex) {
++emitCount;
hasEmit = true;
} else if (node->getOp() == glslang::EOpEndPrimitive) {
FlushStrip();
}
return true;
}
bool visitSelection(glslang::TVisit, glslang::TIntermSelection*) override {
inControlFlow = true;
return true;
}
bool visitLoop(glslang::TVisit, glslang::TIntermLoop*) override {
inControlFlow = true;
return true;
}
bool visitSwitch(glslang::TVisit, glslang::TIntermSwitch*) override {
inControlFlow = true;
return true;
}
void FlushStrip() {
if (emitCount >= 3) {
stripTriangles.push_back(static_cast<Uint32>(emitCount - 2));
}
emitCount = 0;
}
Vector<Uint32> stripTriangles;
Uint32 emitCount = 0;
Bool hasEmit = false;
Bool inControlFlow = false;
};
} // namespace
void ProgramObject::ResolveGsTriangleStripCapture(const glslang::TIntermediate* captureIntermediate) {
m_gsStripTriangles.clear();
m_gsStripCaptureFixup = false;
if (captureIntermediate == nullptr || m_program == nullptr) {
return;
}
if (m_program->getIntermediate(EShLangGeometry) != captureIntermediate) {
return;
}
if (captureIntermediate->getOutputPrimitive() != glslang::ElgTriangleStrip) {
return;
}
GsEmitSequenceTraverser traverser;
const_cast<glslang::TIntermediate*>(captureIntermediate)->getTreeRoot()->traverse(&traverser);
traverser.FlushStrip(); // the invocation end acts as an implicit EndPrimitive
if (!traverser.hasEmit || traverser.inControlFlow || traverser.stripTriangles.empty()) {
return;
}
m_gsStripTriangles = Move(traverser.stripTriangles);
m_gsStripCaptureFixup = true;
}
bool ProgramObject::ShaderIsAttached(const SharedPtr<ShaderObject>& shader) {
MGLOG_D("ProgramObject %u: ShaderIsAttached check for shader %p", m_externalIndex, shader.get());
auto it = std::find_if(m_shaders.begin(), m_shaders.end(),
@@ -321,12 +592,32 @@ namespace MobileGL::MG_State::GLState {
return;
}
// GL_GEOMETRY_INPUT_TYPE. A draw's primitive type has to be compatible with it
// (GL 4.6 core 11.3.1), so it is resolved for every link, not only a capturing one.
m_gsInputPrimitive = GL_NONE;
if (const glslang::TIntermediate* gs = m_program->getIntermediate(EShLangGeometry)) {
switch (gs->getInputPrimitive()) {
case glslang::ElgPoints: m_gsInputPrimitive = GL_POINTS; break;
case glslang::ElgLines: m_gsInputPrimitive = GL_LINES; break;
case glslang::ElgLinesAdjacency: m_gsInputPrimitive = GL_LINES_ADJACENCY; break;
case glslang::ElgTriangles: m_gsInputPrimitive = GL_TRIANGLES; break;
case glslang::ElgTrianglesAdjacency: m_gsInputPrimitive = GL_TRIANGLES_ADJACENCY; break;
default: break;
}
}
MGLOG_D("ProgramObject %u: Starting reflection", m_externalIndex);
DoReflection();
MGLOG_D("ProgramObject %u: Reflection done (linkStatus=%d)", m_externalIndex, (int)m_linkStatus);
if (!ValidateFragmentOutputLocations()) {
return;
}
if (!ResolveTransformFeedbackVaryings()) {
m_linkStatus = false;
MGLOG_E("ProgramObject %u: transform feedback varying resolution failed: %s", m_externalIndex,
m_infoLog.c_str());
return;
}
MGLOG_D("ProgramObject %u: Starting binary generation", m_externalIndex);
GenerateBinary();
@@ -377,6 +377,17 @@ namespace MobileGL::MG_State::GLState {
Bool GetDeleteStatus() const { return m_deleteStatus; }
Bool GetLinkStatus() const { return m_linkStatus; }
// GL_PROGRAM_BINARY_RETRIEVABLE_HINT. MobileGL exposes no program binary format
// (GL_NUM_PROGRAM_BINARY_FORMATS is 0), so the hint is pure state - which is all
// ARB_get_program_binary requires of it.
Bool GetBinaryRetrievableHint() const { return m_binaryRetrievableHint; }
void SetBinaryRetrievableHint(Bool hint) { m_binaryRetrievableHint = hint; }
// glProgramBinary always fails here (there is no format it could accept) and the
// spec then requires the program's LINK_STATUS to read FALSE.
void MarkLinkFailedByProgramBinary() {
ResetLinkArtifacts();
m_infoLog = "No program binary format is supported.";
}
Bool GetValidateStatus() const { return m_validateStatus; }
Int GetActiveAtomicCounterCount() const { return m_program->getNumAtomicCounters(); }
Int GetActiveAttributesCount() const { return m_program->getNumPipeInputs(); }
@@ -465,6 +476,55 @@ namespace MobileGL::MG_State::GLState {
return it == m_shaders.end() ? -1 : (Int)std::distance(m_shaders.begin(), it);
}
// Transform feedback (GL 3.0 core: glTransformFeedbackVaryings applies on
// the NEXT link; the linked snapshot below is what draws and queries see).
struct XfbVarying {
String name;
GLenum type = GL_FLOAT;
GLint size = 1; // array element count
Uint32 bufferIndex = 0; // capture buffer slot
Uint32 offsetBytes = 0; // offset within the capture buffer
Uint32 byteSize = 0; // bytes captured per vertex for this varying
// Offset within the gap-free record a backend that cannot express the GL
// layout captures into; see NeedsScatteredTransformFeedbackCapture.
Uint32 packedOffsetBytes = 0;
};
void SetTransformFeedbackVaryings(Vector<String>&& names, GLenum bufferMode) {
m_requestedXfbVaryings = Move(names);
m_requestedXfbBufferMode = bufferMode;
}
GLenum GetTransformFeedbackBufferMode() const { return m_xfbBufferMode; }
SizeT GetTransformFeedbackVaryingCount() const { return m_xfbVaryings.size(); }
const XfbVarying* GetTransformFeedbackVarying(SizeT index) const {
return index < m_xfbVaryings.size() ? &m_xfbVaryings[index] : nullptr;
}
const Vector<XfbVarying>& GetTransformFeedbackVaryings() const { return m_xfbVaryings; }
// Stride of one captured vertex in the given capture buffer slot.
Uint32 GetTransformFeedbackStride(Uint32 bufferIndex) const {
return bufferIndex < m_xfbStrides.size() ? m_xfbStrides[bufferIndex] : 0;
}
SizeT GetTransformFeedbackBufferCount() const { return m_xfbStrides.size(); }
Int GetTransformFeedbackVaryingMaxLength() const { return m_xfbVaryingNameMaxLength; }
// True when the capture layout uses gl_SkipComponents / gl_NextBuffer
// (ARB_transform_feedback3), which no ES driver can express: it can only pack every
// captured varying into one record with no gaps. A backend that captures through
// such a driver has to capture into scratch storage and scatter the records into the
// application's buffers itself, using packedOffsetBytes as the source offset and
// (bufferIndex, offsetBytes, stride) as the destination.
Bool NeedsScatteredTransformFeedbackCapture() const { return m_xfbNeedsScatteredCapture; }
// Bytes one gap-free captured record occupies.
Uint32 GetTransformFeedbackPackedStride() const { return m_xfbPackedStride; }
// True when the capture stage is a triangle-strip geometry shader with a
// statically-known emit sequence: the Vulkan capture order then needs the GL
// odd-triangle vertex swap after EndTransformFeedback.
Bool HasGsTriangleStripCaptureFixup() const { return m_gsStripCaptureFixup; }
// Triangles per strip, in emission order, for ONE geometry invocation.
const Vector<Uint32>& GetGsStripTriangles() const { return m_gsStripTriangles; }
// GL_GEOMETRY_INPUT_TYPE of the linked geometry stage (GL_POINTS, GL_LINES,
// GL_LINES_ADJACENCY, GL_TRIANGLES or GL_TRIANGLES_ADJACENCY), or GL_NONE when the
// program has no geometry stage. Draws must present a compatible primitive type.
GLenum GetGeometryInputType() const { return m_gsInputPrimitive; }
Uint GetExternalIndex() const { return m_externalIndex; }
// Globally-unique, never-reused id for this program object's lifetime. Unlike the GL
// name (external index), which is freed to a LIFO list and immediately handed back by
@@ -475,6 +535,11 @@ namespace MobileGL::MG_State::GLState {
private:
void ResetLinkArtifacts();
void DoReflection();
// Resolves the requested transform feedback varyings against the linked
// vertex stage; fails the link (GL semantics) on unknown or duplicate
// names or exceeded capture limits.
Bool ResolveTransformFeedbackVaryings();
void ResolveGsTriangleStripCapture(const glslang::TIntermediate* captureIntermediate);
void GenerateBinary();
void WaitUntilGenerationCompleted() const;
void AddDefaultFragmentShaderIfMissing();
@@ -539,6 +604,7 @@ namespace MobileGL::MG_State::GLState {
String m_infoLog;
Bool m_deleteStatus = false;
Bool m_linkStatus = false;
Bool m_binaryRetrievableHint = false;
Bool m_validateStatus = true;
Uint32 m_backendStateVersion = 0;
@@ -558,5 +624,18 @@ namespace MobileGL::MG_State::GLState {
mutable Uint32 m_backendHashMemoVersion = ~0u;
Uint32 m_uboContentVersion = 0;
Uint32 m_linkVersion = 0;
// Transform feedback: request (applies at next link) and linked snapshot.
Vector<String> m_requestedXfbVaryings;
GLenum m_requestedXfbBufferMode = GL_INTERLEAVED_ATTRIBS;
Vector<XfbVarying> m_xfbVaryings;
Vector<Uint32> m_xfbStrides;
Vector<Uint32> m_gsStripTriangles;
Bool m_gsStripCaptureFixup = false;
GLenum m_gsInputPrimitive = GL_NONE;
GLenum m_xfbBufferMode = GL_INTERLEAVED_ATTRIBS;
Int m_xfbVaryingNameMaxLength = 0;
Bool m_xfbNeedsScatteredCapture = false;
Uint32 m_xfbPackedStride = 0;
};
} // namespace MobileGL::MG_State::GLState
@@ -0,0 +1,52 @@
// MobileGL - MobileGL/MG_State/GLState/ProgramState/ProgramPipelineObject.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 <Includes.h>
#include "ProgramObject.h"
namespace MobileGL {
namespace MG_State {
namespace GLState {
// A GL_ARB_separate_shader_objects program pipeline (GL 4.6 core 7.4): a set of
// per-stage program references plus the program glProgramUniform* addresses when no
// program object is in use.
class ProgramPipelineObject {
public:
explicit ProgramPipelineObject(Uint externalIndex) : m_externalIndex(externalIndex) {}
const SharedPtr<ProgramObject>& GetStageProgram(ShaderStage stage) const {
return m_stagePrograms[static_cast<SizeT>(stage)];
}
void SetStageProgram(ShaderStage stage, const SharedPtr<ProgramObject>& program) {
m_stagePrograms[static_cast<SizeT>(stage)] = program;
}
const SharedPtr<ProgramObject>& GetActiveProgram() const { return m_activeProgram; }
void SetActiveProgram(const SharedPtr<ProgramObject>& program) { m_activeProgram = program; }
// Distinct from ProgramObject's, which starts true: a pipeline that has never been
// validated must report GL_VALIDATE_STATUS as 0 (GL 4.6 core table 23.31).
Bool GetValidateStatus() const { return m_validateStatus; }
void SetValidateStatus(Bool value) { m_validateStatus = value; }
const String& GetInfoLog() const { return m_infoLog; }
void SetInfoLog(String log) { m_infoLog = Move(log); }
Uint GetExternalIndex() const { return m_externalIndex; }
private:
Array<SharedPtr<ProgramObject>, static_cast<SizeT>(ShaderStage::ShaderStageCount)> m_stagePrograms{};
SharedPtr<ProgramObject> m_activeProgram;
String m_infoLog;
const Uint m_externalIndex = 0;
Bool m_validateStatus = false;
};
} // namespace GLState
} // namespace MG_State
} // namespace MobileGL
@@ -29,17 +29,25 @@ namespace MobileGL::MG_State::GLState {
if (!CheckIndexAvail(program, m_programObjects)) return; // FIXME: add error reporting here
auto& programObject = m_programObjects[program];
if (programObject != nullptr) {
// Snapshot the attachments: deleting the program is a detach point for shaders
// that were flagged with glDeleteShader while still attached.
const Vector<SharedPtr<ShaderObject>> attachedShaders = programObject->GetAttachedShaders();
programObject->MarkAsDeleted();
programObject.reset();
m_programIndexGenerator.Delete(program);
for (const auto& shader : attachedShaders) {
const Uint shaderName = shader->GetExternalIndex();
if (CheckIndexAvail(shaderName, m_shaderObjects) && m_shaderObjects[shaderName] == shader) {
ReleaseShaderNameIfOrphaned(shaderName);
}
// A program in use is only FLAGGED: its name (and every program query) stays
// valid until it stops being current, at which point UseProgram finishes the job.
if (programObject == m_currentProgram) return;
DestroyProgramSlot(program);
}
}
void ProgramState::DestroyProgramSlot(const Uint program) {
auto& programObject = m_programObjects[program];
// Snapshot the attachments: deleting the program is a detach point for shaders
// that were flagged with glDeleteShader while still attached.
const Vector<SharedPtr<ShaderObject>> attachedShaders = programObject->GetAttachedShaders();
programObject.reset();
m_programIndexGenerator.Delete(program);
for (const auto& shader : attachedShaders) {
const Uint shaderName = shader->GetExternalIndex();
if (CheckIndexAvail(shaderName, m_shaderObjects) && m_shaderObjects[shaderName] == shader) {
ReleaseShaderNameIfOrphaned(shaderName);
}
}
}
@@ -49,10 +57,22 @@ namespace MobileGL::MG_State::GLState {
}
void ProgramState::UseProgram(Uint program) {
const SharedPtr<ProgramObject> previous = m_currentProgram;
if (program == 0) m_currentProgram.reset();
if (!CheckIndexAvail(program, m_programObjects)) return;
m_currentProgram = m_programObjects[program];
if (CheckIndexAvail(program, m_programObjects)) {
m_currentProgram = m_programObjects[program];
}
// A deletion flagged while the program was current takes effect the moment it
// stops being current.
if (previous != nullptr && previous != m_currentProgram && previous->GetDeleteStatus()) {
const Uint previousName = previous->GetExternalIndex();
if (CheckIndexAvail(previousName, m_programObjects) && m_programObjects[previousName] == previous) {
DestroyProgramSlot(previousName);
}
}
}
Uint ProgramState::CreateShader(ShaderStage stage) {
@@ -35,6 +35,9 @@ namespace MobileGL::MG_State::GLState {
private:
Bool ShaderHasGLVisibleAttachment(const SharedPtr<ShaderObject>& shaderObject) const;
// Frees the name slot and releases orphaned attached shaders; the immediate half
// of glDeleteProgram (deferred while the program is current).
void DestroyProgramSlot(Uint program);
template <typename T>
static Bool CheckIndexAvail(const SizeT idx, const Vector<T>& vec) {
@@ -169,6 +169,15 @@ namespace MobileGL::MG_State::GLState {
}
}
const std::optional<String> reservedError =
MG_Util::ShaderTranspiler::FindReservedIdentifierViolation(compileSource);
if (reservedError) {
m_compileStatus = false;
m_shader.reset();
m_infoLog = *reservedError;
return;
}
// Compile for OpenGL here, so that we can do validation and link
// like a real OpenGL driver at linking stage
// Will compile for other backends later.
@@ -154,6 +154,17 @@ namespace MobileGL {
return m_parameters.PointSize;
}
void RenderState::SetPatchVertices(Uint vertices) {
if (m_parameters.PatchVertices == vertices) return;
m_parameters.PatchVertices = vertices;
++m_version;
}
Uint RenderState::GetPatchVertices() const {
return m_parameters.PatchVertices;
}
void RenderState::SetPolygonOffset(Float factor, Float units) {
if (m_parameters.PolygonOffsetFactor == factor && m_parameters.PolygonOffsetUnits == units) return;
@@ -224,6 +224,8 @@ namespace MobileGL {
IntVec4 Viewport = IntVec4(0, 0, 0, 0); // x, y, width, height
Float LineWidth = 1.0f;
Float PointSize = 1.0f;
// GL_PATCH_VERTICES: how many vertices one tessellation patch consumes.
Uint PatchVertices = 3;
Float PolygonOffsetFactor = 0.0f;
Float PolygonOffsetUnits = 0.0f;
@@ -319,6 +321,8 @@ namespace MobileGL {
Float GetLineWidth() const;
void SetPointSize(Float size);
Float GetPointSize() const;
void SetPatchVertices(Uint vertices);
Uint GetPatchVertices() const;
void SetPolygonOffset(Float factor, Float units);
Float GetPolygonOffsetFactor() const;
Float GetPolygonOffsetUnits() const;
@@ -139,6 +139,61 @@ namespace MobileGL {
return m_samplerParameters.maxAnisotropy;
}
// The three border-colour representations are kept in step so a getter of any form has
// an answer whichever form was written. Integer <-> float uses the plain value, matching
// what glTexParameterIiv/Iuiv mean: those forms are for integer texture formats, whose
// border components are the raw integers rather than a normalized fraction.
void SamplerObject::SetBorderColor(const FloatVec4& color) {
if (color == m_samplerParameters.borderColor) return;
m_samplerParameters.borderColor = color;
m_samplerParameters.borderColorI =
IntVec4(static_cast<Int32>(color.x()), static_cast<Int32>(color.y()),
static_cast<Int32>(color.z()), static_cast<Int32>(color.w()));
m_samplerParameters.borderColorUI =
UintVec4(static_cast<Uint32>(color.x()), static_cast<Uint32>(color.y()),
static_cast<Uint32>(color.z()), static_cast<Uint32>(color.w()));
++m_version;
}
void SamplerObject::SetBorderColorI(const IntVec4& color) {
if (color == m_samplerParameters.borderColorI) return;
m_samplerParameters.borderColorI = color;
m_samplerParameters.borderColorUI =
UintVec4(static_cast<Uint32>(color.x()), static_cast<Uint32>(color.y()),
static_cast<Uint32>(color.z()), static_cast<Uint32>(color.w()));
m_samplerParameters.borderColor =
FloatVec4(static_cast<Float>(color.x()), static_cast<Float>(color.y()),
static_cast<Float>(color.z()), static_cast<Float>(color.w()));
++m_version;
}
void SamplerObject::SetBorderColorUI(const UintVec4& color) {
if (color == m_samplerParameters.borderColorUI) return;
m_samplerParameters.borderColorUI = color;
m_samplerParameters.borderColorI =
IntVec4(static_cast<Int32>(color.x()), static_cast<Int32>(color.y()),
static_cast<Int32>(color.z()), static_cast<Int32>(color.w()));
m_samplerParameters.borderColor =
FloatVec4(static_cast<Float>(color.x()), static_cast<Float>(color.y()),
static_cast<Float>(color.z()), static_cast<Float>(color.w()));
++m_version;
}
const FloatVec4& SamplerObject::GetBorderColor() const {
return m_samplerParameters.borderColor;
}
const IntVec4& SamplerObject::GetBorderColorI() const {
return m_samplerParameters.borderColorI;
}
const UintVec4& SamplerObject::GetBorderColorUI() const {
return m_samplerParameters.borderColorUI;
}
SamplerCompareMode SamplerObject::GetCompareMode() const {
return m_samplerParameters.compareMode;
}
@@ -8,6 +8,7 @@
#pragma once
#include <Includes.h>
#include <MG_Util/Math/VectorTypes.h>
namespace MobileGL {
enum class SamplerFilterMode {
@@ -66,8 +67,18 @@ namespace MobileGL {
Float maxLod = 1000.0f;
Float lodBias = 0.0f;
Float maxAnisotropy = 1.0f;
SamplerCompareFunc compareFunc = SamplerCompareFunc::Always;
// GL 4.6 core table 23.18 / GLES 3.2 table 21.16: TEXTURE_COMPARE_FUNC starts at LEQUAL,
// for both sampler objects and the sampler state a texture object carries.
SamplerCompareFunc compareFunc = SamplerCompareFunc::LessEqual;
SamplerCompareMode compareMode = SamplerCompareMode::None;
// TEXTURE_BORDER_COLOR is sampler state (GL 4.6 core table 23.18), so it belongs here and
// not on the texture - a texture object reaches it through the sampler object it owns. The
// three representations are the float, integer and unsigned-integer forms glSamplerParameterfv,
// glSamplerParameterIiv and glSamplerParameterIuiv set; whichever is written last defines
// the colour and the other two follow it, so a getter always has an answer.
FloatVec4 borderColor = {0.0f, 0.0f, 0.0f, 0.0f};
IntVec4 borderColorI = {0, 0, 0, 0};
UintVec4 borderColorUI = {0, 0, 0, 0};
};
namespace MG_State {
@@ -87,6 +98,9 @@ namespace MobileGL {
void SetMaxAnisotropy(Float maxAnisotropy);
void SetSamplerCompareFunc(SamplerCompareFunc func);
void SetCompareMode(SamplerCompareMode mode);
void SetBorderColor(const FloatVec4& color);
void SetBorderColorI(const IntVec4& color);
void SetBorderColorUI(const UintVec4& color);
SamplerWrapMode GetWrapS() const;
SamplerWrapMode GetWrapT() const;
@@ -100,6 +114,9 @@ namespace MobileGL {
Float GetMaxAnisotropy() const;
SamplerCompareMode GetCompareMode() const;
SamplerCompareFunc GetSamplerCompareFunc() const;
const FloatVec4& GetBorderColor() const;
const IntVec4& GetBorderColorI() const;
const UintVec4& GetBorderColorUI() const;
Uint GetExternalIndex() const;
Uint16 GetVersion() const;
// Globally-unique, never-reused id for this sampler object's lifetime. Lets a
@@ -162,6 +162,7 @@ namespace MobileGL {
DepthComponent32F,
Depth24Stencil8,
Depth32FStencil8,
StencilIndex8,
DepthComponent,
DepthStencil,
@@ -24,6 +24,19 @@ namespace MobileGL {
TextureObjectBase::TextureObjectBase(TextureTarget target, Uint externalIndex)
: m_externalIndex(externalIndex), m_lifetimeId(AllocateLifetimeId()), m_target(target) {
m_sampler = MakeShared<SamplerObject>(0);
if (target == TextureTarget::TextureRectangle) {
// A rectangle texture has no mip chain, so its initial sampler state is not
// the shared one: TEXTURE_MIN_FILTER is LINEAR and TEXTURE_WRAP_S/T are
// CLAMP_TO_EDGE (GL 4.6 core table 23.15). Leaving the 2D default of
// NEAREST_MIPMAP_LINEAR in place makes the texture mipmap-incomplete from
// birth, and every lookup that the application never re-filtered reads
// (0, 0, 0, 1) instead of its contents.
m_sampler->SetMinFilter(SamplerFilterMode::Linear);
m_sampler->SetMipmapMode(SamplerMipmapMode::None);
m_sampler->SetWrapS(SamplerWrapMode::ClampToEdge);
m_sampler->SetWrapT(SamplerWrapMode::ClampToEdge);
m_sampler->SetWrapR(SamplerWrapMode::ClampToEdge);
}
}
TextureInternalFormat TextureObjectBase::GetFormat() const {
@@ -75,48 +88,40 @@ namespace MobileGL {
return m_externalIndex;
}
// TEXTURE_BORDER_COLOR is sampler state, so it lives on the SamplerObject this texture
// owns rather than being duplicated here - a sampler object bound over the texture then
// supplies its own, exactly as GL says it should. The texture params version still moves
// on a write, because the DirectGLES texture sync memoises on it.
const FloatVec4& TextureObjectBase::GetBorderColor() const {
return m_borderColor;
return m_sampler->GetBorderColor();
}
void TextureObjectBase::SetBorderColor(const FloatVec4& color) {
if (color == m_borderColor) return;
if (color == m_sampler->GetBorderColor()) return;
m_borderColor = color;
m_borderColorI = IntVec4(static_cast<Int32>(color.x()), static_cast<Int32>(color.y()),
static_cast<Int32>(color.z()), static_cast<Int32>(color.w()));
m_borderColorUI = UintVec4(static_cast<Uint32>(color.x()), static_cast<Uint32>(color.y()),
static_cast<Uint32>(color.z()), static_cast<Uint32>(color.w()));
m_sampler->SetBorderColor(color);
++m_textureParamsVersion;
}
const IntVec4& TextureObjectBase::GetBorderColorI() const {
return m_borderColorI;
return m_sampler->GetBorderColorI();
}
void TextureObjectBase::SetBorderColorI(const IntVec4& color) {
if (color == m_borderColorI) return;
if (color == m_sampler->GetBorderColorI()) return;
m_borderColorI = color;
m_borderColorUI = UintVec4(static_cast<Uint32>(color.x()), static_cast<Uint32>(color.y()),
static_cast<Uint32>(color.z()), static_cast<Uint32>(color.w()));
m_borderColor = FloatVec4(static_cast<Float>(color.x()), static_cast<Float>(color.y()),
static_cast<Float>(color.z()), static_cast<Float>(color.w()));
m_sampler->SetBorderColorI(color);
++m_textureParamsVersion;
}
const UintVec4& TextureObjectBase::GetBorderColorUI() const {
return m_borderColorUI;
return m_sampler->GetBorderColorUI();
}
void TextureObjectBase::SetBorderColorUI(const UintVec4& color) {
if (color == m_borderColorUI) return;
if (color == m_sampler->GetBorderColorUI()) return;
m_borderColorUI = color;
m_borderColorI = IntVec4(static_cast<Int32>(color.x()), static_cast<Int32>(color.y()),
static_cast<Int32>(color.z()), static_cast<Int32>(color.w()));
m_borderColor = FloatVec4(static_cast<Float>(color.x()), static_cast<Float>(color.y()),
static_cast<Float>(color.z()), static_cast<Float>(color.w()));
m_sampler->SetBorderColorUI(color);
++m_textureParamsVersion;
}
@@ -335,6 +340,64 @@ namespace MobileGL {
// 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) {
if (texture == nullptr) return true;
if (!texture->IsComplete()) return false;
if (!mipmapped) return true;
const auto* mipmapTexture = AsMipmapTexture(texture);
if (mipmapTexture == nullptr) return true; // no mip chain to be incomplete about
const UintVec2& levelRange = texture->GetLevelRange();
const Uint baseLevel = levelRange.x();
const Uint storedLevels = mipmapTexture->GetMipmapLevelCount();
if (baseLevel >= storedLevels) return false;
// An array texture's layer count is not a dimension of the image: it stays put all
// the way down the chain (GL 4.6 core 8.14.3). GetMipmapTexelSize reports it in the
// slot after the image's own dimensions.
const TextureTarget target = texture->GetTarget();
Int shrinkingComponents = 3;
if (target == TextureTarget::Texture1DArray) {
shrinkingComponents = 1;
} else if (target == TextureTarget::Texture2DArray || target == TextureTarget::TextureCubeMapArray) {
shrinkingComponents = 2;
}
for (const auto uploadTarget : texture->GetUploadTargets()) {
const IntVec3 baseSize = mipmapTexture->GetMipmapTexelSize(uploadTarget, baseLevel);
Int largest = 0;
for (Int component = 0; component < shrinkingComponents; ++component) {
largest = std::max(largest, baseSize[component]);
}
if (largest <= 0) return false;
// p = log2 of the largest base dimension: the last level the chain needs
// before every dimension has reached 1. TEXTURE_MAX_LEVEL can cut it short.
Uint p = 0;
for (Int extent = largest; extent > 1; extent >>= 1) ++p;
const Uint lastLevel = std::min(baseLevel + p, levelRange.y());
for (Uint level = baseLevel; level <= lastLevel; ++level) {
if (level >= storedLevels) return false;
const IntVec3 actual = mipmapTexture->GetMipmapTexelSize(uploadTarget, level);
for (Int component = 0; component < 3; ++component) {
const Int expected = component < shrinkingComponents
? std::max(1, baseSize[component] >> (level - baseLevel))
: baseSize[component];
if (actual[component] != expected) return false;
}
}
}
return true;
}
} // namespace GLState
} // namespace MG_State
} // namespace MobileGL
@@ -60,6 +60,10 @@ namespace MobileGL::MG_State::GLState {
virtual Bool HasFixedSampleLocations() const = 0;
virtual void SetFixedSampleLocations(Bool fixedSampleLocations) = 0;
virtual Uint64 GetLifetimeId() const = 0;
// Which aspect of a packed depth/stencil texture a sampler reads (GL 4.6 core 8.10).
// DEPTH_COMPONENT until set, and meaningless for every other format.
virtual GLenum GetDepthStencilTextureMode() const = 0;
virtual void SetDepthStencilTextureMode(GLenum mode) = 0;
protected:
virtual Uint GetIndexOfTextureUploadTarget(TextureUploadTarget target) const = 0;
@@ -104,6 +108,8 @@ namespace MobileGL::MG_State::GLState {
Bool HasFixedSampleLocations() const override;
void SetFixedSampleLocations(Bool fixedSampleLocations) override;
Uint64 GetLifetimeId() const override;
GLenum GetDepthStencilTextureMode() const override { return m_depthStencilTextureMode; }
void SetDepthStencilTextureMode(GLenum mode) override { m_depthStencilTextureMode = mode; }
protected:
static Uint64 AllocateLifetimeId();
@@ -113,9 +119,6 @@ namespace MobileGL::MG_State::GLState {
const TextureTarget m_target = TextureTarget::Unknown;
TextureInternalFormat m_internalFormat = TextureInternalFormat::Unknown;
SharedPtr<SamplerObject> m_sampler = nullptr;
FloatVec4 m_borderColor = {0.0f, 0.0f, 0.0f, 0.0f};
IntVec4 m_borderColorI = {0, 0, 0, 0};
UintVec4 m_borderColorUI = {0, 0, 0, 0};
Vec4<TextureSwizzleParam> m_swizzleParams = {TextureSwizzleParam::Red, TextureSwizzleParam::Green,
TextureSwizzleParam::Blue, TextureSwizzleParam::Alpha};
UintVec2 m_levelRange = {0, 1000};
@@ -124,6 +127,7 @@ namespace MobileGL::MG_State::GLState {
// Starts at 1 so a freshly-created backend resource (snapshot 0) never spuriously
// matches before its first sync. Bumped only on dirty=true in MarkStorageDirty.
Uint64 m_contentVersion = 1;
GLenum m_depthStencilTextureMode = GL_DEPTH_COMPONENT;
Int m_samples = 0;
Bool m_fixedSampleLocations = true;
};
@@ -156,6 +160,20 @@ namespace MobileGL::MG_State::GLState {
? static_cast<TextureObjectMipmap*>(texture)
: nullptr;
}
// Whether the texture satisfies the mipmap-completeness rules a minification filter
// that samples the mip chain imposes (GL 4.6 core 8.17): every level from the base to
// the effective max must exist at exactly half the previous one's size. `mipmapped` is
// the effective sampler's answer to "does this filter read more than the base level" -
// when it is false only base-level completeness matters, which the ordinary
// IsComplete() already covers. Sampling an incomplete texture returns (0, 0, 0, 1).
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) {
return (texture && texture->GetStorageType() == TextureStorageType::Mipmap)
? static_cast<const TextureObjectMipmap*>(texture)
@@ -21,10 +21,31 @@ namespace MobileGL {
BindingSlot<BufferObject>& GetBufferBindingSlot(
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:
Uint GetIndexOfTextureUploadTarget(TextureUploadTarget target) const override;
BindingSlot<BufferObject> m_bufferBindingSlot = BindingSlot<BufferObject>(BufferTarget::Texture);
SizeT m_bufferRangeOffset = 0;
SizeT m_bufferRangeSize = kWholeBuffer;
const Vector<TextureUploadTarget> m_uploadTargets{TextureUploadTarget::TextureBuffer};
};
} // namespace GLState
@@ -77,6 +77,26 @@ namespace MobileGL::MG_State::GLState {
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) {
if (index >= MAX_VERTEX_ATTRIBS) return;
@@ -111,6 +131,11 @@ namespace MobileGL::MG_State::GLState {
void VertexArrayObject::SetAttributeDivisor(Uint index, Uint divisor) {
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;
m_attributes[index].Divisor = divisor;
BumpAttributeFormatVersion(index);
@@ -187,18 +212,18 @@ namespace MobileGL::MG_State::GLState {
}
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 (size < 1 || size > 4) return;
auto& attr = m_attributes[attribIndex];
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.Type = type;
attr.Normalized = normalized;
attr.IsInteger = isInteger;
attr.IsBgra = false; // the binding-format path (glVertexAttribFormat) does not carry BGRA
attr.IsBgra = isBgra;
m_attributeRelativeOffset[attribIndex] = relativeOffset;
BumpAttributeFormatVersion(attribIndex);
}
@@ -64,6 +64,12 @@ namespace MobileGL {
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();
const BindingSlot<BufferObject>& GetIndexBufferBindingSlot() const;
@@ -82,7 +88,22 @@ namespace MobileGL {
void SetBindingDivisor(Uint bindingIndex, Uint divisor);
void SetAttributeBinding(Uint attribIndex, Uint bindingIndex);
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 Array<VertexAttributeVersion, MAX_VERTEX_ATTRIBS>& GetAllAttributeVersions() const;
@@ -104,6 +125,23 @@ namespace MobileGL {
m_backendHashMemoVersion = m_configVersion;
}
// Backend-owned resolved-state memo: an opaque pointer into the
// backend's vertex-input-state cache plus the cache's eviction
// epoch, valid while the config version matches. Lets the
// per-draw path skip the content hash AND the cache lookup; the
// epoch guards against the cache evicting the pointee.
Bool GetBackendStateMemo(const void*& outState, Uint64& outEpoch) const {
if (m_backendStateMemoVersion != m_configVersion) return false;
outState = m_backendStateMemo;
outEpoch = m_backendStateMemoEpoch;
return true;
}
void SetBackendStateMemo(const void* state, Uint64 epoch) const {
m_backendStateMemo = state;
m_backendStateMemoEpoch = epoch;
m_backendStateMemoVersion = m_configVersion;
}
private:
void BumpAttributeFormatVersion(Uint index);
void BumpAttributeBufferVersion(Uint index);
@@ -137,6 +175,9 @@ namespace MobileGL {
Uint32 m_configVersion = 0;
mutable Uint64 m_backendHashMemo = 0;
mutable Uint32 m_backendHashMemoVersion = ~0u;
mutable const void* m_backendStateMemo = nullptr;
mutable Uint64 m_backendStateMemoEpoch = 0;
mutable Uint32 m_backendStateMemoVersion = ~0u;
};
} // namespace GLState
} // namespace MG_State
@@ -34,6 +34,11 @@ namespace {
GLint maxFragmentImageUniforms = 4;
GLint maxComputeImageUniforms = 5;
bool maxGeometryImageUniformsQueried = false;
GLfloat minFragmentInterpolationOffset = -0.75f;
GLfloat maxFragmentInterpolationOffset = 0.625f;
GLint fragmentInterpolationOffsetBits = 6;
bool fragmentInterpolationLimitsQueried = false;
bool fragmentInterpolationQueryRaisesError = false;
// Emulates ANGLE-on-Vulkan: the draw reads the indirect command's
// baseInstance word and exposes it through gl_InstanceID.
bool drawLeaksBaseInstanceWord = false;
@@ -108,6 +113,14 @@ namespace {
case GL_MAX_COMPUTE_IMAGE_UNIFORMS:
*data = g_fake.maxComputeImageUniforms;
break;
case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS:
g_fake.fragmentInterpolationLimitsQueried = true;
if (g_fake.fragmentInterpolationQueryRaisesError) {
g_fake.pendingError = GL_INVALID_ENUM;
} else {
*data = g_fake.fragmentInterpolationOffsetBits;
}
break;
// FillInGLESCapabilities reads the context version before running the
// baseInstance probe, which requires ES >= 3.1.
case GL_MAJOR_VERSION:
@@ -155,6 +168,22 @@ namespace {
g_fake.maxTextureMaxAnisotropyQueried = true;
data[0] = g_fake.maxTextureMaxAnisotropy;
break;
case GL_MIN_FRAGMENT_INTERPOLATION_OFFSET:
g_fake.fragmentInterpolationLimitsQueried = true;
if (g_fake.fragmentInterpolationQueryRaisesError) {
g_fake.pendingError = GL_INVALID_ENUM;
} else {
data[0] = g_fake.minFragmentInterpolationOffset;
}
break;
case GL_MAX_FRAGMENT_INTERPOLATION_OFFSET:
g_fake.fragmentInterpolationLimitsQueried = true;
if (g_fake.fragmentInterpolationQueryRaisesError) {
g_fake.pendingError = GL_INVALID_ENUM;
} else {
data[0] = g_fake.maxFragmentInterpolationOffset;
}
break;
// Two-component range queries.
case GL_ALIASED_LINE_WIDTH_RANGE:
case GL_SMOOTH_LINE_WIDTH_RANGE:
@@ -462,6 +491,52 @@ TEST(ImageUniformCapabilities, QueriesRealPerStageLimitsAndConservativelyGatesGe
EXPECT_TRUE(g_fake.maxGeometryImageUniformsQueried);
}
TEST(FragmentInterpolationCapabilities, QueriesOnlyWhenSupportedAndPreservesDriverLimits) {
const auto funcs = MakeFakeGLESFunctions();
ResetFakeDriver();
g_fake.maxVertexSsboBlocks = 0;
MobileGL::MG_External::GLESCapabilities unsupportedCaps;
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(unsupportedCaps, funcs));
EXPECT_FALSE(unsupportedCaps.SupportsShaderMultisampleInterpolation);
EXPECT_FALSE(g_fake.fragmentInterpolationLimitsQueried);
EXPECT_FLOAT_EQ(unsupportedCaps.MinFragmentInterpolationOffset, -0.5f);
EXPECT_FLOAT_EQ(unsupportedCaps.MaxFragmentInterpolationOffset, 0.4375f);
EXPECT_EQ(unsupportedCaps.FragmentInterpolationOffsetBits, 4);
ResetFakeDriver();
g_fake.maxVertexSsboBlocks = 0;
g_fake.extensions.emplace_back("GL_OES_shader_multisample_interpolation");
// A stale error from an earlier capability probe must not make the optional
// interpolation query look like it failed.
g_fake.pendingError = GL_INVALID_OPERATION;
MobileGL::MG_External::GLESCapabilities supportedCaps;
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(supportedCaps, funcs));
EXPECT_TRUE(supportedCaps.SupportsShaderMultisampleInterpolation);
EXPECT_TRUE(g_fake.fragmentInterpolationLimitsQueried);
EXPECT_FLOAT_EQ(supportedCaps.MinFragmentInterpolationOffset, g_fake.minFragmentInterpolationOffset);
EXPECT_FLOAT_EQ(supportedCaps.MaxFragmentInterpolationOffset, g_fake.maxFragmentInterpolationOffset);
EXPECT_EQ(supportedCaps.FragmentInterpolationOffsetBits, g_fake.fragmentInterpolationOffsetBits);
EXPECT_EQ(funcs.glGetError(), GL_NO_ERROR);
}
TEST(FragmentInterpolationCapabilities, QueryErrorIsDrainedAndFallsBackToCoreMinimums) {
ResetFakeDriver();
g_fake.maxVertexSsboBlocks = 0;
g_fake.extensions.emplace_back("GL_OES_shader_multisample_interpolation");
g_fake.fragmentInterpolationQueryRaisesError = true;
const auto funcs = MakeFakeGLESFunctions();
MobileGL::MG_External::GLESCapabilities caps;
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(caps, funcs));
EXPECT_TRUE(g_fake.fragmentInterpolationLimitsQueried);
EXPECT_FLOAT_EQ(caps.MinFragmentInterpolationOffset, -0.5f);
EXPECT_FLOAT_EQ(caps.MaxFragmentInterpolationOffset, 0.4375f);
EXPECT_EQ(caps.FragmentInterpolationOffsetBits, 4);
EXPECT_EQ(funcs.glGetError(), GL_NO_ERROR);
}
// The extension string is what apps gate on (LWJGL builds GLCapabilities from it), so advertising
// it on a driver that cannot filter anisotropically would leave them silently on trilinear.
TEST(TextureAnisotropyCapabilities, ExtensionIsAdvertisedOnlyWhenTheHostDriverSupportsIt) {
+35 -19
View File
@@ -223,7 +223,7 @@ void main() {
PreprocessShaderSource(ShaderStage::Vertex, source);
EXPECT_EQ(source.find("#version 330 core\n"), 0);
EXPECT_EQ(source.find("#version 330 core "), 0);
EXPECT_NE(source.find("in vec3 position;"), String::npos);
EXPECT_NE(source.find("out vec2 uv;"), String::npos);
EXPECT_EQ(source.find("attribute"), String::npos);
@@ -323,7 +323,7 @@ void main() {
PreprocessShaderSource(ShaderStage::Fragment, source);
EXPECT_EQ(source.find("#version 330 core\n"), 0);
EXPECT_EQ(source.find("#version 330 core "), 0);
EXPECT_NE(source.find("out vec4 mg_FragColor;\n"), String::npos);
EXPECT_NE(source.find("in vec2 uv;"), String::npos);
EXPECT_NE(source.find("texture(texture0, uv)"), String::npos);
@@ -379,7 +379,7 @@ void main() {
PreprocessShaderSource(ShaderStage::Fragment, source);
EXPECT_EQ(source.find("#version 330 core\n"), 0);
EXPECT_EQ(source.find("#version 330 core "), 0);
EXPECT_NE(source.find("vec4 sample = texture(DiffuseSampler"), String::npos);
EXPECT_NE(source.find("totalAlpha = totalAlpha + sample.a;"), String::npos);
EXPECT_NE(source.find("float totalSamples = 0.0;"), String::npos);
@@ -405,7 +405,7 @@ void main() {
)";
PreprocessShaderSource(ShaderStage::Vertex, vertexSource);
EXPECT_EQ(vertexSource.find("#version 330 core\n"), 0);
EXPECT_EQ(vertexSource.find("#version 330 core "), 0);
EXPECT_NE(vertexSource.find("in vec3 sample;"), String::npos);
ShaderAttrib vertexAttrib{.shaderType = GL_VERTEX_SHADER, .sourceStr = vertexSource};
@@ -425,7 +425,7 @@ void main() {
)";
PreprocessShaderSource(ShaderStage::Fragment, fragmentSource);
EXPECT_EQ(fragmentSource.find("#version 330 core\n"), 0);
EXPECT_EQ(fragmentSource.find("#version 330 core "), 0);
EXPECT_NE(fragmentSource.find("uniform sampler2D sample;"), String::npos);
EXPECT_NE(fragmentSource.find("texture(sample, texCoord)"), String::npos);
@@ -483,7 +483,9 @@ void main() {
)";
PreprocessShaderSource(ShaderStage::Fragment, source);
EXPECT_EQ(source.find("#version 460 core\n"), 0);
// An explicitly declared modern core version keeps its number (see "keep declared modern
// GLSL versions strict"); only the BOM goes.
EXPECT_EQ(source.find(String(inputVersion) + "\n"), 0);
EXPECT_EQ(source.find("\xef\xbb\xbf"), String::npos);
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source};
@@ -568,10 +570,12 @@ void main() {
)";
PreprocessShaderSource(ShaderStage::Fragment, source);
const SizeT versionPos = source.find("#version 330 core\n");
const SizeT versionPos = source.find("#version 330 core ");
const SizeT outputPos = source.find("out vec4 mg_FragColor;\n");
EXPECT_NE(versionPos, String::npos);
EXPECT_EQ(outputPos, versionPos + std::strlen("#version 330 core\n"));
// The normalized directive carries a marker recording that this 330 came from a legacy
// declaration, so measure the line rather than assuming its length.
EXPECT_EQ(outputPos, source.find('\n', versionPos) + 1);
EXPECT_NE(source.find("// #version 460 core"), String::npos);
// This #line sits ahead of the version directive, where GLSL would never have honoured it, so
// it is still dropped. Directives that follow the version line are kept - see
@@ -684,7 +688,7 @@ void main() {
}
}
TEST_F(ProgramUtilTest, PreprocessModernSampleQualifierStaysAtVersion460) {
TEST_F(ProgramUtilTest, PreprocessModernSampleQualifierStaysAtItsDeclaredVersion) {
using namespace MG_Util::ShaderTranspiler;
String source = R"(#version 400 core
@@ -697,7 +701,7 @@ void main() {
)";
PreprocessShaderSource(ShaderStage::Fragment, source);
EXPECT_EQ(source.find("#version 460 core\n"), 0);
EXPECT_EQ(source.find("#version 400 core\n"), 0);
EXPECT_NE(source.find("sample in vec4 interpolatedColor;"), String::npos);
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source};
@@ -746,7 +750,7 @@ void main() {
PreprocessShaderSource(ShaderStage::Fragment, source);
EXPECT_EQ(source.find("#version 330 core\n"), 0);
EXPECT_EQ(source.find("#version 330 core "), 0);
EXPECT_NE(source.find("layout(location = 0) out vec4 mg_FragData[8];\n"), String::npos);
EXPECT_NE(source.find("mg_FragData[0] = vec4(1.0);"), String::npos);
EXPECT_NE(source.find("mg_FragData[1].a = 0.5;"), String::npos);
@@ -902,16 +906,17 @@ TEST_F(ProgramUtilTest, CompileSimpleVertexShader) {
}
// Legacy desktop sources are normalized to "#version 330 core", which is stricter than the 460 they
// used to be forced to. A shader declaring 330 while using 420-era syntax without the matching
// #extension line is accepted by real drivers, so CompileShader retries it at 460 instead of failing.
TEST_F(ProgramUtilTest, CompileShaderRetriesAt460WhenLegacyVersionRejects420Syntax) {
// used to be forced to. A legacy shader using 420-era syntax without the matching #extension line
// is accepted by real drivers, so CompileShader retries the normalized source at 460 rather than
// failing. Only MobileGL's own normalization is rescued this way - an application-declared
// "#version 330" keeps strict 3.30 semantics, which is what the CTS negative-compile cases need.
TEST_F(ProgramUtilTest, CompileShaderRetriesAt460WhenNormalizedLegacyVersionRejects420Syntax) {
using namespace MG_Util::ShaderTranspiler;
String source = R"(#version 330
String source = R"(#version 130
layout(binding = 0) uniform sampler2D InSampler;
in vec2 texCoord;
out vec4 fragColor;
varying vec2 texCoord;
void main() {
fragColor = texture(InSampler, texCoord);
gl_FragColor = texture2D(InSampler, texCoord);
})";
PreprocessShaderSource(ShaderStage::Fragment, source);
// The normal path still emits 330 - the retry must not become the default.
@@ -952,10 +957,21 @@ void main() {
TEST_F(ProgramUtilTest, RetargetLegacyVersionDirectiveOnlyTouchesNormalizedDesktopCore) {
using namespace MG_Util::ShaderTranspiler;
String normalized = "#version 330 core\nvoid main() {}\n";
// Only MobileGL's own normalization is retargetable, and it is recognised by the marker the
// preprocessor leaves on the directive line - so normalize a legacy source rather than
// hand-writing the directive the marker belongs to.
String normalized = "#version 130\nvoid main() {}\n";
PreprocessShaderSource(ShaderStage::Vertex, normalized);
ASSERT_EQ(normalized.find("#version 330 core "), 0u);
EXPECT_TRUE(RetargetLegacyVersionDirectiveTo460(normalized));
EXPECT_EQ(normalized.find("#version 460 core"), 0u);
// An application that declared 330 itself keeps strict 3.30 semantics: raising it would
// re-legalize the CTS negative-compile cases.
String declared330 = "#version 330 core\nvoid main() {}\n";
EXPECT_FALSE(RetargetLegacyVersionDirectiveTo460(declared330));
EXPECT_EQ(declared330.find("#version 330 core"), 0u);
// Already modern: nothing to retarget.
String modern = "#version 460 core\nvoid main() {}\n";
EXPECT_FALSE(RetargetLegacyVersionDirectiveTo460(modern));
+31 -2
View File
@@ -144,10 +144,19 @@ TEST_F(QueryTest, GenQueriesReturnsDistinctNonzeroIdsAndTracksLiveness) {
EXPECT_NE(ids[0], ids[2]);
EXPECT_NE(ids[1], ids[2]);
// GenQueries only reserves names: "they acquire query state only when they are first used by
// calling BeginQuery" (GL 4.6 core 4.2.1), and IsQuery answers for objects, not for reserved
// names. So all three read as FALSE here even though the names are taken.
EXPECT_EQ(MG_Impl::GLImpl::IsQuery(0), GL_FALSE);
EXPECT_EQ(MG_Impl::GLImpl::IsQuery(ids[0]), GL_FALSE);
EXPECT_EQ(MG_Impl::GLImpl::IsQuery(ids[1]), GL_FALSE);
EXPECT_EQ(MG_Impl::GLImpl::IsQuery(ids[2]), GL_FALSE);
// First use is what creates the object.
MG_Impl::GLImpl::BeginQuery(GL_TIME_ELAPSED, ids[0]);
MG_Impl::GLImpl::EndQuery(GL_TIME_ELAPSED);
EXPECT_EQ(MG_Impl::GLImpl::IsQuery(ids[0]), GL_TRUE);
EXPECT_EQ(MG_Impl::GLImpl::IsQuery(ids[1]), GL_TRUE);
EXPECT_EQ(MG_Impl::GLImpl::IsQuery(ids[2]), GL_TRUE);
EXPECT_EQ(MG_Impl::GLImpl::IsQuery(ids[1]), GL_FALSE);
MG_Impl::GLImpl::DeleteQueries(3, ids);
EXPECT_EQ(MG_Impl::GLImpl::IsQuery(ids[0]), GL_FALSE);
@@ -157,6 +166,26 @@ TEST_F(QueryTest, GenQueriesReturnsDistinctNonzeroIdsAndTracksLiveness) {
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// The direct state access counterpart: glCreateQueries creates the object outright, so unlike a
// glGenQueries name it is a query before anything is ever recorded into it.
TEST_F(QueryTest, CreateQueriesYieldsQueryObjectsImmediately) {
GLuint ids[2] = {0, 0};
MG_Impl::GLImpl::CreateQueries(GL_TIME_ELAPSED, 2, ids);
EXPECT_NE(ids[0], 0u);
EXPECT_NE(ids[1], 0u);
EXPECT_NE(ids[0], ids[1]);
EXPECT_EQ(MG_Impl::GLImpl::IsQuery(ids[0]), GL_TRUE);
EXPECT_EQ(MG_Impl::GLImpl::IsQuery(ids[1]), GL_TRUE);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::DeleteQueries(2, ids);
EXPECT_EQ(MG_Impl::GLImpl::IsQuery(ids[0]), GL_FALSE);
EXPECT_EQ(MG_Impl::GLImpl::IsQuery(ids[1]), GL_FALSE);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(QueryTest, TimeElapsedSpanFallsBackToImmediateZeroResult) {
GLuint id = 0;
MG_Impl::GLImpl::GenQueries(1, &id);
+109 -6
View File
@@ -197,13 +197,15 @@ TEST(DirectGLESSanity, AdvertisesDepthTextureForGlmarkShadowScenes) {
EXPECT_NE(std::find(extensions.begin(), extensions.end(), MobileGL::E_GL_ARB_depth_texture), extensions.end());
}
TEST(DirectGLESSanity, AdvertisesVoxyRequiredRenderingExtensionsWithoutRaisingGLVersion) {
// Voxy only ever needed the extensions, which stay advertised whatever the version is; the version
// assertion just pins what the backend really reports, now that V_OpenGL40 is in the list.
TEST(DirectGLESSanity, AdvertisesVoxyRequiredRenderingExtensions) {
MobileGL::MG_Backend::DirectGLES::BackendObject_DirectGLES backend;
const auto& rendererInfo = backend.GetRendererInfo().RendererGLInfo;
const auto& extensions = rendererInfo.Extensions;
EXPECT_EQ(rendererInfo.TargetGLVersion.Major, 3);
EXPECT_EQ(rendererInfo.TargetGLVersion.Minor, 3);
EXPECT_EQ(rendererInfo.TargetGLVersion.Major, 4);
EXPECT_EQ(rendererInfo.TargetGLVersion.Minor, 0);
EXPECT_EQ(rendererInfo.TargetGLVersion.Patch, 0);
EXPECT_NE(std::find(extensions.begin(), extensions.end(), MobileGL::E_GL_ARB_compute_shader),
@@ -242,6 +244,19 @@ TEST(DirectGLESSanity, BindingZeroClearsPreviousNativeTextureBinding) {
ASSERT_NE(frontendTextureObject, nullptr);
ASSERT_EQ(frontendTextureObject->GetExternalIndex(), frontendTexture);
// A texture with no image is incomplete, and an incomplete texture samples as (0, 0, 0, 1) -
// which DirectGLES expresses by leaving the native target unbound (see "sample a
// mipmap-incomplete texture as black"). This test is about the bind-0 clear, so the texture
// has to be complete enough to get bound in the first place: a format plus a level 0. At 1x1
// that single level is the whole mip chain, so it stays complete under any filter. The state
// is set directly rather than through glTexImage2D because the mock GLES table below wires
// only the binding entry points, not the upload path.
frontendTextureObject->SetInternalFormat(TextureInternalFormat::RGBA8);
MG_State::GLState::AsMipmapTexture(frontendTextureObject.get())
->AllocateStorage(TextureUploadTarget::Texture2D, 0, {{1, 1, 1}, 4});
ASSERT_FALSE(MG_State::GLState::SamplesAsIncompleteTexture(
frontendTextureObject.get(), frontendTextureObject->GetSamplerObject().get()));
auto& backendTexture = DirectGLES::TextureImpl::g_backendTextureObjects.GetOrCreate(frontendTextureObject);
backendTexture = MakeShared<DirectGLES::TextureImpl::BackendTextureObject>();
const GLuint backendTextureId = backendTexture->GetBackendTextureId();
@@ -397,13 +412,15 @@ TEST(DirectVulkanSanity, RenderPassExtentUsesSwapchainSizeOnlyForDefaultFramebuf
MobileGL::IntVec2(512, 512));
}
TEST(DirectVulkanSanity, AdvertisesVoxyRequiredRenderingExtensionsWithoutRaisingGLVersion) {
// See the DirectGLES twin above: the target version follows the advertised list, the extensions are
// what matter.
TEST(DirectVulkanSanity, AdvertisesVoxyRequiredRenderingExtensions) {
MobileGL::MG_Backend::DirectVulkan::BackendObject_DirectVulkan backend;
const auto& rendererInfo = backend.GetRendererInfo().RendererGLInfo;
const auto& extensions = rendererInfo.Extensions;
EXPECT_EQ(rendererInfo.TargetGLVersion.Major, 3);
EXPECT_EQ(rendererInfo.TargetGLVersion.Minor, 3);
EXPECT_EQ(rendererInfo.TargetGLVersion.Major, 4);
EXPECT_EQ(rendererInfo.TargetGLVersion.Minor, 0);
EXPECT_EQ(rendererInfo.TargetGLVersion.Patch, 0);
EXPECT_NE(std::find(extensions.begin(), extensions.end(), MobileGL::E_GL_ARB_compute_shader),
@@ -516,6 +533,50 @@ TEST(DirectGLESSanity, PreservesHostPerStageImageUniformLimits) {
EXPECT_EQ(params.MaxComputeImageUniforms, 5);
}
TEST(FragmentInterpolationCapabilities, PlumbsGLESAndBothVulkanPropertyPaths) {
using namespace MobileGL;
MG_External::GLESCapabilities glesCaps;
glesCaps.MinFragmentInterpolationOffset = -0.75f;
glesCaps.MaxFragmentInterpolationOffset = 0.625f;
glesCaps.FragmentInterpolationOffsetBits = 6;
MG_Backend::DirectGLES::BackendObject_DirectGLES glesBackend;
glesBackend.ApplyGLESCapabilitiesForTesting(glesCaps);
EXPECT_FLOAT_EQ(glesBackend.GetDynamicParameters().MinFragmentInterpolationOffset, -0.75f);
EXPECT_FLOAT_EQ(glesBackend.GetDynamicParameters().MaxFragmentInterpolationOffset, 0.625f);
EXPECT_EQ(glesBackend.GetDynamicParameters().FragmentInterpolationOffsetBits, 6);
VkPhysicalDeviceProperties properties{};
// A common Vulkan limit pair: max is one representable 4-bit step below 0.5.
properties.limits.minInterpolationOffset = -0.5f;
properties.limits.maxInterpolationOffset = 0.4375f;
properties.limits.subPixelInterpolationOffsetBits = 4;
MG_External::VulkanCapabilities vkCaps;
MG_Util::BackendLoader::FillInVulkanCapabilities(vkCaps, properties);
EXPECT_FLOAT_EQ(vkCaps.MinFragmentInterpolationOffset, -0.5f);
EXPECT_FLOAT_EQ(vkCaps.MaxFragmentInterpolationOffset, 0.4375f);
EXPECT_EQ(vkCaps.FragmentInterpolationOffsetBits, 4);
vkCaps.MinFragmentInterpolationOffset = -0.875f;
vkCaps.MaxFragmentInterpolationOffset = 0.75f;
vkCaps.FragmentInterpolationOffsetBits = 7;
MG_Backend::DirectVulkan::BackendObject_DirectVulkan vkBackend;
vkBackend.ApplyVulkanCapabilitiesForTesting(vkCaps);
EXPECT_FLOAT_EQ(vkBackend.GetDynamicParameters().MinFragmentInterpolationOffset, -0.875f);
EXPECT_FLOAT_EQ(vkBackend.GetDynamicParameters().MaxFragmentInterpolationOffset, 0.75f);
EXPECT_EQ(vkBackend.GetDynamicParameters().FragmentInterpolationOffsetBits, 7);
// Invalid/zero host data cannot under-advertise the OpenGL 4 minimums.
MG_External::VulkanCapabilities invalidCaps;
invalidCaps.MinFragmentInterpolationOffset = 0.0f;
invalidCaps.MaxFragmentInterpolationOffset = 0.0f;
invalidCaps.FragmentInterpolationOffsetBits = 0;
vkBackend.ApplyVulkanCapabilitiesForTesting(invalidCaps);
EXPECT_LE(vkBackend.GetDynamicParameters().MinFragmentInterpolationOffset, -0.5f);
EXPECT_FLOAT_EQ(vkBackend.GetDynamicParameters().MaxFragmentInterpolationOffset, 0.4375f);
EXPECT_EQ(vkBackend.GetDynamicParameters().FragmentInterpolationOffsetBits, 4);
}
TEST(DirectVulkanSanity, AdvertisesSubgroupOnlyWhenVulkanReportsUsableSupport) {
using namespace MobileGL;
@@ -638,6 +699,48 @@ TEST(GetterSanity, ClampsMaxVertexAttribsToCurrentValueStorageCapacity) {
MG_State::pGLContext.reset();
}
TEST(GetterSanity, ReportsFragmentInterpolationLimitsForFloatAndIntegerQueries) {
using namespace MobileGL;
auto previousContext = Move(MG_State::pGLContext);
auto previousBackend = Move(MG_Backend::pActiveBackendObject);
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
MG_Backend::DynamicBackendParameters params;
params.MinFragmentInterpolationOffset = -0.75f;
params.MaxFragmentInterpolationOffset = 0.4375f;
params.FragmentInterpolationOffsetBits = 6;
MG_Backend::pActiveBackendObject = MakeUnique<DynamicParameterBackend>(params);
GLfloat floatValue = 0.0f;
MG_Impl::GLImpl::GetFloatv(GL_MIN_FRAGMENT_INTERPOLATION_OFFSET, &floatValue);
EXPECT_FLOAT_EQ(floatValue, -0.75f);
MG_Impl::GLImpl::GetFloatv(GL_MAX_FRAGMENT_INTERPOLATION_OFFSET, &floatValue);
EXPECT_FLOAT_EQ(floatValue, 0.4375f);
MG_Impl::GLImpl::GetFloatv(GL_FRAGMENT_INTERPOLATION_OFFSET_BITS, &floatValue);
EXPECT_FLOAT_EQ(floatValue, 6.0f);
GLint intValue = 0;
MG_Impl::GLImpl::GetIntegerv(GL_MIN_FRAGMENT_INTERPOLATION_OFFSET, &intValue);
EXPECT_EQ(intValue, -1);
MG_Impl::GLImpl::GetIntegerv(GL_MAX_FRAGMENT_INTERPOLATION_OFFSET, &intValue);
EXPECT_EQ(intValue, 0);
MG_Impl::GLImpl::GetIntegerv(GL_FRAGMENT_INTERPOLATION_OFFSET_BITS, &intValue);
EXPECT_EQ(intValue, 6);
GLboolean boolValue = GL_FALSE;
MG_Impl::GLImpl::GetBooleanv(GL_MIN_FRAGMENT_INTERPOLATION_OFFSET, &boolValue);
EXPECT_EQ(boolValue, GL_TRUE);
MG_Impl::GLImpl::GetBooleanv(GL_MAX_FRAGMENT_INTERPOLATION_OFFSET, &boolValue);
EXPECT_EQ(boolValue, GL_TRUE);
MG_Impl::GLImpl::GetBooleanv(GL_FRAGMENT_INTERPOLATION_OFFSET_BITS, &boolValue);
EXPECT_EQ(boolValue, GL_TRUE);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Backend::pActiveBackendObject = Move(previousBackend);
MG_State::pGLContext = Move(previousContext);
}
TEST(GetterSanity, PerStageImageUniformQueriesMatchShaderCompilerLimits) {
using namespace MobileGL;
+18 -3
View File
@@ -15,6 +15,7 @@
#include <Config.h>
#include <MG_Backend/BackendObjects.h>
#include <MG_Backend/DirectGLES/Managers.h>
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
#include <MG_Impl/GLImpl/RenderState/GL_RenderState.h>
#include <MG_Impl/GLImpl/Sampler/GL_Sampler.h>
@@ -296,6 +297,18 @@ TEST_F(TextureTest, CopyTextureSubImage2DUsesNamedObjectAndRestoresBinding) {
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &boundTexture);
MG_Impl::GLImpl::BindTextureUnit(0, boundTexture);
// The copy is only allowed to reach the backend when the destination region fits the level and
// the read framebuffer can supply pixels, so the call has to be set up as a legal one.
MG_Impl::GLImpl::TextureStorage2D(namedTexture, 3, GL_RGBA8, 16, 16);
GLuint readFramebuffer = 0;
GLuint readTexture = 0;
MG_Impl::GLImpl::CreateFramebuffers(1, &readFramebuffer);
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &readTexture);
MG_Impl::GLImpl::TextureStorage2D(readTexture, 1, GL_RGBA8, 16, 16);
MG_Impl::GLImpl::NamedFramebufferTexture(readFramebuffer, GL_COLOR_ATTACHMENT0, readTexture, 0);
MG_Impl::GLImpl::BindFramebuffer(GL_READ_FRAMEBUFFER, readFramebuffer);
const auto boundBefore = MG_State::pGLContext->GetTextureUnitObject(0)
.GetBindingSlot(TextureTarget::Texture2D)
.GetBoundObject();
@@ -1867,11 +1880,13 @@ TEST_F(TextureTest, DirectGLESTreats2DArrayAsSupportedTextureTarget) {
EXPECT_TRUE(IsSupportedTextureTarget(TextureTarget::Texture2DArray));
EXPECT_TRUE(IsSupportedTextureTarget(TextureTarget::Texture3D));
EXPECT_TRUE(IsSupportedTextureTarget(TextureTarget::Texture2D));
// 1D and 1D-array are emulated as 2D / 2D-array (MapToBackendTextureTarget), matching
// SPIRV-Cross's ES 1D-as-2D shader emission; only rectangle textures stay unsupported.
// 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
// rectangle as a plain 2D - it is single-level and already clamps, so only the
// non-normalized coordinates differ and LowerRectImages handles those.
EXPECT_TRUE(IsSupportedTextureTarget(TextureTarget::Texture1D));
EXPECT_TRUE(IsSupportedTextureTarget(TextureTarget::Texture1DArray));
EXPECT_FALSE(IsSupportedTextureTarget(TextureTarget::TextureRectangle));
EXPECT_TRUE(IsSupportedTextureTarget(TextureTarget::TextureRectangle));
}
// 2D-array textures keep their layer count constant across mip levels (GL 3.3 §3.9);
@@ -9,6 +9,7 @@
#include "Loader.h"
#include "MG_Util/Types.h"
#include <Config.h>
#include <cmath>
#if defined(_WIN32)
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN 1
@@ -823,9 +824,19 @@ namespace MobileGL::MG_Util::BackendLoader {
if (std::strcmp(extension, "GL_EXT_texture_norm16") == 0) {
caps.SupportsNorm16Texture = true;
}
if (std::strcmp(extension, "GL_EXT_render_snorm") == 0) {
caps.SupportsRenderSnorm = true;
}
if (std::strcmp(extension, "GL_EXT_sRGB_write_control") == 0) {
caps.SupportsSrgbWriteControl = true;
}
if (std::strcmp(extension, "GL_EXT_texture_filter_anisotropic") == 0) {
caps.SupportsTextureFilterAnisotropy = true;
}
if (std::strcmp(extension, "GL_EXT_texture_border_clamp") == 0 ||
std::strcmp(extension, "GL_OES_texture_border_clamp") == 0) {
caps.SupportsTextureBorderClamp = true;
}
if (std::strcmp(extension, "GL_EXT_base_instance") == 0) {
caps.SupportsBaseInstance = true;
}
@@ -838,8 +849,14 @@ namespace MobileGL::MG_Util::BackendLoader {
if (std::strcmp(extension, "GL_NV_shader_noperspective_interpolation") == 0) {
caps.SupportsNoperspectiveInterpolation = true;
}
if (std::strcmp(extension, "GL_OES_shader_multisample_interpolation") == 0) {
caps.SupportsShaderMultisampleInterpolation = true;
}
}
}
caps.SupportsShaderMultisampleInterpolation =
caps.SupportsShaderMultisampleInterpolation || caps.GLESVersion.Major > 3 ||
(caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 2);
// Detect optional raster/color-mask entry points by whether they loaded. glColorMaski is GLES
// 3.2 core (no extension string), so pointer presence is the reliable signal for all of these.
@@ -900,6 +917,15 @@ namespace MobileGL::MG_Util::BackendLoader {
GLint maxColorAttachments = 8;
GLint maxClipDistances = 8;
GLint maxViewports = 16;
GLfloat minFragmentInterpolationOffset = -0.5f;
GLfloat maxFragmentInterpolationOffset = 0.4375f;
GLint fragmentInterpolationOffsetBits = 4;
// Core minimums of both APIs (GL 4.6 table 23.53, ES 3.1 table 20.40); the probe
// below only ever widens them.
GLint minProgramTextureGatherOffset = -8;
GLint maxProgramTextureGatherOffset = 7;
GLint maxPatchVertices = 32;
GLint maxTessGenLevel = 64;
glesFuncs.glGetFloatv(GL_ALIASED_LINE_WIDTH_RANGE, aliasedLineWidthRange);
glesFuncs.glGetFloatv(GL_SMOOTH_LINE_WIDTH_RANGE, smoothLineWidthRange);
glesFuncs.glGetFloatv(GL_SMOOTH_LINE_WIDTH_GRANULARITY, &smoothLineWidthGranularity);
@@ -919,6 +945,23 @@ namespace MobileGL::MG_Util::BackendLoader {
glesFuncs.glGetIntegerv(GL_MAX_INTEGER_SAMPLES, &maxIntegerSamples);
glesFuncs.glGetIntegerv(GL_MAX_SAMPLES, &maxSamples);
glesFuncs.glGetIntegerv(GL_MAX_SAMPLE_MASK_WORDS, &maxSampleMaskWords);
// MobileGL's sample-mask state only stores a single 32-bit word (see
// RenderState::SampleMaskValue) and SampleMaski_State() rejects any
// maskNumber other than 0. Advertising the real driver's value here (e.g.
// NVIDIA's GLES driver reports 2) makes glSampleMaski(1, ...) - which
// dEQP's per-case gluStateReset always issues up to GL_MAX_SAMPLE_MASK_WORDS -
// raise GL_INVALID_VALUE and aborts the whole glcts process after every
// single test case. 1 is a spec-legal value (the minimum required), so cap
// to what is actually implemented instead of forwarding the raw driver limit.
maxSampleMaskWords = std::min(maxSampleMaskWords, 1);
glesFuncs.glGetIntegerv(GL_MAX_PATCH_VERTICES, &maxPatchVertices);
glesFuncs.glGetIntegerv(GL_MAX_TESS_GEN_LEVEL, &maxTessGenLevel);
glesFuncs.glGetIntegerv(GL_MIN_PROGRAM_TEXTURE_GATHER_OFFSET, &minProgramTextureGatherOffset);
glesFuncs.glGetIntegerv(GL_MAX_PROGRAM_TEXTURE_GATHER_OFFSET, &maxProgramTextureGatherOffset);
// A driver that leaves the probe untouched (pre-ES 3.1, or an ignored enum) must not
// drag the advertised range below what GL 4.0 requires of us.
minProgramTextureGatherOffset = std::min(minProgramTextureGatherOffset, -8);
maxProgramTextureGatherOffset = std::max(maxProgramTextureGatherOffset, 7);
glesFuncs.glGetIntegerv(GL_MAX_TEXTURE_IMAGE_UNITS, &maxTextureImageUnits);
glesFuncs.glGetIntegerv(GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS, &maxVertexTextureImageUnits);
glesFuncs.glGetIntegerv(GL_MAX_COMPUTE_TEXTURE_IMAGE_UNITS, &maxComputeTextureImageUnits);
@@ -950,8 +993,35 @@ namespace MobileGL::MG_Util::BackendLoader {
glesFuncs.glGetIntegerv(GL_MAX_VIEWPORTS, &maxViewports);
glesFuncs.glGetIntegerv(GL_MAX_VIEWPORT_DIMS, maxViewportDims);
glesFuncs.glGetIntegerv(GL_VIEWPORT_SUBPIXEL_BITS, &viewportSubpixelBits);
if (caps.SupportsShaderMultisampleInterpolation && glesFuncs.glGetFloatv) {
const auto drainErrors = [&glesFuncs]() {
Bool hadError = false;
if (glesFuncs.glGetError) {
while (glesFuncs.glGetError() != GL_NO_ERROR) hadError = true;
}
return hadError;
};
// Isolate these optional queries from errors raised by preceding capability
// probes, then consume any query error so initialization never leaks it into
// the application's first glGetError call.
drainErrors();
glesFuncs.glGetFloatv(GL_MIN_FRAGMENT_INTERPOLATION_OFFSET, &minFragmentInterpolationOffset);
glesFuncs.glGetFloatv(GL_MAX_FRAGMENT_INTERPOLATION_OFFSET, &maxFragmentInterpolationOffset);
glesFuncs.glGetIntegerv(GL_FRAGMENT_INTERPOLATION_OFFSET_BITS, &fragmentInterpolationOffsetBits);
if (drainErrors()) {
MGLOG_W("Fragment interpolation limit query failed; using OpenGL minimums");
minFragmentInterpolationOffset = -0.5f;
maxFragmentInterpolationOffset = 0.4375f;
fragmentInterpolationOffsetBits = 4;
}
}
// Only legal to query once the extension has been seen in the loop above, hence not batched
// with the unconditional probes: on a driver without it this raises GL_INVALID_ENUM.
// Core from ES 3.2 on, whatever the extension string says.
if (caps.GLESVersion.Major > 3 || (caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 2)) {
caps.SupportsTextureBorderClamp = true;
}
if (caps.SupportsTextureFilterAnisotropy) {
GLfloat maxTextureMaxAnisotropy = 1.0f;
glesFuncs.glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &maxTextureMaxAnisotropy);
@@ -979,6 +1049,10 @@ namespace MobileGL::MG_Util::BackendLoader {
caps.MaxIntegerSamples = maxIntegerSamples;
caps.MaxSamples = maxSamples;
caps.MaxSampleMaskWords = maxSampleMaskWords;
caps.MaxPatchVertices = maxPatchVertices;
caps.MaxTessGenLevel = maxTessGenLevel;
caps.MinProgramTextureGatherOffset = minProgramTextureGatherOffset;
caps.MaxProgramTextureGatherOffset = maxProgramTextureGatherOffset;
caps.MaxTextureImageUnits = maxTextureImageUnits;
caps.MaxVertexTextureImageUnits = maxVertexTextureImageUnits;
caps.MaxComputeTextureImageUnits = maxComputeTextureImageUnits;
@@ -990,6 +1064,18 @@ namespace MobileGL::MG_Util::BackendLoader {
caps.MaxComputeWorkGroupInvocations = maxComputeWorkGroupInvocations;
caps.MaxShaderStorageBufferBindings = maxShaderStorageBufferBindings;
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.MaxUniformBlockSize = maxUniformBlockSize;
caps.MaxImageUnits = maxImageUnits;
@@ -1007,6 +1093,19 @@ namespace MobileGL::MG_Util::BackendLoader {
caps.ViewportBoundsRangeMin = viewportBoundsRange[0];
caps.ViewportBoundsRangeMax = viewportBoundsRange[1];
caps.ViewportSubpixelBits = viewportSubpixelBits;
caps.MinFragmentInterpolationOffset =
std::isfinite(minFragmentInterpolationOffset) && minFragmentInterpolationOffset <= -0.5f
? minFragmentInterpolationOffset
: -0.5f;
caps.MaxFragmentInterpolationOffset = 0.4375f;
caps.FragmentInterpolationOffsetBits = 4;
if (fragmentInterpolationOffsetBits >= 4 && std::isfinite(maxFragmentInterpolationOffset)) {
const Float requiredMaxOffset = 0.5f - std::ldexp(1.0f, -fragmentInterpolationOffsetBits);
if (maxFragmentInterpolationOffset >= requiredMaxOffset) {
caps.MaxFragmentInterpolationOffset = maxFragmentInterpolationOffset;
caps.FragmentInterpolationOffsetBits = fragmentInterpolationOffsetBits;
}
}
MGLOG_I(" GL_ALIASED_LINE_WIDTH_RANGE: [%.3f, %.3f]", caps.AliasedLineWidthRangeMin,
caps.AliasedLineWidthRangeMax);
MGLOG_I(" GL_SMOOTH_LINE_WIDTH_RANGE: [%.3f, %.3f]", caps.SmoothLineWidthRangeMin,
@@ -1031,9 +1031,20 @@ namespace MobileGL {
String GLESShadingLanguageVersionString;
Bool SupportsPersistentMapping = false;
Bool SupportsNorm16Texture = false;
// GL_EXT_render_snorm is present, so the signed-normalized formats are colour-renderable
// (and usable as multisample texture storage) rather than texture-only.
Bool SupportsRenderSnorm = false;
// GL_EXT_sRGB_write_control is present, so GL_FRAMEBUFFER_SRGB can be turned off.
// GLES has no such switch in core: writes into an sRGB attachment are ALWAYS encoded,
// while desktop GL leaves GL_FRAMEBUFFER_SRGB disabled by default and writes raw.
Bool SupportsSrgbWriteControl = false;
// GL_EXT_texture_filter_anisotropic is present, so sampler/texture
// anisotropy may be forwarded without raising GL_INVALID_ENUM in GLES.
Bool SupportsTextureFilterAnisotropy = false;
// GL_TEXTURE_BORDER_COLOR and GL_CLAMP_TO_BORDER: ES 3.2 core, or
// EXT/OES_texture_border_clamp before that. Without it every border-colour parameter
// raises INVALID_ENUM on the driver, so the syncs have to be gated on it.
Bool SupportsTextureBorderClamp = false;
// GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT of the host driver; only queried when the
// extension above is present, and left at 1.0 (no anisotropy) otherwise.
Float MaxTextureMaxAnisotropy = 1.0f;
@@ -1055,6 +1066,9 @@ namespace MobileGL {
// SPIRV-Cross's `#extension ... : require` would fail to compile and MobileGL falls back
// to stripping the NoPerspective decoration (smooth interpolation) via StripNoPerspectivePass.
Bool SupportsNoperspectiveInterpolation = false;
// GLES 3.2 core or GL_OES_shader_multisample_interpolation exposes
// interpolateAtOffset and the three fragment-offset limit queries.
Bool SupportsShaderMultisampleInterpolation = false;
// GL_RENDERER contains "ANGLE".
Bool IsAngleRenderer = false;
// GL_RENDERER contains both "ANGLE" and "llvmpipe".
@@ -1092,6 +1106,10 @@ namespace MobileGL {
Int MaxIntegerSamples = 1;
Int MaxSamples = 1;
Int MaxSampleMaskWords = 1;
Int MaxPatchVertices = 32;
Int MaxTessGenLevel = 64;
Int MinProgramTextureGatherOffset = -8;
Int MaxProgramTextureGatherOffset = 7;
Int MaxTextureImageUnits = 32;
Int MaxVertexTextureImageUnits = 32;
Int MaxComputeTextureImageUnits = 32;
@@ -1103,6 +1121,8 @@ namespace MobileGL {
Int MaxComputeWorkGroupInvocations = 128;
Int MaxShaderStorageBufferBindings = 8;
Int MaxTextureBufferSize = 65536;
// GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT; 1 means the offset is unconstrained.
Int TextureBufferOffsetAlignment = 1;
Int MaxUniformBufferBindings = 24;
Int MaxUniformBlockSize = 16384;
Int MaxImageUnits = 8;
@@ -1120,6 +1140,9 @@ namespace MobileGL {
Float ViewportBoundsRangeMin = 0.0f;
Float ViewportBoundsRangeMax = 0.0f;
Int ViewportSubpixelBits = 0;
Float MinFragmentInterpolationOffset = -0.5f;
Float MaxFragmentInterpolationOffset = 0.4375f;
Int FragmentInterpolationOffsetBits = 4;
};
} // namespace MG_External
@@ -9,6 +9,7 @@
#include "Loader.h"
#include <Config.h>
#include <cmath>
namespace MobileGL::MG_Util::BackendLoader {
namespace {
@@ -40,6 +41,25 @@ namespace MobileGL::MG_Util::BackendLoader {
return MaxSampleCountFromFlags(commonFlags);
}
void FillFragmentInterpolationLimits(MG_External::VulkanCapabilities& caps,
const VkPhysicalDeviceLimits& limits) {
caps.MinFragmentInterpolationOffset =
std::isfinite(limits.minInterpolationOffset) && limits.minInterpolationOffset <= -0.5f
? limits.minInterpolationOffset
: -0.5f;
caps.MaxFragmentInterpolationOffset = 0.4375f;
caps.FragmentInterpolationOffsetBits = 4;
const Int bits = static_cast<Int>(limits.subPixelInterpolationOffsetBits);
if (bits >= 4 && std::isfinite(limits.maxInterpolationOffset)) {
const Float requiredMaxOffset = 0.5f - std::ldexp(1.0f, -bits);
if (limits.maxInterpolationOffset >= requiredMaxOffset) {
caps.MaxFragmentInterpolationOffset = limits.maxInterpolationOffset;
caps.FragmentInterpolationOffsetBits = bits;
}
}
}
VulkanDynamicFunctions LoadVulkanDynamicFunctions(VkInstance instance) {
VulkanDynamicFunctions loaded{};
if (instance == VK_NULL_HANDLE) {
@@ -157,6 +177,8 @@ namespace MobileGL::MG_Util::BackendLoader {
caps.MaxComputeWorkGroupInvocations = static_cast<Int>(p.limits.maxComputeWorkGroupInvocations);
caps.MaxShaderStorageBufferBindings = static_cast<Int>(p.limits.maxDescriptorSetStorageBuffers);
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.MaxUniformBlockSize = static_cast<Int>(p.limits.maxUniformBufferRange);
caps.MaxImageUnits = static_cast<Int>(p.limits.maxPerStageDescriptorStorageImages);
@@ -171,6 +193,7 @@ namespace MobileGL::MG_Util::BackendLoader {
caps.ViewportBoundsRangeMin = p.limits.viewportBoundsRange[0];
caps.ViewportBoundsRangeMax = p.limits.viewportBoundsRange[1];
caps.ViewportSubpixelBits = static_cast<Int>(p.limits.viewportSubPixelBits);
FillFragmentInterpolationLimits(caps, p.limits);
VkPhysicalDeviceFeatures supportedFeatures{};
vkGetPhysicalDeviceFeatures(physicalDevice, &supportedFeatures);
@@ -247,6 +270,8 @@ namespace MobileGL::MG_Util::BackendLoader {
caps.MaxComputeWorkGroupInvocations = static_cast<Int>(properties.limits.maxComputeWorkGroupInvocations);
caps.MaxShaderStorageBufferBindings = static_cast<Int>(properties.limits.maxDescriptorSetStorageBuffers);
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.MaxUniformBlockSize = static_cast<Int>(properties.limits.maxUniformBufferRange);
caps.MaxImageUnits = static_cast<Int>(properties.limits.maxPerStageDescriptorStorageImages);
@@ -261,6 +286,7 @@ namespace MobileGL::MG_Util::BackendLoader {
caps.ViewportBoundsRangeMin = properties.limits.viewportBoundsRange[0];
caps.ViewportBoundsRangeMax = properties.limits.viewportBoundsRange[1];
caps.ViewportSubpixelBits = static_cast<Int>(properties.limits.viewportSubPixelBits);
FillFragmentInterpolationLimits(caps, properties.limits);
caps.SupportsWideLines = false;
// This helper only receives properties, not VkPhysicalDeviceFeatures. Leave optional
// stage writes disabled rather than inferring them from descriptor limits alone.
@@ -54,6 +54,8 @@ namespace MobileGL {
Int MaxComputeWorkGroupInvocations = 128;
Int MaxShaderStorageBufferBindings = 8;
Int MaxTextureBufferSize = 65536;
// GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT; 1 means the offset is unconstrained.
Int TextureBufferOffsetAlignment = 1;
Int MaxUniformBufferBindings = 24;
Int MaxUniformBlockSize = 16384;
Int MaxImageUnits = 8;
@@ -68,6 +70,9 @@ namespace MobileGL {
Float ViewportBoundsRangeMin = 0.0f;
Float ViewportBoundsRangeMax = 0.0f;
Int ViewportSubpixelBits = 0;
Float MinFragmentInterpolationOffset = -0.5f;
Float MaxFragmentInterpolationOffset = 0.4375f;
Int FragmentInterpolationOffsetBits = 4;
Bool SupportsWideLines = false;
// Storage-image descriptors are limited per stage by
// maxPerStageDescriptorStorageImages, but writes/atomics outside compute additionally
@@ -28,6 +28,7 @@ namespace MobileGL {
bool IsStencilFormatInternalFormat(TextureInternalFormat internalformat) {
switch (internalformat) {
case TextureInternalFormat::StencilIndex8:
case TextureInternalFormat::Depth24Stencil8:
case TextureInternalFormat::Depth32FStencil8:
case TextureInternalFormat::DepthStencil:
@@ -251,6 +251,8 @@ namespace MobileGL {
return TextureInternalFormat::Depth24Stencil8;
case GL_DEPTH32F_STENCIL8:
return TextureInternalFormat::Depth32FStencil8;
case GL_STENCIL_INDEX8:
return TextureInternalFormat::StencilIndex8;
case GL_DEPTH_COMPONENT:
return TextureInternalFormat::DepthComponent;
case GL_DEPTH_STENCIL:
@@ -286,6 +288,35 @@ namespace MobileGL {
return TextureInternalFormat::SRGB8;
case GL_COMPRESSED_SRGB_ALPHA:
return TextureInternalFormat::SRGB8Alpha8;
// BPTC and ETC2/EAC follow the same deviation as RGTC above, for the same reason: they
// are specific formats core GL requires (BPTC from 4.2, ETC2/EAC from 4.3) that this
// stack has no compressor for. INVALID_ENUM was never a legal answer for them, and
// uncompressed storage is the same trade the RGTC formats already take.
case GL_COMPRESSED_RGBA_BPTC_UNORM:
return TextureInternalFormat::RGBA8;
case GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM:
return TextureInternalFormat::SRGB8Alpha8;
case GL_COMPRESSED_RGB_BPTC_SIGNED_FLOAT:
case GL_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT:
return TextureInternalFormat::RGB16F;
case GL_COMPRESSED_RGB8_ETC2:
return TextureInternalFormat::RGB8;
case GL_COMPRESSED_SRGB8_ETC2:
return TextureInternalFormat::SRGB8;
case GL_COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2:
case GL_COMPRESSED_RGBA8_ETC2_EAC:
return TextureInternalFormat::RGBA8;
case GL_COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2:
case GL_COMPRESSED_SRGB8_ALPHA8_ETC2_EAC:
return TextureInternalFormat::SRGB8Alpha8;
case GL_COMPRESSED_R11_EAC:
return TextureInternalFormat::R8;
case GL_COMPRESSED_SIGNED_R11_EAC:
return TextureInternalFormat::R8Snorm;
case GL_COMPRESSED_RG11_EAC:
return TextureInternalFormat::RG8;
case GL_COMPRESSED_SIGNED_RG11_EAC:
return TextureInternalFormat::RG8Snorm;
case GL_ALPHA:
case GL_RED:
return TextureInternalFormat::Red;

Some files were not shown because too many files have changed in this diff Show More