Compare commits

...
9 Commits
Author SHA1 Message Date
BZLZHH f3d52faad4 [Perf] (MG_State, MG_Backend): stop glViewport from evicting a cached VkPipeline
RenderState kept one version counter for all render state, and DirectVulkan read
it in three places: the pipeline memo key, the SetupDrawSnapshot fast-path guard,
and that guard's store. So glViewport, glScissor, glBlendColor, glStencilMask,
glClearColor, glPolygonOffset, glLineWidth and the point-size family - none of
which can alter a VkPipeline, all of which an application changes between draws -
knocked the next draw off both fast paths and made it rebuild a pipeline lookup
that was already correct.

The counter is now split. m_version still moves on every state change, because
the draw snapshot really does depend on all of it. m_pipelineStateVersion moves
only for the state a backend bakes into a pipeline object, and it is what the
three DirectVulkan sites read.

The exclusion list is the eight VkDynamicState entries PipelineFactory declares
plus the state that is not pipeline state at all (the clear values, hints, the
point-size family, clamp read colour, the primitive restart index). glStencilFunc
is the one setter that had to be split rather than classified: Func is in the
pipeline payload but Ref and ValueMask are dynamic state, so it bumps the
pipeline version only when Func actually changes.

Capabilities are deliberately NOT in the exclusion list even though several look
like dynamic state: GL_FRAMEBUFFER_SRGB feeds the render-pass hash, depth and
stencil test feed drawUsesDepthStencil, and scissor test, blend, cull face,
polygon offset fill, primitive restart, colour logic op and rasterizer discard
all feed the pipeline payload.

Two smaller draw-path wins ride along, both removing work whose answer was
already in hand. UploadAndBindVertexStreams searched all 32 VAO attribute slots
for the SharedPtr matching a binding's buffer key, once per binding per draw -
but VertexInputStateFactory writes bindingBufferKeys[b] and
bindingAttributeLocations[b] from the same loop iteration, one binding per
attribute with no merging, so the attribute at that location IS the buffer, by
construction. UploadAndBindIndexBuffer round-tripped the element-array buffer's
raw pointer back through the GL name table on every indexed draw, costing a map
lookup and an atomic refcount pair, when the binding slot's SharedPtr was already
in scope forty lines above - where a comment says exactly that about the vertex
path.

Behaviour-neutral by construction and verified as such: a 13355-case subset of
GL30-GL45 covering viewport, scissor, blend, stencil, depth, polygon offset,
clear, multisample, cull, logic op, line width and point state, plus the whole
direct_state_access suite, is identical before and after on both backends - in
the failure list and in the crashed-case set. direct_state_access stays at
Espryt 370/371 and Magma 371/371.
2026-08-05 12:49:54 -04:00
BZLZHH ba81ee114e [Feat] (MG_Backend, MG_Impl, MG_Util): attach one layer of any layered texture on DirectVulkan
Whether a backend can attach a single layer of a texture to a framebuffer was one
Bool, so it could only give the most conservative answer any target needed.
DirectVulkan therefore declined every layer of every target and
direct_state_access.framebuffers_texture_layer_attachment failed with 542
messages across four targets.

The three ways a GL layer maps onto Vulkan are independent capabilities, so the
flag becomes a per-TextureTarget mask. A 2D or 2D multisample array layer IS a
VkImage array layer and needed nothing but the gate opened. A cube map array is
one 2D image with arrayLayers = 6 * cubeCount and CUBE_COMPATIBLE, which is a
shape VkTextureManager simply did not have - it is declined softly when the depth
is not a whole number of cubes or the level is not square, because that function's
Bool return exists for unrepresentable shapes and asserting there would abort on
ordinary input, GL_PROXY_TEXTURE_CUBE_MAP_ARRAY above all. A 3D texture's layer is
a z slice, which needs a 2D-array-compatible image and a per-slice clear, because
vkCmdClearColorImage cannot address a subset of a 3D image's slices - a render
pass whose only content is its LOAD_OP_CLEAR can, since its attachment is a 2D
view over that one slice.

VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT is asked for per format and withdrawn per
format, mirroring the MUTABLE_FORMAT pattern already in this file: the capability
is per format+usage, so a single global probe answers a different question than
the one the frontend goes on to ask. Losing it costs per-slice attachment for that
format; failing creation would lose the texture.

Three things found on the way that are not the headline:

glFramebufferTextureLayer, the non-DSA twin, had no gate at all and additionally
refused cube map arrays that GL 4.5 requires it to accept. GL 4.6 core 9.2.8 makes
the two entry points equivalent, so they now decline in the same places - leaving
one ungated is what let an unrepresentable attachment reach the renderer.

ComputeFullMipLevelCount takes max(x, y, z), and for every array shape z is the
layer count rather than a mip-able axis, so a 4x4 array with 192 layers asked for
six mip levels on an image whose legal maximum is three
(VUID-VkImageCreateInfo-mipLevels-00958). Only the image's own extent can bound
it. lavapipe had been letting that through.

A layered GL clear queues layerCount = depth, which is illegal for a
VK_IMAGE_TYPE_3D image (VUID-vkCmdClearColorImage-baseArrayLayer-01472 pins it to
0/1, read as the whole mip level) and the old code passed it straight through.

Takes framebuffers_texture_layer_attachment green on DirectVulkan, so the whole
direct_state_access suite is 371/371 there; Espryt stays 370/371, the remaining
case being the fp64 one it declines by design.

Known and deliberately not fixed here, with a FIXME at the site:
KHR-GL44/45/46.geometry_shader.layered_framebuffer.clear_call_support now fails on
DirectVulkan - a layered clear of a 3D texture reads back zeros. Those cases exist
only in the GL44+ lists, above the 4.0 this backend reports. An A/B of a 6935-case
subset (cube map array, texture storage, framebuffer, 3D, the full DSA suite and
the GL33 texture group) is otherwise clean on both backends: 16 cases fixed and
none broken on Espryt, 15 fixed and those 2 broken on Magma, and zero difference
anywhere at GL 4.0 or below. The FIXME records which causes were already ruled out
by bisection so the next reader does not repeat them.
2026-08-05 12:07:04 -04:00
BZLZHH c8c7b19579 [Feat] (MG_Backend, MG_Util): give DirectVulkan GL's provoking vertex
Vulkan's built-in convention is "provoking vertex first"; GL's default is
LAST_VERTEX_CONVENTION, and GL derives both flat shading and the transform
feedback vertex order from it. DirectVulkan had no way to say so, which is why
direct_state_access.queries_functional failed on a value with nothing in its log
- the primitives came back counted against a strip recorded in the wrong vertex
order.

VK_EXT_provoking_vertex is now enabled when present, and the mode is a hashed
field of the pipeline payload rather than dynamic state, because it is baked into
VkPipelineRasterizationStateCreateInfo: two draws differing only in it must not
collide on one cached VkPipeline, or whichever mode built first would stick for
the rest of the frame. The pNext is chained only when the mode is not Vulkan's
default, so a device without the extension produces a byte-identical
VkGraphicsPipelineCreateInfo to before.

Two carve-outs, both measured rather than reasoned:

A geometry shader already emits its triangles in GL's vertex order, so asking for
LAST rotates them a second time and transform_feedback.geometry reads back the
wrong vertices. The mode is one pipeline bit and the input-assembler path wants
the opposite, so the two cannot both be satisfied: a program that runs a geometry
shader and captures transform feedback keeps Vulkan's own convention. That test
is read off the program's own shader list, not
programObj.rasterizationProducerStage - the latter is filled by the clip-fixup
analysis, which does not run for every program and reads Unknown for exactly the
programs this guard exists to catch. Both halves are link-time facts folded into
programObj.hash, so no pipeline memo can hand back one built for the other mode;
keying on IsTransformFeedbackActive() instead would be a live bug, since neither
memo key moves on glBeginTransformFeedback.

transformFeedbackPreservesProvokingVertex is deliberately not requested. It buys
nothing here - the capture order queries_functional needs comes from
provokingVertexLast alone - and leaving it off keeps
VUID-VkGraphicsPipelineCreateInfo-topology-04884 disarmed, so a TRIANGLE_FAN
pipeline may take LAST on any device.

The blit pipeline routes through the same selector: it has no flat varying and no
capture, but on a device without provokingVertexModePerPipeline a blit left on
FIRST inside a render pass whose draws are LAST is an illegal mix.

Per the POST rule the new extension gets rows for provokingVertexLast and for the
two properties that change what MobileGL can promise.

Fixes queries_functional on Magma (370/371). An A/B over a 976-case transform
feedback / geometry shader / layered rendering subset of GL30-GL45 is otherwise
identical on both backends and additionally takes 14 geometry_shader rendering
and layered_rendering cases from failing to passing on Magma.
2026-08-05 10:04:30 -04:00
BZLZHH 0e7692251d [Feat] (MG_State, MG_Impl, MG_Util): store a compressed texture image and hand it back
glCompressedTexImage2D rejected every internalformat with GL_INVALID_ENUM, so
direct_state_access.textures_get_image threw at its first compressed call and
reported InternalError with nothing in the log at all - the uncompressed half of
the case had already passed.

The compressed bytes are now kept verbatim, in a side-channel beside the texel
shadow rather than in place of it. That placement is the load-bearing decision:
both backends pair MapMipmapData with GetMipmapByteSize while sizing their copy
regions from GetMipmapTexelSize, and DirectGLES additionally divides the byte
size by the texel count to recover bytes-per-texel, so putting 16 bytes where a
4x4 RGBA8 extent says 64 would be an out-of-bounds read on both. The texel
storage therefore stays uncompressed and correctly sized - the image samples as
zeros, which is the same deviation the RGTC/BPTC/ETC2 arms of
ConvertGLEnumToTextureInternalFormat already document - while
glGetCompressedTexImage returns the image *as stored*, which GL 4.6 core 8.11
requires and which no re-encode could satisfy byte for byte. Nothing ever hands
the compressed bytes to GLES or Vulkan, so the shadow is authoritative rather
than potentially stale, which is why the readback never asks a backend.

The accepted set is exactly the RGTC/BPTC/ETC2-EAC formats core GL requires, and
it is deliberately the same set ConvertGLEnumToTextureInternalFormat can back
with uncompressed storage, so the upload can never accept a format whose texel
shadow it cannot allocate. imageSize is checked against the block arithmetic,
which is also what keeps the copy in bounds.

Three things the shape depends on. AllocateStorage clears the compressed tag, so
a glTexImage2D or glTexStorage2D over the level un-compresses it - without that,
textures_compressed_subimage would flip branches and start asking for data
MobileGL cannot produce. GL_TEXTURE_COMPRESSED and
GL_TEXTURE_COMPRESSED_IMAGE_SIZE are answered per level rather than per texture,
because a compressed internalformat handed to glTexImage2D resolves to
uncompressed storage and must keep reading as uncompressed. And
GL_TEXTURE_INTERNAL_FORMAT now reports the compressed token for such a level, or
it would claim GL_RGBA8 while GL_TEXTURE_COMPRESSED said true.

Still rejected on purpose: glCompressedTexImage1D/3D and every
glCompressedTexSubImage*, which caps the blast radius.

Fixes textures_get_image on both backends (Espryt 370/371, Magma 369/371). A/B
over a 1210-case compressed/texture-storage/texture-view/buffer-storage subset of
KHR-GL45 is identical before and after on both backends but for
get_texture_sub_image.errors_test, which stops throwing and fails on a value
instead.
2026-08-05 09:40:29 -04:00
BZLZHH 34f09291da [Feat] (MG_State, MG_Backend, MG_Util): feed a 64-bit vertex attribute on DirectVulkan
glVertexAttribLFormat validated its arguments and then refused unconditionally
with "64-bit vertex attributes are not supported", so
direct_state_access.vertex_arrays_attribute_format failed every GL_DOUBLE
subcase on both backends - the format never landed, the draw fetched whatever
the attribute held before, and the captured values came back as reinterpreted
garbage.

The attribute is now real state. IsLong is its own bit rather than being
inferred from Float64, because glVertexAttribFormat(GL_DOUBLE) also reads
doubles - it just asks for them converted to float - so the type alone cannot
tell the two apart. It participates in the format comparison, so an L-format
call over a plain one still bumps the version, and glVertexAttribPointer clears
it inside the mutation block so the clear and the bump stay atomic.
GL_VERTEX_ATTRIB_ARRAY_LONG stops being hardcoded false, and the pname is now
accepted by the attribute queries at all.

Support is detected, never assumed. SupportsFloat64VertexAttributes comes from
VkPhysicalDeviceFeatures::shaderFloat64 on DirectVulkan and is false on
DirectGLES - not a driver question there and never will be, since ES has no
GL_DOUBLE vertex format and ESSL has no fp64 type to consume one with. A backend
without it declines in the entry point, with the GL error and a log line naming
the reason, rather than accepting state no draw could honour. Both cases get a
DriverPost row so the loss is named at startup instead of at draw setup.

On DirectVulkan the attribute deliberately does not use VK_FORMAT_R64*_SFLOAT:
those are optional and lavapipe advertises zero features for all four of them.
It is fetched as its 32-bit word pair (R32G32_UINT / R32G32B32A32_UINT) and
bitcast back to double in the shader by a new SPIR-V pass, which is bit-exact
and needs no format capability at all. The pass re-declares the input as uvec2 /
uvec4, demotes the original variable to a Private global and seeds it once at
the top of the entry point, so every existing load keeps its id and its double
type and no other instruction is rewritten. Both halves branch on nothing but
"is this attribute long", so they cannot disagree - and if the pass ever fails,
the assertion fires rather than letting a UINT format sit under a double input.
The pointer types are all created before any variable that names them and the
demoted variable is moved after them, since the types-and-variables section may
not forward-reference a type.

dvec3/dvec4 are declined rather than fetched wrong: six or eight uint32
components have no single VkFormat, and GL spreads such an input over two
attribute locations, which the location-per-index model here does not express.

Fixes vertex_arrays_attribute_format on Magma (369/371). On Espryt it stays
failing, now as a detected and explained decline rather than a blanket refusal.
2026-08-05 08:49:23 -04:00
BZLZHH 3b65e646e1 [Fix] (MG_Backend): give every colour attachment its own backend slot on DirectGLES
ES only accepts glDrawBuffers bufs[s] == GL_COLOR_ATTACHMENTs, so a desktop
glDrawBuffer(GL_COLOR_ATTACHMENT3) cannot be expressed directly and DirectGLES
compacts: it physically relocates the draw buffer's image onto backend point 0 so
ES's output-0-to-attachment-0 rule lands on the right image. The clears were
therefore always correct. The read side was not.

GetBackendAttachmentType derived the attachment-to-point map by searching the
draw-buffer array and falling back to the identity point for anything it did not
find. That derivation is not injective against the compaction: after clearing
attachments 0..7 one at a time, every one of them has been relocated onto point 0
in turn, so a later glReadBuffer(GL_COLOR_ATTACHMENT0) - not a draw buffer any
more - takes the identity fallback to point 0 and reads attachment 7's image.
Hence the single mismatch, 0.875 where 0 was expected: 7/8 is attachment 7's clear
colour.

The map is now stored state rather than a re-derivation, and kept a permutation:
a draw buffer takes the point ES forces on it, everything else keeps its identity
point when that point survived, and an attachment evicted from its identity point
is parked on the lowest free one so it stays addressable for glReadBuffer and
blits. With identity draw buffers nothing moves and not one extra GL call is
issued, which is what keeps ordinary rendering untouched.

Two things the permutation depends on. The attachment loop now detaches a colour
point whose frontend owner is empty - SyncAttachmentObject only ever attaches, so
without this a point handed to an empty attachment would still hold the previous
owner's image and hand it back. And QueryReadColorAttachmentInternalFormat asked
GL_COLOR_ATTACHMENT0 for the format it sizes the multisample-resolve scratch
renderbuffer from; it now asks the point the read buffer actually names, since
that is only CA0 when the map happens to be identity.

Fixes framebuffers_read_draw_buffer on Espryt. A 5677-case readback and
framebuffer subset of GL30-33 stays at zero failures on both backends.
2026-08-05 08:24:01 -04:00
BZLZHH 25b9370815 [Fix] (MG_Backend): stop a renderbuffer blit reading a freed image layout
VkRenderPassManager kept m_renderbufferResources on FastSTL's open-addressing
UnorderedMap while BlitFramebuffer caches a raw pointer into one of its elements -
ResolveColorBlitBinding stores &rbResource->layout - and then calls
MaterializePendingClearForRenderbuffer, which looks that same resource up again.
FastSTL's operator[] runs its load-factor check before find_key and reallocates
the whole bucket array when occupancy crosses it, so even a plain lookup relocates
every element; erase only tombstones and never lowers the occupancy, so the
doubling keeps firing. After a relocation the cached pointer names freed storage
still holding the pre-clear VK_IMAGE_LAYOUT_UNDEFINED, BlitFramebuffer takes its
"source image layout is undefined" early return, and the blit is silently dropped
- glReadPixels then returns the zero-filled fresh allocation.

That is why the failures looked arbitrary: which iteration breaks is pure
arithmetic on the table's occupancy, and the observed set (GL_R8 at k=0,1,3,7,
GL_R16 at k=6, GL_RG16 at k=4) is exactly the doubling ladder. Padding the map
with unrelated live renderbuffers moves the failures to the positions the model
predicts and every previously failing format then passes, so nothing else hides
behind it.

Reordering the materialize ahead of the resolves - the fix ReadPixels got, see the
note at its call site - does not cover this, because BlitFramebuffer resolves two
bindings and the second resolve still runs after the first pointer is taken. The
depth blit, GetOrCreateRenderPass's depthRenderbufferResource and
ReadDepthStencilPixels cache the same kind of pointer, so the invariant belongs in
the container rather than in a per-call-site ordering rule. m_textureResources was
already node-based for exactly this reason; this is the map that was left behind.

Fixes renderbuffers_storage_multisample on DirectVulkan.
2026-08-05 08:24:01 -04:00
BZLZHH 4ce808b9f2 [Feat] (MG_State, MG_Impl, MG_Backend): let a bound program pipeline actually draw
The pipeline object bookkeeping landed already - names, stage slots, queries -
but nothing consumed it. Every draw asked the context for the current program,
got null because a pipeline is used with program zero, and drew nothing;
glCreateShaderProgramv was still a stub returning zero, so
direct_state_access.program_pipelines_functional could not even build its stage
programs and reported InternalError on both backends.

glCreateShaderProgramv is written as the exact call sequence the spec defines it
to be, with one deviation that matters: the link goes straight to
ProgramObject::Link(false) rather than through LinkProgram, because LinkProgram
injects a default fragment shader into a program that has none - correct for a
whole program, wrong for a separable vertex-stage one whose fragment stage comes
from the pipeline. glDetachShader defers removal to the next link, so the program
keeps the shader object it was built from while correctly no longer reporting it
attached. GL_PROGRAM_SEPARABLE joins glProgramParameteri and glGetProgramiv.

Everything downstream of a draw - both backends, the uniform plumbing, the draw
validation - is written against one linked program, so rather than teach all of
it about stages, the pipeline is flattened: GetProgramForDraw() composites the
stage programs' shaders into a single hidden program object and caches it against
a signature of each stage program's lifetime id and link generation, so it is
rebuilt exactly when a stage or a stage's link changes. The composite carries no
GL name - it must not answer glIsProgram, and it must not consume a name the
application could be handed.

Uniform entry points get their own resolver rather than sharing that one:
glUniform* addresses the pipeline's active program, not the composited draw
program. GL_CURRENT_PROGRAM still reads the program in use, which is zero here.

Fixes program_pipelines_functional on both backends.
2026-08-05 07:20:42 -04:00
BZLZHH 5545d31c37 [Feat] (MG_Backend, MG_Util): give a cube map array real storage on DirectGLES
TextureCubeMapArray was missing from every storage and upload switch in the
DirectGLES texture sync, so a cube map array reached the driver with no storage
at all - and from the glFramebufferTextureLayer branch, so attaching one of its
layers fell through to glFramebufferTexture2D and raised INVALID_ENUM. Every
GL_TEXTURE_CUBE_MAP_ARRAY colour check in
direct_state_access.framebuffers_texture_layer_attachment read nothing.

ES 3.2 has GL_TEXTURE_CUBE_MAP_ARRAY natively and it stores exactly like a 2D
array whose depth is six times the cube count, so each switch gains the case
beside Texture2DArray and nothing else changes. 1D arrays join the layer branch
for the same reason - their backend image is a 2D array.

Per the POST rule the new GLES dependency gets a capability
(SupportsTextureCubeMapArray, ES 3.2 core or EXT/OES_texture_cube_map_array) and
a DriverPost row saying what a user loses without it.

Takes framebuffers_texture_layer_attachment from failing to passing on Espryt. It
still fails on DirectVulkan, which declines a layered attachment outright.
2026-08-05 06:58:00 -04:00
51 changed files with 1984 additions and 171 deletions
+1
View File
@@ -190,6 +190,7 @@ set(SOURCE_FILES
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecomposeWorkgroupVec3Pass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecoratePositionInvariantPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp
+29 -1
View File
@@ -358,7 +358,35 @@ namespace MobileGL {
// 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;
// Which layered texture targets this backend can attach ONE layer of to a framebuffer
// and then really clear, render and read back that layer. Bit (1u << TextureTarget) is
// set for each supported target. Deliberately per target rather than one flag: the three
// ways a GL layer maps onto Vulkan are independent capabilities. A 2D or 2D multisample
// array layer IS a VkImage array layer and needs nothing extra; a 3D texture's layer is
// a z slice, which needs a 2D-array-compatible image and a per-slice clear that
// vkCmdClearColorImage cannot express; a cube map array needs an image shape and the
// imageCubeArray feature before it can be attached at any layer at all. Defaults to 0 so
// a backend that never sets it gets the conservative answer.
Uint32 PerLayerFramebufferAttachmentTargets = 0;
static constexpr Uint32 PerLayerFramebufferAttachmentBit(TextureTarget target) {
return (static_cast<Int>(target) >= 0 &&
static_cast<Int>(target) < static_cast<Int>(TextureTarget::TextureTargetCount))
? (1u << static_cast<Uint32>(target))
: 0u;
}
Bool SupportsPerLayerFramebufferAttachment(TextureTarget target) const {
const Uint32 bit = PerLayerFramebufferAttachmentBit(target);
return bit != 0 && (PerLayerFramebufferAttachmentTargets & bit) != 0;
}
// Whether glVertexAttribLFormat / glVertexArrayAttribLFormat can be honoured, i.e.
// whether a 64-bit vertex attribute can actually reach a shader unconverted. Detected,
// never assumed: DirectVulkan needs VkPhysicalDeviceFeatures::shaderFloat64 (the
// attribute travels as its 32-bit word pair, so no VK_FORMAT_R64* is required, but the
// bitcast result is Float64); DirectGLES can never have it, ESSL having no fp64 type at
// all. Defaults to false so a backend that never sets it gets the conservative answer.
Bool SupportsFloat64VertexAttributes = false;
SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024;
Uint32 SubgroupSize = 0;
Uint32 SubgroupSupportedStages = 0;
@@ -1112,8 +1112,28 @@ namespace MobileGL::MG_Backend::DirectGLES {
// 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;
// SyncAttachmentObject (Managers.cpp, the glFramebufferTextureLayer branch) routes exactly
// five upload targets 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. The cube map array is the one target that also needs
// ES-level support before it has any storage to attach.
m_dynamicParameters.PerLayerFramebufferAttachmentTargets = 0;
if (DirectGLES::g_GLESFuncs.glFramebufferTextureLayer != nullptr) {
using DynParams = MG_Backend::DynamicBackendParameters;
m_dynamicParameters.PerLayerFramebufferAttachmentTargets |=
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture3D) |
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture1DArray) |
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture2DArray) |
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture2DMultisampleArray);
if (m_GLESCapabilities.SupportsTextureCubeMapArray) {
m_dynamicParameters.PerLayerFramebufferAttachmentTargets |=
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::TextureCubeMapArray);
}
}
// Not a driver question and never will be: OpenGL ES has no double-precision vertex format
// and ESSL has no fp64 type to consume one with, so a 64-bit vertex attribute has nowhere to
// land on this backend regardless of what the driver underneath happens to support.
m_dynamicParameters.SupportsFloat64VertexAttributes = false;
m_dynamicParameters.MaxDrawBuffers = m_GLESCapabilities.MaxDrawBuffers;
m_dynamicParameters.MaxColorAttachments = m_GLESCapabilities.MaxColorAttachments;
m_dynamicParameters.MaxClipDistances = m_GLESCapabilities.MaxClipDistances;
+18 -8
View File
@@ -753,7 +753,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
const auto& program = MG_State::pGLContext->GetCurrentProgram();
const auto& program = MG_State::pGLContext->GetProgramForDraw();
if (!program) return;
const auto& vao = MG_State::pGLContext->GetBoundVertexArray();
@@ -1377,7 +1377,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_backendProgramObjects.CollectGarbageIfNeeded();
SamplerImpl::g_backendSamplerObjects.CollectGarbageIfNeeded();
auto& currentProgram = MG_State::pGLContext->GetCurrentProgram();
auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
if (!currentProgram || !currentProgram->GetLinkStatus()) {
g_GLESFuncs.glUseProgram(0);
g_lastUsedBackendProgramId = 0;
@@ -1546,7 +1546,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Frontend target the current program samples at a given unit; resolves an
// aliased native binding when two real textures compete for it (see below).
// Only consulted on a conflict, so the ordinary unit costs nothing.
const auto& currentProgram = MG_State::pGLContext->GetCurrentProgram();
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
const auto sampledTargetForUnit = [&currentProgram](Int unit) {
if (!currentProgram || !currentProgram->GetLinkStatus()) {
return TextureTarget::Unknown;
@@ -1679,7 +1679,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// this as much as draws do — e.g. Flywheel's cull shader reads the
// _FlwFrameUniforms block and the _flw_depthPyramid sampler.
static void BindCurrentProgramWithResources() {
const auto& currentProgram = MG_State::pGLContext->GetCurrentProgram();
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
if (currentProgram && currentProgram->GetLinkStatus()) {
#ifdef TRACY_ENABLE
ZoneScopedNC("BindCurrentProgram", TRACY_ZONECOLOR_BACKEND);
@@ -1865,7 +1865,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
static SharedPtr<PrgramImpl::BackendProgramObjectImpl> GetCurrentBackendProgram() {
const auto& currentProgram = MG_State::pGLContext->GetCurrentProgram();
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
if (!currentProgram || !currentProgram->GetLinkStatus()) {
return nullptr;
}
@@ -2012,7 +2012,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
TextureImpl::SyncImageTextureBindings();
PrgramImpl::SyncCurrentProgram();
const auto& currentProgram = MG_State::pGLContext->GetCurrentProgram();
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
if (!currentProgram || !currentProgram->GetLinkStatus()) {
g_GLESFuncs.glUseProgram(0);
PrgramImpl::g_lastUsedBackendProgramId = 0;
@@ -2511,9 +2511,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
static GLenum QueryReadColorAttachmentInternalFormat() {
GLint attachmentType = 0;
GLint attachmentName = 0;
g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
// Ask the point the backend read buffer actually names, not COLOR_ATTACHMENT0. The colour map
// in BackendFramebufferObject is a permutation, so the read attachment's image only sits at
// CA0 when that map is identity; querying CA0 unconditionally would size the resolve
// renderbuffer from a different attachment's format and either convert wrongly or fail the
// blit outright.
GLint readBuffer = GL_COLOR_ATTACHMENT0;
g_GLESFuncs.glGetIntegerv(GL_READ_BUFFER, &readBuffer);
if (readBuffer < GL_COLOR_ATTACHMENT0 || readBuffer > GL_COLOR_ATTACHMENT31) {
readBuffer = GL_COLOR_ATTACHMENT0;
}
g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_READ_FRAMEBUFFER, static_cast<GLenum>(readBuffer),
GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE, &attachmentType);
g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_READ_FRAMEBUFFER, static_cast<GLenum>(readBuffer),
GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME, &attachmentName);
if (attachmentName == 0) {
return 0;
+174 -15
View File
@@ -1305,6 +1305,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
m_syncedAttributeVersions[attribIndex].BufferVersion;
if (!needsSyncFormat && !needsSyncBuffer) continue;
// Defence in depth. The frontend already declines glVertexAttribLFormat on this
// backend (SupportsFloat64VertexAttributes is false - ES has no GL_DOUBLE vertex
// format and ESSL has no fp64 type), so IsLong should never arrive here; if it ever
// did, passing GL_DOUBLE to glVertexAttribPointer would only raise GL_INVALID_ENUM on
// the real driver. Disabling rather than merely skipping matters: becoming long bumps
// FormatVersion, not SwitchVersion, so the enable/disable block above will not run
// again and an already-enabled array would stay enabled with no pointer and no
// ARRAY_BUFFER binding - which ES 3.1+ makes an INVALID_OPERATION at draw.
if (attrib.IsLong) {
MGLOG_E("DirectGLES: vertex attribute %u is a 64-bit (GL_DOUBLE) array, which this "
"backend cannot feed - disabling the array",
attribIndex);
g_GLESFuncs.glDisableVertexAttribArray(attribIndex);
continue;
}
if (!BindAttributeBuffer(attrib)) {
continue;
}
@@ -1366,6 +1382,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
continue;
}
// Same reason as SyncToBackend: there is no ES vertex format for a 64-bit array, and
// this path only ever reaches glVertexAttribPointer/IPointer.
if (attrib.IsLong) {
g_GLESFuncs.glDisableVertexAttribArray(attribIndex);
continue;
}
const auto* clientData = reinterpret_cast<const Uint8*>(attrib.Offset);
const SizeT elementSize = GetAttributeByteSize(attrib.Type, attrib.Size, attrib.IsBgra);
if (!clientData || elementSize == 0 || attrib.Size <= 0) {
@@ -1888,6 +1911,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
break;
case TextureTarget::Texture3D:
case TextureTarget::Texture2DArray:
// ES 3.2 has GL_TEXTURE_CUBE_MAP_ARRAY natively and it stores exactly
// like a 2D array whose depth is 6 * the cube count.
case TextureTarget::TextureCubeMapArray:
g_GLESFuncs.glTexImage3D(
glUploadTarget, static_cast<GLint>(level), (GLint)glInternalFormat,
static_cast<GLsizei>(uploadSize.x()), static_cast<GLsizei>(uploadSize.y()),
@@ -1965,6 +1991,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
break;
case TextureTarget::Texture3D:
case TextureTarget::Texture2DArray:
case TextureTarget::TextureCubeMapArray:
g_GLESFuncs.glTexStorage3D(target, static_cast<GLsizei>(mipmapCount), glInternalFormat,
static_cast<GLsizei>(storageSize.x()),
static_cast<GLsizei>(storageSize.y()),
@@ -2017,6 +2044,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
break;
case TextureTarget::Texture3D:
case TextureTarget::Texture2DArray:
case TextureTarget::TextureCubeMapArray:
g_GLESFuncs.glTexSubImage3D(
glUploadTarget, static_cast<GLint>(level), 0, 0, 0,
static_cast<GLsizei>(uploadSize.x()),
@@ -2080,7 +2108,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
break;
}
case TextureTarget::Texture3D:
case TextureTarget::Texture2DArray: {
case TextureTarget::Texture2DArray:
case TextureTarget::TextureCubeMapArray: {
g_GLESFuncs.glTexImage3D(
glUploadTarget, static_cast<GLint>(level), (GLint)glInternalFormat,
static_cast<GLsizei>(uploadSize.x()),
@@ -2180,6 +2209,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
break;
case TextureTarget::Texture3D:
case TextureTarget::Texture2DArray:
// ES 3.2 has GL_TEXTURE_CUBE_MAP_ARRAY natively and it stores exactly
// like a 2D array whose depth is 6 * the cube count.
case TextureTarget::TextureCubeMapArray:
g_GLESFuncs.glTexSubImage3D(glUploadTarget, static_cast<GLint>(level), 0, 0, 0,
static_cast<GLsizei>(uploadSize.x()),
static_cast<GLsizei>(uploadSize.y()),
@@ -2523,6 +2555,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
// Identity until a non-identity draw-buffer array forces a relocation. A framebuffer
// that is never draw-bound never runs the recompute, so the table has to start out
// matching what the attachment loop will physically do.
for (Uint i = 0; i < MAX_COLOR_ATTACHMENT_SLOTS; ++i) {
m_backendColorSlots[i] = GL_COLOR_ATTACHMENT0 + i;
}
g_GLESFuncs.glGenFramebuffers(1, &m_backendFBOId);
if (m_backendFBOId == 0) {
MGLOG_E("Failed to generate framebuffer object.");
@@ -2596,6 +2634,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
std::fill(std::begin(m_frontendDrawBuffers), std::end(m_frontendDrawBuffers),
FramebufferAttachmentType::Unknown);
std::fill(std::begin(m_backendDrawBuffers), std::end(m_backendDrawBuffers), GL_NONE);
// NOTE: this does NOT empty the backend ES framebuffer - m_backendFBOId keeps every
// attachment it had, possibly under a non-identity permutation. Declaring the table
// identity here is safe only because every attachment version below is invalidated too,
// so the next sync re-attaches all non-empty attachments at their identity points AND
// (see SyncToBackend's attachment loop) detaches any colour point whose frontend owner
// is empty. Without that detach a stale image would survive under a point the table now
// claims for a different, empty attachment.
for (Uint i = 0; i < MAX_COLOR_ATTACHMENT_SLOTS; ++i) {
m_backendColorSlots[i] = GL_COLOR_ATTACHMENT0 + i;
}
m_frontendReadBuffer = FramebufferAttachmentType::Unknown;
m_backendReadBuffer = GL_NONE;
std::fill(m_syncedFrontendAttachmentVersions.begin(), m_syncedFrontendAttachmentVersions.end(),
@@ -2630,6 +2678,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
} else if (const auto uploadTarget = attachmentObject.GetTextureUploadTarget();
uploadTarget == TextureUploadTarget::Texture3D ||
uploadTarget == TextureUploadTarget::Texture2DArray ||
uploadTarget == TextureUploadTarget::Texture1DArray ||
uploadTarget == TextureUploadTarget::CubeMapArray ||
uploadTarget == TextureUploadTarget::Texture2DMultisampleArray) {
// Single slice/layer of a 3D or array texture: ES has no
// glFramebufferTexture3D, layers attach via glFramebufferTextureLayer.
@@ -2785,6 +2835,95 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
}
Bool BackendFramebufferObject::RecomputeBackendColorSlots(
const FramebufferObject::FramebufferAttachmentArray& stateDrawBuffers) {
// Only the first GL_MAX_COLOR_ATTACHMENTS points exist in the backend. The frontend's own
// limit (ValidateColorAttachmentInRange, which reads the clamped
// GetDynamicParameters().MaxColorAttachments) is never larger than this raw ES cap, so an
// index the frontend accepted is always < slotCount. Indices at or above it can never own
// an image and stay on their identity point - never touched, never a GL error.
const Uint slotCount =
std::min<Uint>(MAX_COLOR_ATTACHMENT_SLOTS,
static_cast<Uint>(std::max<Int>(g_GLESCapabilities.MaxColorAttachments, 1)));
GLenum newSlots[MAX_COLOR_ATTACHMENT_SLOTS];
for (Uint i = 0; i < MAX_COLOR_ATTACHMENT_SLOTS; ++i) {
newSlots[i] = GL_COLOR_ATTACHMENT0 + i;
}
Bool assigned[MAX_COLOR_ATTACHMENT_SLOTS] = {false};
Bool slotTaken[MAX_COLOR_ATTACHMENT_SLOTS] = {false};
// 1. ES pins draw-buffer slot s to GL_COLOR_ATTACHMENTs, so an attachment named by draw
// buffer slot s has no choice: its image must sit at backend point s. This has to
// agree with the compaction the caller just pushed through glDrawBuffers.
for (Uint s = 0; s < FramebufferObject::MAX_DRAW_BUFFERS && s < slotCount; ++s) {
const auto frontendBuf = stateDrawBuffers[s];
if (frontendBuf < FramebufferAttachmentType::Color0 ||
frontendBuf > FramebufferAttachmentType::Color31) {
continue; // GL_NONE, or a default-framebuffer FRONT/BACK token: never relocated.
}
const Uint a =
static_cast<Uint>(frontendBuf) - static_cast<Uint>(FramebufferAttachmentType::Color0);
// Neither guard may ever fire: a duplicate draw buffer is already INVALID_OPERATION
// and an out-of-range one is rejected by ValidateColorAttachmentInRange. If one did
// fire the table would disagree with the glDrawBuffers the caller already issued,
// which is the exact non-injectivity this table exists to remove.
MOBILEGL_ASSERT(a < slotCount && !assigned[a],
"Draw buffer %u names colour attachment %u which is out of range or duplicated.", s,
a);
if (a >= slotCount || assigned[a]) {
continue;
}
newSlots[a] = GL_COLOR_ATTACHMENT0 + s;
assigned[a] = true;
slotTaken[s] = true;
}
// 2. Everything else keeps its identity point when that point survived step 1. This is
// what makes the ordinary drawBuffers[s] == COLOR_ATTACHMENTs case a strict no-op:
// the table stays identity, nothing moves, no attachment is re-issued.
for (Uint a = 0; a < slotCount; ++a) {
if (assigned[a] || slotTaken[a]) {
continue;
}
newSlots[a] = GL_COLOR_ATTACHMENT0 + a;
assigned[a] = true;
slotTaken[a] = true;
}
// 3. What is left are attachments whose identity point step 1 took away. Park them on the
// lowest free point. They are not draw buffers, so nothing is rendered through them;
// they only have to stay addressable for glReadBuffer and blits, and the map has to
// stay injective so reading one of them cannot land on another's image.
for (Uint a = 0; a < slotCount; ++a) {
if (assigned[a]) {
continue;
}
for (Uint s = 0; s < slotCount; ++s) {
if (!slotTaken[s]) {
newSlots[a] = GL_COLOR_ATTACHMENT0 + s;
assigned[a] = true;
slotTaken[s] = true;
break;
}
}
}
Bool moved = false;
for (Uint a = 0; a < MAX_COLOR_ATTACHMENT_SLOTS; ++a) {
if (m_backendColorSlots[a] == newSlots[a]) {
continue;
}
m_backendColorSlots[a] = newSlots[a];
moved = true;
// This attachment's image now belongs at a different backend point. Its frontend
// version has not changed, so the attachment loop would skip it; force it.
m_syncedFrontendAttachmentVersions[static_cast<SizeT>(FramebufferAttachmentType::Color0) + a] =
static_cast<Uint16>(~0u);
}
return moved;
}
void BackendFramebufferObject::SyncToBackend(
const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject, FramebufferTarget asTarget) {
#ifdef TRACY_ENABLE
@@ -2840,6 +2979,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
nEffectiveBuffers = i + 1;
}
g_GLESFuncs.glDrawBuffers(nEffectiveBuffers, m_backendDrawBuffers);
// The line above pinned backend point s to draw-buffer slot s, so the images have to
// be moved under those points. Rebuild the whole colour map and, when anything moved,
// also drop the read-buffer memo: SyncReadBufferToBackend keys it on the frontend
// enum alone, which does not change when the point under it does.
if (RecomputeBackendColorSlots(stateDrawBuffers)) {
m_frontendReadBuffer = FramebufferAttachmentType::Unknown;
}
MGLOG_D("DBAPPLY beFbo=%u target=%d n=%d db0=0x%x feDb0=%d", m_backendFBOId, (int)asTarget,
nEffectiveBuffers, m_backendDrawBuffers[0], (int)stateDrawBuffers[0]);
}
@@ -2885,6 +3031,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
// relevant FRONTEND!!! version should be checked and updated
if (m_syncedFrontendAttachmentVersions[i] != attachmentVersions[i]) {
// SyncAttachmentObject only ever attaches: for an empty frontend attachment it
// returns true and issues nothing, so the point keeps whatever was there. That is
// what makes m_backendColorSlots a permutation of the PHYSICAL layout rather than
// a claim about one - a point handed to an attachment with no image would
// otherwise still hold the previous owner's image and glReadBuffer would return
// it. Bounded by GL_MAX_COLOR_ATTACHMENTS because GL_COLOR_ATTACHMENTn above the
// driver's limit is INVALID_ENUM, and restricted to colour points because
// FRONT_LEFT/BACK_LEFT and co. are not ES attachment points at all.
const Bool isColorPoint =
frontendType >= FramebufferAttachmentType::Color0 &&
frontendType <= FramebufferAttachmentType::Color31 &&
(static_cast<Int>(frontendType) - static_cast<Int>(FramebufferAttachmentType::Color0)) <
g_GLESCapabilities.MaxColorAttachments;
if (isColorPoint && attachmentObject.IsEmpty() && glBackendAttachment != GL_NONE) {
g_GLESFuncs.glFramebufferRenderbuffer(glFBOTarget, glBackendAttachment, GL_RENDERBUFFER, 0);
}
if (SyncAttachmentObject(glFBOTarget, attachmentObject, glBackendAttachment)) {
m_syncedFrontendAttachmentVersions[i] = attachmentVersions[i];
}
@@ -2946,21 +3108,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
GLenum BackendFramebufferObject::GetBackendAttachmentType(FramebufferAttachmentType frontendAtt) const {
GLenum glBackendReadBuffer = GL_NONE;
auto it = std::find(m_frontendDrawBuffers, m_frontendDrawBuffers + FramebufferObject::MAX_DRAW_BUFFERS,
frontendAtt);
Bool notFound = (it == m_frontendDrawBuffers + FramebufferObject::MAX_DRAW_BUFFERS);
if (notFound) {
MGLOG_D(
"%s: frontendAtt not found in draw buffer (probably not remapped), just use the same as frontend",
__func__);
glBackendReadBuffer = MG_Util::ConvertFramebufferAttachmentTypeToGLEnum(frontendAtt);
} else {
MGLOG_D("%s: frontendAtt found in draw buffer, keep it consistent as in read buffers", __func__);
auto index = std::distance(m_frontendDrawBuffers, it);
glBackendReadBuffer = m_backendDrawBuffers[index];
// Only colour attachments are ever relocated; depth/stencil, the default framebuffer's
// FRONT/BACK names and None map straight through.
if (frontendAtt < FramebufferAttachmentType::Color0 || frontendAtt > FramebufferAttachmentType::Color31) {
return MG_Util::ConvertFramebufferAttachmentTypeToGLEnum(frontendAtt);
}
return glBackendReadBuffer;
// The table is a permutation of the backend colour points, so this is the one point that
// owns this attachment. Searching the draw-buffer array instead returned the identity
// point for every attachment that was not a draw buffer - which is exactly the point a
// relocated draw buffer had just taken over, so COLOR_ATTACHMENT0 read back the image of
// whatever attachment was last made the draw buffer.
const Uint index = static_cast<Uint>(frontendAtt) - static_cast<Uint>(FramebufferAttachmentType::Color0);
return m_backendColorSlots[index];
}
StateBackendObjectRegistry<MG_State::GLState::FramebufferObject, BackendFramebufferObject>
+22
View File
@@ -433,6 +433,28 @@ namespace MobileGL::MG_Backend::DirectGLES {
this array could be provided as data directly to ES `glDrawBuffers` function
*/
GLenum m_backendDrawBuffers[MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS] = {GL_NONE};
static constexpr Uint MAX_COLOR_ATTACHMENT_SLOTS =
static_cast<Uint>(FramebufferAttachmentType::Color31) -
static_cast<Uint>(FramebufferAttachmentType::Color0) + 1;
/* Where each frontend GL_COLOR_ATTACHMENTn image physically lives in the backend ES
framebuffer, as a GL_COLOR_ATTACHMENTm enum. ES only accepts glDrawBuffers bufs[s] ==
GL_COLOR_ATTACHMENTs, so a GL draw-buffer slot s naming attachment a forces a's image
under backend slot s. This table is the single owner of that decision and is kept a
PERMUTATION of the backend colour slots: every other attachment keeps its identity
slot when that slot survived, and is parked on the lowest free slot when it did not.
Deriving the point per-query from the draw-buffer array instead handed the identity
point to any attachment that was not a draw buffer - i.e. exactly the point a
relocated draw buffer had just taken over. The permutation is only true of the
PHYSICAL framebuffer because the attachment loop detaches a point whose frontend
owner is empty; do not remove that detach. */
GLenum m_backendColorSlots[MAX_COLOR_ATTACHMENT_SLOTS] = {GL_NONE};
/* Rebuild m_backendColorSlots from the frontend draw-buffer array. Returns true when any
attachment moved, i.e. when the physical attachments and the memoised read buffer have
to be re-applied. */
Bool RecomputeBackendColorSlots(
const MG_State::GLState::FramebufferObject::FramebufferAttachmentArray& stateDrawBuffers);
FramebufferAttachmentType m_frontendReadBuffer = FramebufferAttachmentType::Color0;
GLenum m_backendReadBuffer = GL_COLOR_ATTACHMENT0;
@@ -810,6 +810,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
m_dynamicParameters.SupportsWideLines = m_vulkanCaps.SupportsWideLines;
// A 2D or 2D multisample array texture is a VK_IMAGE_TYPE_2D image whose GL depth IS its
// arrayLayers, so a GL layer is a Vulkan array layer with nothing to translate.
// ResolveAttachmentBaseArrayLayer already passes the attachment's layer through. The other
// layered targets are declared separately as their own machinery lands.
{
using DynParams = MG_Backend::DynamicBackendParameters;
m_dynamicParameters.PerLayerFramebufferAttachmentTargets |=
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture2DArray) |
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture2DMultisampleArray);
// A cube map array is one 2D image with arrayLayers = 6 * cubeCount, so a GL layer is a
// Vulkan array layer here too - but the image cannot be created without imageCubeArray.
// A 3D texture's GL layer is a z slice, which only a 2D view over a 2D-array-compatible
// image can name. Optimistic: a format that refuses the flag is caught at image creation
// and declines the slice view there, which the clear path handles as a soft miss.
if (m_vulkanCaps.Supports2DArrayCompatible3DImages) {
m_dynamicParameters.PerLayerFramebufferAttachmentTargets |=
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture3D);
}
if (m_vulkanCaps.SupportsImageCubeArray) {
m_dynamicParameters.PerLayerFramebufferAttachmentTargets |=
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::TextureCubeMapArray);
}
}
m_dynamicParameters.SupportsFloat64VertexAttributes = m_vulkanCaps.SupportsShaderFloat64;
m_dynamicParameters.MaxShaderStorageBlockSize =
std::min(m_vulkanCaps.MaxShaderStorageBlockSize, kMaxAdvertisedShaderStorageBlockSize);
if (m_vulkanCaps.SupportsShaderSubgroup) {
@@ -209,6 +209,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
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)));
XXHASH_VERIFY(
XXH64_update(m_hashState, &payload.provokingVertexMode, sizeof(payload.provokingVertexMode)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthTestEnable, sizeof(payload.depthTestEnable)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthWriteEnable, sizeof(payload.depthWriteEnable)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthBiasEnable, sizeof(payload.depthBiasEnable)));
@@ -397,6 +399,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
raster.depthBiasEnable = payload.depthBiasEnable ? VK_TRUE : VK_FALSE;
raster.rasterizerDiscardEnable = payload.rasterizerDiscardEnable ? VK_TRUE : VK_FALSE;
raster.lineWidth = 1.0f;
// Only chain the struct when the mode is not Vulkan's implicit default: a device without
// VK_EXT_provoking_vertex enabled must never see this pNext entry, and the renderer's
// selector already collapses to FIRST in exactly that case - so a device without the
// extension produces a byte-identical VkGraphicsPipelineCreateInfo to before.
VkPipelineRasterizationProvokingVertexStateCreateInfoEXT provokingVertexState{
VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_PROVOKING_VERTEX_STATE_CREATE_INFO_EXT};
if (payload.provokingVertexMode != VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT) {
provokingVertexState.provokingVertexMode = payload.provokingVertexMode;
provokingVertexState.pNext = raster.pNext;
raster.pNext = &provokingVertexState;
}
VkPipelineMultisampleStateCreateInfo ms{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO};
ms.rasterizationSamples = payload.rasterizationSamples;
@@ -35,6 +35,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkPolygonMode polygonMode = VK_POLYGON_MODE_FILL;
VkCullModeFlags cullMode = VK_CULL_MODE_BACK_BIT;
VkFrontFace frontFace = VK_FRONT_FACE_CLOCKWISE;
// GL's provoking vertex, baked into the pipeline (VK_EXT_provoking_vertex). It selects
// which vertex a flat varying takes AND the vertex order transform feedback records for
// strips/fans, so it is part of the pipeline's identity, not dynamic state. Defaults to
// Vulkan's own convention, which is what a device without the extension gets.
VkProvokingVertexModeEXT provokingVertexMode = VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT;
Bool depthTestEnable = false;
Bool depthWriteEnable = false;
Bool depthBiasEnable = false;
@@ -2458,6 +2458,33 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
// A 64-bit vertex input has to arrive as its 32-bit word pair: VK_FORMAT_R64*_SFLOAT is
// optional and lavapipe advertises none of them at all. The pass is unconditional so it
// always agrees with the Float64 case in VertexInputStateFactory::ToVkVertexFormat, and
// ReflectVertexInputs below then sees an ordinary uvec2/uvec4 input.
//
// Failure here is not recoverable and must not be swallowed: ToVkVertexFormat has already
// committed to R32G32{,B32A32}_UINT for the attribute, so a module still declaring
// `in double` would reconcile to Unknown and build a pipeline with a UINT format under a
// double input - garbage with no diagnostic anywhere.
if (shaders[i] && shaders[i]->GetShaderStage() == ShaderStage::Vertex) {
Vector<Uint> packedSpirv;
const Bool packOk = MG_Util::ShaderTranspiler::ShaderCompiler::PackDoubleVertexInputsForVulkan(
moduleSpirvs[i], packedSpirv);
MOBILEGL_ASSERT(packOk,
"ProgramFactory: 64-bit vertex input packing failed for program %u; the "
"vertex-input format and the shader input type now disagree",
program.GetExternalIndex());
if (packOk) {
moduleSpirvs[i] = std::move(packedSpirv);
} else {
MGLOG_E("ProgramFactory: failed to pack 64-bit vertex inputs for program %u; "
"double-typed vertex attributes will be fetched as uint32 words and not "
"reinterpreted",
program.GetExternalIndex());
}
}
// When Vulkan can legally access storage images without a statically declared
// format, let GL's glBindImageTexture format select the runtime image view. This
// provides desktop-driver-compatible behavior for packs such as iterationRP, whose
@@ -29,6 +29,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Stride, sizeof(attr.Stride)));
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Offset, sizeof(attr.Offset)));
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsInteger, sizeof(attr.IsInteger)));
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsLong, sizeof(attr.IsLong)));
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsBgra, sizeof(attr.IsBgra)));
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Divisor, sizeof(attr.Divisor)));
@@ -97,7 +98,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
const VkFormat sourceVkFormat =
ToVkVertexFormat(attr.Type, attr.Size, attr.Normalized, attr.IsInteger, attr.IsBgra);
ToVkVertexFormat(attr.Type, attr.Size, attr.Normalized, attr.IsInteger, attr.IsBgra, attr.IsLong);
if (sourceVkFormat == VK_FORMAT_UNDEFINED) {
MGLOG_E("Unsupported vertex attribute layout (location=%u, type=%s, size=%d): the array is "
"enabled but cannot be mapped to a VkFormat",
@@ -273,7 +274,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
VkFormat VertexInputStateFactory::ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger,
Bool isBgra) {
Bool isBgra, Bool isLong) {
if (isBgra) {
// GL_BGRA: four reversed-order components, always normalized (enforced at validation), only
// legal with GL_UNSIGNED_BYTE or a 2_10_10_10 type. The reversed VkFormats put the
@@ -298,6 +299,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
case DataType::Int2101010Rev:
if (isInteger || size != 4) return VK_FORMAT_UNDEFINED;
return normalized ? VK_FORMAT_A2B10G10R10_SNORM_PACK32 : VK_FORMAT_A2B10G10R10_SSCALED_PACK32;
case DataType::Float64:
// A 64-bit attribute is fetched as its 32-bit word pair and bitcast back to double in the
// shader (PackDoubleVertexInputsPass does the shader half). That is bit-exact and, unlike
// VK_FORMAT_R64*_SFLOAT, needs no format capability: lavapipe reports bufferFeatures = 0
// for every R64 float format, so a native 64-bit vertex fetch is simply unavailable there
// while shaderFloat64 is not. Both halves key off nothing but the attribute being long,
// so they always agree without extra plumbing.
if (!isLong || isInteger || normalized) return VK_FORMAT_UNDEFINED;
switch (size) {
case 1: return VK_FORMAT_R32G32_UINT;
case 2: return VK_FORMAT_R32G32B32A32_UINT;
// A dvec3/dvec4 input is 6/8 uint32 components: no single VkFormat, and GL spreads it
// over two attribute locations, which the location-per-VAO-index model here does not
// express. Declined rather than fetched wrong.
default: return VK_FORMAT_UNDEFINED;
}
case DataType::Float32:
switch (size) {
case 1: return VK_FORMAT_R32_SFLOAT;
@@ -93,7 +93,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
static SizeT GetAttributeByteSize(DataType type, Int size, Bool isBgra);
private:
static VkFormat ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger, Bool isBgra = false);
static VkFormat ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger, Bool isBgra = false,
Bool isLong = false);
static Bool IsScaledIntegerVertexFormat(VkFormat format);
static VkFormat ToFloat32VertexFormat(Int componentCount);
Bool SupportsVertexBufferFormat(VkFormat format) const;
@@ -87,9 +87,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
static VkImageViewType ResolveAttachmentViewType(
const MG_State::GLState::FramebufferAttachmentObject& attachment,
const VkTextureManager::TextureResource& resource) {
return !attachment.IsLayered() && IsCubeMapFaceUploadTarget(attachment.GetTextureUploadTarget()) ?
VK_IMAGE_VIEW_TYPE_2D :
resource.viewType;
if (attachment.IsLayered()) {
return resource.viewType;
}
// A non-layered attachment names ONE layer, so the view over it is a plain 2D view whatever
// the image's own view type is. The cube-face upload targets always meant this; a cube map
// array attached through glFramebufferTextureLayer means it too, and a CUBE_ARRAY view over
// a single layer is not a legal attachment. The CUBE arm is inert today - no frontend path
// produces a non-layered cube attachment without a face upload target - and is kept for
// symmetry with CUBE_ARRAY.
if (IsCubeMapFaceUploadTarget(attachment.GetTextureUploadTarget()) ||
resource.viewType == VK_IMAGE_VIEW_TYPE_CUBE_ARRAY || resource.viewType == VK_IMAGE_VIEW_TYPE_CUBE) {
return VK_IMAGE_VIEW_TYPE_2D;
}
return resource.viewType;
}
static MG_State::GLState::ITextureObject* ResolveCompleteColorAttachmentTexture(
@@ -16,6 +16,7 @@
#include "MG_State/GLState/FramebufferState/FramebufferObject.h"
#include <Includes.h>
#include <unordered_map>
#include <vk_mem_alloc.h>
namespace MobileGL::MG_Backend::DirectVulkan {
@@ -314,7 +315,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint64 deferredAtFrame = 0;
};
UnorderedMap<MG_State::GLState::RenderbufferObject*, RenderbufferResource> m_renderbufferResources;
// Node-based std::unordered_map, deliberately not FastSTL's open-addressing UnorderedMap:
// callers cache a RenderbufferResource* - or a bare &resource->layout - and then make further
// calls that touch this map. BlitFramebuffer is the one that bit: it resolves the source and
// destination colour bindings (ResolveColorBlitBinding caches &rbResource->layout), then
// materializes the source's pending clear, which looks that same resource up again. FastSTL's
// operator[] runs its load-factor check before find_key and reallocates the whole bucket array
// when occupancy crosses it, so even a plain lookup relocates every element; erase only
// tombstones and never decrements the occupancy, so the doubling keeps firing. After a
// relocation the cached pointer names freed storage still holding the pre-clear
// VK_IMAGE_LAYOUT_UNDEFINED, and BlitFramebuffer bails out at "source image layout is
// undefined", silently dropping the blit - renderbuffers_storage_multisample read back zero
// instead of the clear colour on exactly the iterations that grew the table.
//
// Reordering the materialize ahead of the resolves - the fix ReadPixels got - does not cover
// this: the destination resolve still runs after the source pointer is taken. The depth blit,
// GetOrCreateRenderPass's depthRenderbufferResource and ReadDepthStencilPixels cache the same
// kind of pointer, so the invariant belongs in the container rather than in a per-call-site
// ordering rule. m_textureResources is node-based for the same reason. This buys stability
// across rehash and insert only - erase still invalidates the erased element, which is safe
// here because a renderbuffer that is an FBO attachment is held alive by that attachment.
std::unordered_map<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
@@ -564,6 +564,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
outShape.depth = 1;
outShape.arrayLayers = 6;
return true;
case TextureUploadTarget::CubeMapArray:
case TextureUploadTarget::ProxyCubeMapArray:
// GL_TEXTURE_CUBE_MAP_ARRAY is an array texture whose layers happen to be cube faces:
// one 2D image with arrayLayers = 6 * cubeCount, CUBE_COMPATIBLE so the whole thing can
// be sampled as a samplerCubeArray. glTexStorage3D hands the 6*n through as the GL depth
// and the upload path's depthSelectsArrayLayer already lists VK_IMAGE_VIEW_TYPE_CUBE_ARRAY,
// so the copies address layers correctly.
//
// A depth that is not a whole number of cubes, or a non-square level, has no Vulkan shape
// - declined the way every other unrepresentable target is. This function's Bool return
// exists for exactly that; asserting here would abort the process on ordinary application
// input, GL_PROXY_TEXTURE_CUBE_MAP_ARRAY above all.
if (texelSize.z() <= 0 || (texelSize.z() % 6) != 0 || texelSize.x() != texelSize.y()) {
return false;
}
outShape.imageType = VK_IMAGE_TYPE_2D;
outShape.viewType = VK_IMAGE_VIEW_TYPE_CUBE_ARRAY;
outShape.imageFlags = VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT;
outShape.depth = 1;
outShape.arrayLayers = static_cast<Uint32>(texelSize.z());
return true;
default:
return false;
}
@@ -822,8 +843,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (resource == nullptr || resource->image == VK_NULL_HANDLE || mipLevel >= resource->mipLevels) {
return VK_NULL_HANDLE;
}
if (layerCount == 0 || baseArrayLayer >= resource->arrayLayers ||
baseArrayLayer + layerCount > resource->arrayLayers) {
// A 3D image has arrayLayers == 1 and keeps its GL layers on the z axis, so a per-slice
// attachment view is a 2D view whose "array layer" is the slice - legal only on a
// 2D-array-compatible image (VUID-VkImageViewCreateInfo-image-04970), which
// SyncTextureResource asks for and may have had refused per format.
if (resource->viewType == VK_IMAGE_VIEW_TYPE_3D && viewType == VK_IMAGE_VIEW_TYPE_2D) {
const Uint32 sliceCount = std::max(resource->depth >> mipLevel, 1u);
if ((resource->imageCreateFlags & VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT) == 0 ||
layerCount == 0 || baseArrayLayer >= sliceCount || baseArrayLayer + layerCount > sliceCount) {
MGLOG_D("%s: cannot name slice span [%u, %u) of 3D textureId=%d (mip %u has %u slices, "
"2D-array-compatible=%d)",
__func__, baseArrayLayer, baseArrayLayer + layerCount, texture.GetExternalIndex(),
mipLevel, sliceCount,
(int)((resource->imageCreateFlags & VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT) != 0));
return VK_NULL_HANDLE;
}
} else if (layerCount == 0 || baseArrayLayer >= resource->arrayLayers ||
baseArrayLayer + layerCount > resource->arrayLayers) {
MGLOG_D("%s: invalid layer span [%u, %u) for textureId=%d arrayLayers=%u",
__func__, baseArrayLayer, baseArrayLayer + layerCount, texture.GetExternalIndex(),
resource->arrayLayers);
@@ -1492,11 +1528,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// 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
: (mipLevels > 1 ? std::max(mipLevels, ComputeFullMipLevelCount(texelSize)) : 1u);
TextureShapeInfo shapeInfo{};
const Bool supportedShape = TryResolveTextureShapeInfo(texture, uploadTarget, texelSize, shapeInfo);
// ComputeFullMipLevelCount takes max(x, y, z), and for every ARRAY shape z is the layer
// count, not a mip-able axis: a 4x4 array with 192 layers asked for 6 levels on an image
// whose legal maximum is 3 (VUID-VkImageCreateInfo-mipLevels-00958). Only the image's own
// extent - width, height and shapeInfo.depth, which is 1 for every array - can bound it.
// lavapipe has been letting this through unvalidated; a strict driver would not.
const IntVec3 mipExtent{texelSize.x(), texelSize.y(), static_cast<Int>(shapeInfo.depth)};
const Uint32 fullMipLevels = ComputeFullMipLevelCount(mipExtent);
const Uint32 backingMipLevels =
isMultisampleTexture ? 1u : (mipLevels > 1 ? std::min(std::max(mipLevels, fullMipLevels), fullMipLevels) : 1u);
if (!supportedShape) {
// 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).
@@ -1554,6 +1596,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
(formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT) != 0;
const Bool supportsStorageImage = storageImageCapable && markedAsStorageImage;
VkImageCreateFlags imageCreateFlags = shapeInfo.imageFlags;
// One z slice of a 3D texture can only be attached to a framebuffer through a 2D view over
// it, which needs the image to be 2D-array-compatible (Vulkan 1.1 core, promoted from
// VK_KHR_maintenance1). Asked for optimistically and withdrawn per format below if the
// driver refuses - losing it only costs per-slice attachment, while failing creation would
// lose the texture entirely.
if (shapeInfo.imageType == VK_IMAGE_TYPE_3D && !isMultisampleTexture &&
m_2dArrayCompatibleUnsupported.find(format) == m_2dArrayCompatibleUnsupported.end()) {
imageCreateFlags |= VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT;
}
if (storageImageCapable && IsMutableStorageImageFormat(format) &&
m_mutableFormatUnsupported.find(format) == m_mutableFormatUnsupported.end()) {
imageCreateFlags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
@@ -1711,7 +1762,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
imageInfo.pNext = &formatListInfo;
}
if (isMultisampleTexture || (imageInfo.flags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) != 0) {
if (isMultisampleTexture || (imageInfo.flags & (VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT |
VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT)) != 0) {
VkImageFormatProperties imageFormatProperties{};
VkResult imageFormatResult = vkGetPhysicalDeviceImageFormatProperties(
m_physicalDevice, format, imageInfo.imageType, imageInfo.tiling, imageInfo.usage,
@@ -1734,6 +1786,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_physicalDevice, format, imageInfo.imageType, imageInfo.tiling, imageInfo.usage,
imageInfo.flags, &imageFormatProperties);
}
if (imageFormatResult != VK_SUCCESS && !isMultisampleTexture &&
(imageInfo.flags & VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT) != 0) {
// Losing 2D-array compatibility only costs per-slice framebuffer attachment for this
// format; failing creation would lose the texture entirely. Remembered so later syncs
// neither reprobe nor flag-mismatch against this image and recreate it.
MGLOG_W("%s: VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT is unsupported for format=%d "
"textureId=%d; creating without it (per-slice framebuffer attachment will be "
"unavailable for it)",
__func__, static_cast<Int>(format), texture.GetExternalIndex());
m_2dArrayCompatibleUnsupported.insert(format);
imageInfo.flags &= ~VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT;
imageCreateFlags = imageInfo.flags;
imageFormatResult = vkGetPhysicalDeviceImageFormatProperties(
m_physicalDevice, format, imageInfo.imageType, imageInfo.tiling, imageInfo.usage,
imageInfo.flags, &imageFormatProperties);
}
if (imageFormatResult != VK_SUCCESS ||
(isMultisampleTexture && (imageFormatProperties.sampleCounts & resolvedSampleCount) == 0)) {
MGLOG_D("%s: image flags=0x%x sampleCount=%d are unsupported for textureId=%d target=%s "
@@ -487,6 +487,10 @@ private:
// 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;
// Formats whose 3D images refused VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT. Per format+usage,
// exactly like the mutable-format verdict above, so it is answered at image creation and
// remembered rather than probed once globally.
std::unordered_set<VkFormat> m_2dArrayCompatibleUnsupported;
std::unordered_map<TextureIdentity, WeakPtr<MG_State::GLState::ITextureObject>, TextureIdentityHash> m_aliveObjects;
std::unordered_map<TextureIdentity, TextureResource, TextureIdentityHash> m_textureResources;
// Textures that have been bound to a GL image unit (see MarkStorageImageTexture).
@@ -3037,17 +3037,6 @@ void main() {
vkBuffers.assign(bindingCount, VK_NULL_HANDLE);
vkOffsets.assign(bindingCount, 0);
auto findBufferByKey = [&](SizeT bufferKey) -> const SharedPtr<MG_State::GLState::BufferObject>* {
const auto& attrs = vao.GetAllAttributes();
for (Uint32 location = 0; location < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS; ++location) {
const auto& attr = attrs[location];
if (attr.Buffer && reinterpret_cast<SizeT>(attr.Buffer.get()) == bufferKey) {
return &attr.Buffer;
}
}
return nullptr;
};
auto uploadConvertedStream = [&](VertexInputStateFactory::VertexStreamConversion conversion,
const MG_State::GLState::VertexAttribute& attribute,
const Uint8* sourceData, SizeT sourceStride,
@@ -3150,14 +3139,14 @@ void main() {
continue;
}
const SizeT bufferKey = vertexInputState.bindingBufferKeys[binding];
// The VAO attribute already holds the buffer's SharedPtr; use it by reference directly
// instead of re-resolving it from the GL context by external index (a map lookup +
// atomic refcount every binding every draw).
const SharedPtr<MG_State::GLState::BufferObject>* sourceBufferSharedPtr = findBufferByKey(bufferKey);
MOBILEGL_ASSERT(sourceBufferSharedPtr != nullptr && *sourceBufferSharedPtr != nullptr,
// VertexInputStateFactory fills bindingBufferKeys[b] and bindingAttributeLocations[b]
// from the SAME loop iteration, one binding per enabled attribute with no merging, so
// this attribute's Buffer IS the SharedPtr by construction - no need to search the VAO's
// 32 slots for it. The client-memory branch above has already returned, so the location
// is in range here.
const auto& sourceBufferShared = vao.GetAttribute(bindingLocation).Buffer;
MOBILEGL_ASSERT(sourceBufferShared != nullptr,
"UploadAndBindVertexStreams failed to resolve source buffer");
const auto& sourceBufferShared = *sourceBufferSharedPtr;
BufferSlice slice{};
const SizeT sourceSize = sourceBufferShared->GetSize();
const SizeT baseOffset =
@@ -3401,8 +3390,12 @@ void main() {
substituteRestartIndex = restartIndex;
}
const auto* indexBuffer =
pIndexBufferView->forceClientMemory ? nullptr : vao.GetIndexBufferBindingSlot().GetBoundObject().get();
// Bound by reference so the SharedPtr below is the one already in hand rather than a fresh
// GL-name map lookup plus an atomic refcount pair on every indexed draw - the vertex path
// above documents the same cost.
const SharedPtr<MG_State::GLState::BufferObject>& indexBufferShared =
vao.GetIndexBufferBindingSlot().GetBoundObject();
const auto* indexBuffer = pIndexBufferView->forceClientMemory ? nullptr : indexBufferShared.get();
if (indexBuffer == nullptr) {
// No element-array buffer: the view's byte offset is a raw client pointer
// (desktop drivers accept client-memory indices and the GL CTS relies on
@@ -3441,7 +3434,6 @@ void main() {
"DrawElements index range out of bounds");
BufferSlice slice{};
auto indexBufferShared = MG_State::pGLContext->GetBufferObject(indexBuffer->GetExternalIndex());
MOBILEGL_ASSERT(indexBufferShared != nullptr, "UploadAndBindIndexBuffer failed to resolve shared EBO");
if (substituteRestart) {
// The whole buffer is rewritten, not just this draw's range, so that every element
@@ -3694,6 +3686,13 @@ void main() {
.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST,
.cullMode = VK_CULL_MODE_NONE,
.frontFace = VK_FRONT_FACE_CLOCKWISE,
// Functionally irrelevant to the blit (no flat varying, no capture), but on a device with
// provokingVertexModePerPipeline == VK_FALSE a blit pipeline left on FIRST inside a render
// pass whose draw pipelines are LAST is an illegal mix. Note this does NOT cover
// GenerateDepthMipmapWithShader, which builds its pipeline directly and keeps Vulkan's
// FIRST - legal only because it creates and begins its own render pass. Anything that ever
// records that pipeline inside an outer render pass must route through this selector too.
.provokingVertexMode = SelectProvokingVertexMode(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, false),
.depthTestEnable = false,
.depthWriteEnable = false,
.depthCompareOp = VK_COMPARE_OP_ALWAYS,
@@ -4024,6 +4023,16 @@ void main() {
return true;
}
// A program that runs a geometry shader AND captures transform feedback. Both halves are
// link-time properties, so this is safe to fold into a pipeline keyed on the program hash.
static Bool ProgramCapturesXfbFromGeometryStage(const MG_State::GLState::ProgramObject& program) {
if (program.GetTransformFeedbackVaryingCount() == 0) return false;
for (const auto& shader : program.GetAttachedShaders()) {
if (shader && shader->GetShaderStage() == ShaderStage::Geometry) return true;
}
return false;
}
VkPipeline VulkanRenderer::GetOrCreatePipeline(
GLenum mode,
const MG_State::GLState::ProgramObject& program,
@@ -4051,7 +4060,10 @@ void main() {
auto& vis = m_vertexInputStateFactory->GetOrCreateVertexInputState(vao);
const Uint64 vertexLayoutHash = vis.layoutHash;
const Uint64 renderPassHash = renderPassEntry.hash;
const Uint renderStateVersion = MG_State::pGLContext->GetRenderStateParametersVersion();
// The pipeline-relevant subset only: glViewport / glScissor / glBlendColor / glStencilMask
// and friends are dynamic state or not pipeline state at all, and keying the memo on the
// all-state counter made any of them evict a perfectly good VkPipeline.
const Uint renderStateVersion = MG_State::pGLContext->GetPipelineStateVersion();
for (Uint32 i = 0; i < m_pipelineMemoCount; ++i) {
const PipelineMemoEntry& entry = m_pipelineMemo[i];
if (entry.pipeline != VK_NULL_HANDLE && entry.mode == mode &&
@@ -4242,6 +4254,16 @@ void main() {
? MG_Util::ConvertCullFaceModeToVkEnum(MG_State::pGLContext->GetCullFaceMode(), invertClockwise)
: VK_CULL_MODE_NONE,
.frontFace = VK_FRONT_FACE_CLOCKWISE,
// Read the geometry stage off the program's own shader list rather than
// programObj.rasterizationProducerStage: that field is filled by the clip-fixup analysis,
// which does not run for every program, so it reads Unknown for exactly the
// geometry-plus-capture programs this guard exists to catch. Both inputs are link-time
// facts folded into programObj.hash, which is what the pipeline memo and the
// SetupDrawSnapshot fast path key on - so no memo can hand back a pipeline built for the
// other mode. IsTransformFeedbackActive() would be a live bug here: neither memo key
// moves on glBeginTransformFeedback.
.provokingVertexMode = SelectProvokingVertexMode(
vkTopology, ProgramCapturesXfbFromGeometryStage(program)),
.depthTestEnable = depthTestEnabled,
.depthWriteEnable = depthTestEnabled && MG_State::pGLContext->GetDepthMask(),
.depthBiasEnable = polygonOffsetFillEnabled,
@@ -4666,7 +4688,7 @@ void main() {
snap.imageIndex != m_imageIndexAcquired) {
return false;
}
const auto& program = *MG_State::pGLContext->GetCurrentProgram();
const auto& program = *MG_State::pGLContext->GetProgramForDraw();
if (program.GetLifetimeId() != snap.programLifetimeId ||
program.GetBackendStateVersion() != snap.programVersion) {
return false;
@@ -4681,7 +4703,7 @@ void main() {
drawFbo->GetObjectVersion() != snap.fboVersion) {
return false;
}
if (MG_State::pGLContext->GetRenderStateParametersVersion() != snap.renderStateVersion ||
if (MG_State::pGLContext->GetPipelineStateVersion() != snap.renderStateVersion ||
MG_State::pGLContext->GetTextureBindGeneration() != snap.bindGeneration) {
return false;
}
@@ -4792,7 +4814,7 @@ void main() {
return false;
}
const auto& vao = *MG_State::pGLContext->GetBoundVertexArray();
const auto& program = *MG_State::pGLContext->GetCurrentProgram();
const auto& program = *MG_State::pGLContext->GetProgramForDraw();
ProgramFactory::CompileOptionFlags transformFlags = GetShaderTransformFlags(m_swapchainObject.GetPreTransform());
// Captured draws take the xfb-decorated program variant.
if (m_transformFeedbackFeatureEnabled && MG_State::pGLContext->IsTransformFeedbackActive() &&
@@ -5145,7 +5167,7 @@ void main() {
snap.drawFbo = drawFbo.get();
snap.fboVersion = drawFbo->GetObjectVersion();
snap.drawFboIsDefault = drawFbo->IsDefaultFramebuffer();
snap.renderStateVersion = MG_State::pGLContext->GetRenderStateParametersVersion();
snap.renderStateVersion = MG_State::pGLContext->GetPipelineStateVersion();
snap.bindGeneration = MG_State::pGLContext->GetTextureBindGeneration();
snap.baseTransformFlags = GetShaderTransformFlags(m_swapchainObject.GetPreTransform()).GetRaw();
snap.resolvedTransformFlags = transformFlags.GetRaw();
@@ -5176,7 +5198,7 @@ void main() {
void VulkanRenderer::DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ) {
m_textureManager->CollectGarbage();
auto& frame = m_frameContext.GetCurrent();
const auto& program = *MG_State::pGLContext->GetCurrentProgram();
const auto& program = *MG_State::pGLContext->GetProgramForDraw();
ProgramFactory::CompileOptionFlags transformFlags = 0;
const auto& programObj = m_programFactory->GetOrCreateProgram(program, transformFlags);
@@ -5216,7 +5238,7 @@ void main() {
void VulkanRenderer::DispatchComputeIndirect(GLintptr indirect) {
m_textureManager->CollectGarbage();
auto& frame = m_frameContext.GetCurrent();
const auto& program = *MG_State::pGLContext->GetCurrentProgram();
const auto& program = *MG_State::pGLContext->GetProgramForDraw();
ProgramFactory::CompileOptionFlags transformFlags = 0;
const auto& programObj = m_programFactory->GetOrCreateProgram(program, transformFlags);
@@ -5894,6 +5916,83 @@ void main() {
QueueClearBufferPayload(buffer, drawbuffer, payload);
}
Bool VulkanRenderer::ClearDepthSliceWithRenderPass(VkCommandBuffer commandBuffer,
MG_State::GLState::ITextureObject& texture, Uint32 mipLevel,
Uint32 depthSlice, const VkClearValue& clearValue) {
auto* resource = m_textureManager->SyncTextureAndGetDescriptor(texture);
if (resource == nullptr || resource->image == VK_NULL_HANDLE) return false;
if (m_frameContext.GetCurrentFrameIndex() >= m_deferredDepthMipmapCleanup.size()) return false;
// A 2D view over one z slice. Returns VK_NULL_HANDLE when the image is not
// 2D-array-compatible, which is the whole reason this can fail.
const VkImageView sliceView = m_textureManager->GetOrCreateAttachmentViewAtMipLevel(
texture, mipLevel, depthSlice, 1, VK_IMAGE_VIEW_TYPE_2D);
if (sliceView == VK_NULL_HANDLE) return false;
VkAttachmentDescription colorAttachment{};
colorAttachment.format = resource->format;
colorAttachment.samples = VK_SAMPLE_COUNT_1_BIT;
colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
colorAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
colorAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
colorAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
// Hand the slice back in the layout the caller already tracks for the whole image, so its
// closing barrier stays truthful and resource->layout is never touched from in here.
colorAttachment.finalLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
VkAttachmentReference colorRef{};
colorRef.attachment = 0;
colorRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
VkSubpassDescription subpass{};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.colorAttachmentCount = 1;
subpass.pColorAttachments = &colorRef;
VkRenderPassCreateInfo renderPassInfo{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO};
renderPassInfo.attachmentCount = 1;
renderPassInfo.pAttachments = &colorAttachment;
renderPassInfo.subpassCount = 1;
renderPassInfo.pSubpasses = &subpass;
VkRenderPass renderPass = VK_NULL_HANDLE;
if (vkCreateRenderPass(m_device, &renderPassInfo, nullptr, &renderPass) != VK_SUCCESS) return false;
const Uint32 levelWidth = std::max(resource->extent.width >> mipLevel, 1u);
const Uint32 levelHeight = std::max(resource->extent.height >> mipLevel, 1u);
VkFramebufferCreateInfo framebufferInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO};
framebufferInfo.renderPass = renderPass;
framebufferInfo.attachmentCount = 1;
framebufferInfo.pAttachments = &sliceView;
framebufferInfo.width = levelWidth;
framebufferInfo.height = levelHeight;
framebufferInfo.layers = 1;
VkFramebuffer framebuffer = VK_NULL_HANDLE;
if (vkCreateFramebuffer(m_device, &framebufferInfo, nullptr, &framebuffer) != VK_SUCCESS) {
vkDestroyRenderPass(m_device, renderPass, nullptr);
return false;
}
VkRenderPassBeginInfo beginInfo{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO};
beginInfo.renderPass = renderPass;
beginInfo.framebuffer = framebuffer;
beginInfo.renderArea.extent = {levelWidth, levelHeight};
beginInfo.clearValueCount = 1;
beginInfo.pClearValues = &clearValue;
// The load op is the whole operation: begin and end with nothing in between.
vkCmdBeginRenderPass(commandBuffer, &beginInfo, VK_SUBPASS_CONTENTS_INLINE);
vkCmdEndRenderPass(commandBuffer);
// The image view is owned and memoised by the texture resource; only these two are throwaway.
auto& deferredCleanup = m_deferredDepthMipmapCleanup[m_frameContext.GetCurrentFrameIndex()];
deferredCleanup.renderPasses.push_back(renderPass);
deferredCleanup.framebuffers.push_back(framebuffer);
return true;
}
Bool VulkanRenderer::MaterializePendingClearForTexture(VkCommandBuffer commandBuffer,
MG_State::GLState::ITextureObject& texture) {
Vector<PendingClearEntry> pendingClears;
@@ -5930,16 +6029,79 @@ void main() {
MOBILEGL_ASSERT(pendingClear.key.mipLevel < resource->mipLevels,
"MaterializePendingClearForTexture: textureId=%d pending clear mip=%u out of range %u",
texture.GetExternalIndex(), pendingClear.key.mipLevel, resource->mipLevels);
MOBILEGL_ASSERT(pendingClear.key.baseArrayLayer + pendingClear.key.layerCount <= resource->arrayLayers,
"MaterializePendingClearForTexture: textureId=%d pending clear layer span [%u, %u) exceeds arrayLayers=%u",
// FIXME: a layered clear of a GL_TEXTURE_3D texture still reads back wrong.
// KHR-GL44/45/46.geometry_shader.layered_framebuffer.clear_call_support fails on
// DirectVulkan: it attaches a 4-deep 3D texture with glFramebufferTexture (layered),
// clears with glClearBufferiv, then reads each slice back through
// glFramebufferTextureLayer and gets zeros. Those cases exist only in the GL44+ lists,
// above the 4.0 this backend reports, so they are outside the current conformance
// claim - but the feature (layered attachment, GL 3.2) is not, so an application can
// reach this.
//
// Already ruled out by bisecting with temporary bypasses, so do not re-test these:
// - the per-slice render-pass clear below (disabling it changes nothing)
// - the per-target gate in FramebufferTextureLayer_State (it already permits
// Texture3D here; bypassing it changes nothing)
// - VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT on the 3D image (not requesting it
// changes nothing)
// What IS fixed here is the subresource range below: a layered GL clear queues
// layerCount = depth, which is illegal for a VK_IMAGE_TYPE_3D image, and the old code
// passed it straight through - running the case standalone against the previous build
// trips MOBILEGL_ASSERT(baseArrayLayer + layerCount <= arrayLayers) as 0 + 4 <= 1.
//
// Note when picking this up: the case does not reproduce standalone the way it behaves
// in a batch run (batch passed before this change, standalone asserted), so it depends
// on state left by earlier cases. Reproduce it inside a chunk, not on its own.
//
// A 3D image keeps its GL layers on the z axis (arrayLayers == 1), so the pending
// clear's "layer" is a slice index bounded by the mip level's depth.
const Bool clearAddressesDepthSlices = resource->viewType == VK_IMAGE_VIEW_TYPE_3D;
const Uint32 clearableLayers = clearAddressesDepthSlices
? std::max(resource->depth >> pendingClear.key.mipLevel, 1u)
: resource->arrayLayers;
MOBILEGL_ASSERT(pendingClear.key.baseArrayLayer + pendingClear.key.layerCount <= clearableLayers,
"MaterializePendingClearForTexture: textureId=%d pending clear layer span [%u, %u) exceeds %u",
texture.GetExternalIndex(), pendingClear.key.baseArrayLayer,
pendingClear.key.baseArrayLayer + pendingClear.key.layerCount, resource->arrayLayers);
pendingClear.key.baseArrayLayer + pendingClear.key.layerCount, clearableLayers);
// Whether this clear names a strict SUBSET of the level. A layered attachment
// (glFramebufferTexture) queues layerCount = the whole depth, a single-slice one
// (glFramebufferTextureLayer) queues 1 - so the key already distinguishes them, and it is
// the clear's span that decides, not the image's slice count. Reading the latter sent a
// layered clear of a 3D texture down the per-slice path, where it cleared slice zero and
// left the rest stale (geometry_shader.layered_framebuffer.clear_call_support).
const Bool clearsWholeLevel =
pendingClear.key.baseArrayLayer == 0 && pendingClear.key.layerCount >= clearableLayers;
if (clearAddressesDepthSlices && clearableLayers > 1 && !clearsWholeLevel) {
// vkCmdClearColorImage cannot clear a subset of a 3D image's slices:
// VUID-vkCmdClearColorImage-baseArrayLayer-01472 pins baseArrayLayer to 0 and
// layerCount to 1 for VK_IMAGE_TYPE_3D, i.e. the whole mip level. A render pass whose
// only content is its LOAD_OP_CLEAR does address exactly one slice, because its
// attachment is a 2D view over that slice.
auto clearPayload3D = pendingClear.payload;
PreCompensateSrgbClearColor(clearPayload3D, resource->format);
VkClearValue sliceClearValue{};
sliceClearValue.color = MakeVkClearColorValue(clearPayload3D, ColorFormatLacksAlpha(&texture));
if (!ClearDepthSliceWithRenderPass(commandBuffer, texture, pendingClear.key.mipLevel,
pendingClear.key.baseArrayLayer, sliceClearValue)) {
// The device or the format refused VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT, so
// there is no way to name this slice. Leaving it uncleared is wrong pixels;
// asserting would abort a process that glFramebufferTextureLayer can reach at will.
MGLOG_W("MaterializePendingClearForTexture: textureId=%d slice %u could not be cleared "
"(no 2D-array-compatible view)",
texture.GetExternalIndex(), pendingClear.key.baseArrayLayer);
}
continue;
}
VkImageSubresourceRange subresourceRange{};
subresourceRange.baseMipLevel = pendingClear.key.mipLevel;
subresourceRange.levelCount = 1;
subresourceRange.baseArrayLayer = pendingClear.key.baseArrayLayer;
subresourceRange.layerCount = pendingClear.key.layerCount;
// VUID-vkCmdClearColorImage-baseArrayLayer-01472: for a VK_IMAGE_TYPE_3D image the range
// must name baseArrayLayer 0 and layerCount 1, which Vulkan reads as "the whole mip
// level" - the z extent is not an array dimension. A layered GL clear queues
// layerCount = depth, which is the right GL answer and an illegal Vulkan one.
subresourceRange.baseArrayLayer = clearAddressesDepthSlices ? 0u : pendingClear.key.baseArrayLayer;
subresourceRange.layerCount = clearAddressesDepthSlices ? 1u : pendingClear.key.layerCount;
auto clearPayload = pendingClear.payload;
if ((resource->aspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0) {
@@ -9279,6 +9441,41 @@ void main() {
}
}
VkProvokingVertexModeEXT VulkanRenderer::SelectProvokingVertexMode(VkPrimitiveTopology topology,
Bool capturesXfbFromGeometryStage) const {
if (!m_provokingVertexLastEnabled) {
return VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT;
}
// Measured, and identical on lavapipe and on the NVIDIA Vulkan driver: a geometry shader's
// emitted triangle strip is already recorded in GL's provoking-last vertex order, so asking
// for LAST rotates it a second time. The input-assembler path has the opposite problem, and
// the mode is a single pipeline bit, so the two cannot be satisfied at once: a program that
// both runs a geometry shader and captures transform feedback keeps Vulkan's own convention,
// and pays for it with a GL-wrong flat vertex in that one case. Deliberately a link-time
// program property, not IsTransformFeedbackActive() - see the memo note in the header.
if (capturesXfbFromGeometryStage) {
return VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT;
}
// VUID-VkGraphicsPipelineCreateInfo-topology-04884 only bites when
// transformFeedbackPreservesProvokingVertex is enabled; when it is not, a fan may take LAST.
if (m_provokingVertexXfbPreserveEnabled && topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN &&
!m_provokingVertexFanPreserved) {
return VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT;
}
// Only provokingVertexModePerPipeline lets modes differ inside one render pass instance;
// elsewhere every pipeline takes GL's default so the render pass stays self-consistent, and
// glProvokingVertex(GL_FIRST_VERTEX_CONVENTION) goes unhonoured. Honouring it there would
// mean ending the render pass on every glProvokingVertex change; not worth it until a target
// device actually lacks the property.
if (!m_provokingVertexModePerPipeline) {
return VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT;
}
return (MG_State::pGLContext != nullptr &&
MG_State::pGLContext->GetProvokingVertexMode() == ProvokingVertexMode::FirstVertex)
? VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT
: VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT;
}
Bool VulkanRenderer::IsTimerQuerySupported() const {
return m_timerQuerySupported && m_timerQueryManager != nullptr;
}
@@ -9925,6 +10122,16 @@ void main() {
deviceFeatures.wideLines = supportedDeviceFeatures.wideLines;
m_logicOpFeatureEnabled = deviceFeatures.logicOp == VK_TRUE;
deviceFeatures.shaderInt64 = supportedDeviceFeatures.shaderInt64;
// Required for any module that declares OpCapability Float64 - which is every shader with a
// double in it, including the 64-bit vertex attribute path (the attribute itself arrives as
// uint32 words, but the bitcast result and everything computed from it is Float64). Without
// it vkCreateShaderModule is invalid usage (VUID-VkShaderModuleCreateInfo-pCode-08740),
// which is why SupportsFloat64VertexAttributes gates the entry point on the same feature.
deviceFeatures.shaderFloat64 = supportedDeviceFeatures.shaderFloat64;
// Required before a VK_IMAGE_VIEW_TYPE_CUBE_ARRAY view may be created
// (VUID-VkImageViewCreateInfo-viewType-01004). Without it a cube map array texture cannot
// get its sampled or full view, so SyncTextureResource fails and the texture stays unbacked.
deviceFeatures.imageCubeArray = supportedDeviceFeatures.imageCubeArray;
// Required for desktop GL image load/store semantics. iterationRP writes storage
// images from vertex and fragment stages and uses formats outside Vulkan's small
// mandatory storage-image set.
@@ -10105,6 +10312,79 @@ void main() {
MGLOG_I("Enabled optional device extension: %s", VK_EXT_TRANSFORM_FEEDBACK_EXTENSION_NAME);
}
}
// VK_EXT_provoking_vertex. Two independent features live behind one extension:
// provokingVertexLast -> flat varyings, gl_Layer/gl_ViewportIndex and
// the input-assembler capture order.
// transformFeedbackPreservesProvokingVertex -> spec-level guarantee for the capture order;
// only legal when the transformFeedback
// feature is also enabled, which is why this
// block sits after the one above.
// They are enabled independently on purpose: gating the first on the second would leave flat
// shading GL-wrong on any device without VK_EXT_transform_feedback, for no legality reason.
m_provokingVertexLastEnabled = false;
m_provokingVertexXfbPreserveEnabled = false;
m_provokingVertexModePerPipeline = false;
m_provokingVertexFanPreserved = false;
VkPhysicalDeviceProvokingVertexFeaturesEXT provokingVertexFeatures{};
provokingVertexFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROVOKING_VERTEX_FEATURES_EXT;
if (IsExtensionSupported(availableExtensions, VK_EXT_PROVOKING_VERTEX_EXTENSION_NAME) &&
getPhysicalDeviceFeatures2 != nullptr) {
VkPhysicalDeviceFeatures2 featureQuery{};
featureQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2;
featureQuery.pNext = &provokingVertexFeatures;
getPhysicalDeviceFeatures2(m_physicalDevice.handle, &featureQuery);
VkPhysicalDeviceProvokingVertexPropertiesEXT provokingVertexProperties{};
provokingVertexProperties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROVOKING_VERTEX_PROPERTIES_EXT;
auto getPhysicalDeviceProperties2 = reinterpret_cast<PFN_vkGetPhysicalDeviceProperties2>(
vkGetInstanceProcAddr(m_instance, "vkGetPhysicalDeviceProperties2"));
if (getPhysicalDeviceProperties2 == nullptr) {
getPhysicalDeviceProperties2 = reinterpret_cast<PFN_vkGetPhysicalDeviceProperties2>(
vkGetInstanceProcAddr(m_instance, "vkGetPhysicalDeviceProperties2KHR"));
}
if (getPhysicalDeviceProperties2 != nullptr) {
VkPhysicalDeviceProperties2 propertyQuery{};
propertyQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2;
propertyQuery.pNext = &provokingVertexProperties;
getPhysicalDeviceProperties2(m_physicalDevice.handle, &propertyQuery);
}
m_provokingVertexModePerPipeline = provokingVertexProperties.provokingVertexModePerPipeline == VK_TRUE;
m_provokingVertexFanPreserved =
provokingVertexProperties.transformFeedbackPreservesTriangleFanProvokingVertex == VK_TRUE;
if (provokingVertexFeatures.provokingVertexLast == VK_TRUE) {
// transformFeedbackPreservesProvokingVertex is deliberately NOT requested. Measured:
// asking for it regresses transform_feedback.geometry on GL33 through GL45. A
// geometry shader emits its triangles already in GL's vertex order, and the pipeline
// that captures them runs on FIRST (see SelectProvokingVertexMode); without the
// guarantee the driver leaves that stream alone, but with it the capture is forced to
// follow the pipeline's FIRST convention and comes back rotated. The guarantee buys
// nothing here either - the input-assembler capture order that
// direct_state_access.queries_functional needs comes from provokingVertexLast alone,
// which was confirmed by measurement. Leaving it off also keeps VU 04884 disarmed, so
// a TRIANGLE_FAN pipeline may take LAST on any device.
const Bool wantXfbPreserve = false;
if (!IsExtensionAlreadyEnabled(enabledDeviceExtensions, VK_EXT_PROVOKING_VERTEX_EXTENSION_NAME)) {
enabledDeviceExtensions.push_back(VK_EXT_PROVOKING_VERTEX_EXTENSION_NAME);
}
provokingVertexFeatures.provokingVertexLast = VK_TRUE;
provokingVertexFeatures.transformFeedbackPreservesProvokingVertex =
wantXfbPreserve ? VK_TRUE : VK_FALSE;
provokingVertexFeatures.pNext = const_cast<void*>(deviceCreateInfo.pNext);
deviceCreateInfo.pNext = &provokingVertexFeatures;
m_provokingVertexLastEnabled = true;
m_provokingVertexXfbPreserveEnabled = wantXfbPreserve;
MGLOG_I("Enabled optional device extension: %s (transformFeedbackPreservesProvokingVertex=%s)",
VK_EXT_PROVOKING_VERTEX_EXTENSION_NAME, wantXfbPreserve ? "true" : "false");
}
}
if (!m_provokingVertexLastEnabled) {
MGLOG_W("VK_EXT_provoking_vertex is unavailable; flat-shaded varyings take a primitive's first "
"vertex instead of GL's last, and transform feedback records TRIANGLE_STRIP/TRIANGLE_FAN "
"triangles rotated (0,1,2 / 1,3,2 instead of 0,1,2 / 2,1,3)");
}
if (!m_transformFeedbackFeatureEnabled) {
MGLOG_W("VK_EXT_transform_feedback is unavailable; transform feedback capture will not work");
}
@@ -498,6 +498,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// VK_EXT_transform_feedback (GL transform feedback capture)
Bool m_transformFeedbackFeatureEnabled = false;
// VK_EXT_provoking_vertex. Vulkan's built-in convention is "provoking vertex first"; GL's
// default is LAST_VERTEX_CONVENTION, and GL derives BOTH flat shading and the transform
// feedback vertex order from it. provokingVertexLast alone fixes flat shading and the
// input-assembler capture order and has no dependency on transform feedback; only
// transformFeedbackPreservesProvokingVertex does.
Bool m_provokingVertexLastEnabled = false;
// transformFeedbackPreservesProvokingVertex was actually enabled at device creation. Kept
// separate because it is the only thing that arms
// VUID-VkGraphicsPipelineCreateInfo-topology-04884, the rule that forbids a TRIANGLE_FAN
// pipeline from asking for LAST on a device that cannot preserve a fan's provoking vertex.
Bool m_provokingVertexXfbPreserveEnabled = false;
// provokingVertexModePerPipeline: when VK_FALSE every pipeline in one render pass instance
// must agree on the mode, so glProvokingVertex(GL_FIRST_VERTEX_CONVENTION) cannot be honoured
// per draw and every pipeline takes GL's default (LAST) instead.
Bool m_provokingVertexModePerPipeline = false;
// transformFeedbackPreservesTriangleFanProvokingVertex.
Bool m_provokingVertexFanPreserved = false;
// Per-pipeline provoking-vertex mode. capturesXfbFromGeometryStage must be a LINK-TIME
// property of the program, never the dynamic "is transform feedback active" flag: the
// 8-entry m_pipelineMemo and the SetupDrawSnapshot fast path key on programObj.hash and
// GetRenderStateParametersVersion(), neither of which moves when glBeginTransformFeedback is
// called, so a dynamic input here would hand back a stale VkPipeline.
VkProvokingVertexModeEXT SelectProvokingVertexMode(VkPrimitiveTopology topology,
Bool capturesXfbFromGeometryStage) const;
// 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;
@@ -791,6 +815,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
GLenum filter);
// Clears one z slice of a VK_IMAGE_TYPE_3D colour image. See the call site in
// MaterializePendingClearForTexture for why a transfer clear cannot do this.
Bool ClearDepthSliceWithRenderPass(VkCommandBuffer commandBuffer,
MG_State::GLState::ITextureObject& texture, Uint32 mipLevel,
Uint32 depthSlice, const VkClearValue& clearValue);
Bool MaterializePendingClearForTexture(VkCommandBuffer commandBuffer,
MG_State::GLState::ITextureObject& texture);
Bool MaterializePendingClearForRenderbuffer(
@@ -15,7 +15,7 @@
namespace MobileGL::MG_Impl::GLImpl {
static Bool ValidateCurrentProgramForExecution(const char* functionName) {
const auto& currentProgram = MG_State::pGLContext->GetCurrentProgram();
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
if (!currentProgram) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
@@ -37,7 +37,7 @@ namespace MobileGL::MG_Impl::GLImpl {
static Bool ValidateCurrentProgramForCompute(const char* functionName) {
if (!ValidateCurrentProgramForExecution(functionName)) return false;
const auto& currentProgram = MG_State::pGLContext->GetCurrentProgram();
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
if (currentProgram->GetShaderIndexByStage(ShaderStage::Compute) < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
@@ -173,7 +173,7 @@ namespace MobileGL::MG_Impl::GLImpl {
// 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 auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
const GLenum gsInput = currentProgram ? currentProgram->GetGeometryInputType() : GL_NONE;
if (gsInput != GL_NONE && mode != GL_PATCHES) {
Bool compatible = false;
@@ -707,7 +707,7 @@ namespace MobileGL::MG_Impl::GLImpl {
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Transform feedback is already active."));
return;
}
const auto& program = MG_State::pGLContext->GetCurrentProgram();
const auto& program = MG_State::pGLContext->GetProgramForDraw();
if (!program || !program->GetLinkStatus() || program->GetTransformFeedbackVaryingCount() == 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
@@ -320,7 +320,7 @@ DECLARE_GL_FUNCTION_HEAD(void, GetProgramResourceiv, GLuint program, GLenum prog
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_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_HEAD(GLuint, CreateShaderProgramv, GLenum type, GLsizei count, const GLchar* const* strings) DECLARE_GL_FUNCTION_END(GLuint, CreateShaderProgramv, type, count, strings)
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)
@@ -1069,11 +1069,36 @@ namespace MobileGL::MG_Impl::GLImpl {
case TextureTarget::Texture2DMultisampleArray:
textureUploadTarget = TextureUploadTarget::Texture2DMultisampleArray;
break;
case TextureTarget::Texture1DArray:
textureUploadTarget = TextureUploadTarget::Texture1DArray;
break;
case TextureTarget::TextureCubeMapArray:
textureUploadTarget = TextureUploadTarget::CubeMapArray;
break;
default:
RecordUnsupportedFramebufferTextureAttachmentError(
__func__, "FramebufferTextureLayer requires a 3D, 2D array or 2D multisample array texture.");
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"FramebufferTextureLayer requires a 3D, array, 2D multisample "
"array, or cube map array texture."));
return;
}
// The same backend question the DSA twin asks. GL 4.6 core 9.2.8 makes the two entry points
// equivalent, so they have to decline in the same places - leaving this one ungated is what
// let an unrepresentable attachment reach the renderer, and it also refused cube map arrays
// that GL requires it to accept.
{
const auto& layerLimits = MG_Backend::pActiveBackendObject
? MG_Backend::pActiveBackendObject->GetDynamicParameters()
: MG_Backend::DynamicBackendParameters{};
const TextureTarget layeredTarget = textureObject->GetTarget();
if ((layer != 0 || layeredTarget == TextureTarget::TextureCubeMapArray) &&
!layerLimits.SupportsPerLayerFramebufferAttachment(layeredTarget)) {
RecordUnsupportedFramebufferTextureAttachmentError(
__func__, "This backend does not resolve a framebuffer attachment's layer onto its image.");
return;
}
}
AttachFramebufferTextureLayer(__func__, target, attachment, texture, level, layer, textureUploadTarget);
}
@@ -1433,11 +1458,19 @@ namespace MobileGL::MG_Impl::GLImpl {
// so a slice lands outside the image and the renderer asserts on the clear. Letting it
// through there would only move the failure downstream, so it is declined instead - layer
// zero always works, being the plain first-slice attachment.
const Bool backsLayeredAttachment = limits.SupportsPerLayerFramebufferAttachment;
// A cube map array additionally has no image shape at all in VkTextureManager, so on that
// backend it cannot be an attachment whatever the layer is.
const Bool isCubeMapArray = textureObject->GetTarget() == TextureTarget::TextureCubeMapArray;
if ((layer != 0 && !backsLayeredAttachment) || (isCubeMapArray && !backsLayeredAttachment)) {
// ...and it is a DIFFERENT question per target: a 2D/2D-multisample array layer is a Vulkan
// array layer, a 3D layer is a z slice, and a cube map array needs a cube-compatible image
// before it has any layer to name. Ask the backend about this texture's target rather than
// guessing from one blanket flag.
const TextureTarget layeredTextureTarget = textureObject->GetTarget();
const Bool backsThisTargetsLayers = limits.SupportsPerLayerFramebufferAttachment(layeredTextureTarget);
// Layer zero of a 3D or array texture is the plain first-slice attachment every backend can
// already express, so it stays legal even where per-layer selection is not backed. A cube map
// array has no such fallback: layer zero is still one face of one cube inside a
// cube-compatible image, so it needs the same support layer 5 does.
const Bool needsPerLayerSupport =
layer != 0 || layeredTextureTarget == TextureTarget::TextureCubeMapArray;
if (needsPerLayerSupport && !backsThisTargetsLayers) {
RecordUnsupportedFramebufferTextureAttachmentError(
__func__, "This backend does not resolve a framebuffer attachment's layer onto its image.");
return;
+57 -15
View File
@@ -701,6 +701,9 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_PROGRAM_BINARY_RETRIEVABLE_HINT:
*params = programObject->GetBinaryRetrievableHint() ? GL_TRUE : GL_FALSE;
break;
case GL_PROGRAM_SEPARABLE:
*params = programObject->GetSeparable() ? GL_TRUE : GL_FALSE;
break;
case GL_GEOMETRY_VERTICES_OUT:
case GL_GEOMETRY_INPUT_TYPE:
@@ -1094,7 +1097,7 @@ namespace MobileGL::MG_Impl::GLImpl {
void Uniformv_State(GLint location, GLsizei count, T* value) {
if (location == -1) return;
auto& programObject = MG_State::pGLContext->GetCurrentProgram();
auto& programObject = MG_State::pGLContext->GetProgramForUniform();
if (programObject == nullptr) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
@@ -1286,7 +1289,7 @@ namespace MobileGL::MG_Impl::GLImpl {
// If transpose is GL_TRUE, we need to transpose the matrix data
if (location == -1) return;
auto& programObject = MG_State::pGLContext->GetCurrentProgram();
auto& programObject = MG_State::pGLContext->GetProgramForUniform();
if (programObject == nullptr) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
@@ -1321,7 +1324,7 @@ namespace MobileGL::MG_Impl::GLImpl {
// If transpose is GL_TRUE, we need to transpose the matrix data
if (location == -1) return;
auto& programObject = MG_State::pGLContext->GetCurrentProgram();
auto& programObject = MG_State::pGLContext->GetProgramForUniform();
if (programObject == nullptr) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
@@ -1361,7 +1364,7 @@ namespace MobileGL::MG_Impl::GLImpl {
// If transpose is GL_TRUE, we need to transpose the matrix data
if (location == -1) return;
auto& programObject = MG_State::pGLContext->GetCurrentProgram();
auto& programObject = MG_State::pGLContext->GetProgramForUniform();
if (programObject == nullptr) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
@@ -1394,7 +1397,7 @@ namespace MobileGL::MG_Impl::GLImpl {
void UniformMatrixNonSquarefv_State(const char* caller, GLint location, GLsizei count) {
if (location == -1) return;
auto& programObject = MG_State::pGLContext->GetCurrentProgram();
auto& programObject = MG_State::pGLContext->GetProgramForUniform();
if (programObject == nullptr) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
@@ -2055,7 +2058,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void UniformMatrix2dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
if (location == -1) return;
auto& programObject = MG_State::pGLContext->GetCurrentProgram();
auto& programObject = MG_State::pGLContext->GetProgramForUniform();
if (programObject == nullptr) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
@@ -2081,7 +2084,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void UniformMatrix3dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
if (location == -1) return;
auto& programObject = MG_State::pGLContext->GetCurrentProgram();
auto& programObject = MG_State::pGLContext->GetProgramForUniform();
if (programObject == nullptr) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
@@ -2107,7 +2110,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void UniformMatrix4dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
if (location == -1) return;
auto& programObject = MG_State::pGLContext->GetCurrentProgram();
auto& programObject = MG_State::pGLContext->GetProgramForUniform();
if (programObject == nullptr) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
@@ -2133,7 +2136,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void UniformMatrix2x3dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
if (location == -1) return;
auto& programObject = MG_State::pGLContext->GetCurrentProgram();
auto& programObject = MG_State::pGLContext->GetProgramForUniform();
if (programObject == nullptr) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
@@ -2159,7 +2162,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void UniformMatrix2x4dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
if (location == -1) return;
auto& programObject = MG_State::pGLContext->GetCurrentProgram();
auto& programObject = MG_State::pGLContext->GetProgramForUniform();
if (programObject == nullptr) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
@@ -2185,7 +2188,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void UniformMatrix3x2dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
if (location == -1) return;
auto& programObject = MG_State::pGLContext->GetCurrentProgram();
auto& programObject = MG_State::pGLContext->GetProgramForUniform();
if (programObject == nullptr) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
@@ -2211,7 +2214,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void UniformMatrix3x4dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
if (location == -1) return;
auto& programObject = MG_State::pGLContext->GetCurrentProgram();
auto& programObject = MG_State::pGLContext->GetProgramForUniform();
if (programObject == nullptr) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
@@ -2237,7 +2240,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void UniformMatrix4x2dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
if (location == -1) return;
auto& programObject = MG_State::pGLContext->GetCurrentProgram();
auto& programObject = MG_State::pGLContext->GetProgramForUniform();
if (programObject == nullptr) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
@@ -2263,7 +2266,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void UniformMatrix4x3dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
if (location == -1) return;
auto& programObject = MG_State::pGLContext->GetCurrentProgram();
auto& programObject = MG_State::pGLContext->GetProgramForUniform();
if (programObject == nullptr) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
@@ -2746,7 +2749,7 @@ namespace MobileGL::MG_Impl::GLImpl {
void ProgramParameteri(GLuint program, GLenum pname, GLint value) {
auto& programObject = TryToGetProgramObject(program);
if (!programObject) return;
if (pname != GL_PROGRAM_BINARY_RETRIEVABLE_HINT) {
if (pname != GL_PROGRAM_BINARY_RETRIEVABLE_HINT && pname != GL_PROGRAM_SEPARABLE) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "pname is not an accepted value."));
@@ -2758,9 +2761,48 @@ namespace MobileGL::MG_Impl::GLImpl {
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "value must be GL_TRUE or GL_FALSE."));
return;
}
if (pname == GL_PROGRAM_SEPARABLE) {
programObject->SetSeparable(value == GL_TRUE);
return;
}
programObject->SetBinaryRetrievableHint(value == GL_TRUE);
}
// GL 4.6 core 7.3: glCreateShaderProgramv is defined as the exact sequence below, so it
// is written as that sequence rather than as a private shortcut - every error it can
// raise is one of theirs, raised at the point they would raise it.
GLuint CreateShaderProgramv(GLenum type, GLsizei count, const GLchar* const* strings) {
const GLuint shader = CreateShader_State(type);
if (shader == 0) return 0;
ShaderSource_State(shader, count, strings, nullptr);
CompileShader_State(shader);
const GLuint program = CreateProgram_State();
if (program != 0) {
const auto& shaderObject = MG_State::pGLContext->GetShaderObject(shader);
const auto& programObject = MG_State::pGLContext->GetProgramObject(program);
// The program is separable whether or not the shader compiled: a failed
// compile leaves an unlinked but otherwise well-formed separable program.
if (programObject) programObject->SetSeparable(true);
if (shaderObject && programObject && shaderObject->GetCompileStatus()) {
AttachShader_State(program, shader);
// Not LinkProgram_State: that injects a default fragment shader into a
// program that has none, which is exactly wrong for a separable
// vertex-stage program - the pipeline supplies the real one.
programObject->Link(false);
// glDetachShader defers the removal to the next link, so the program keeps
// the shader object it was built from while no longer reporting it attached.
DetachShader_State(program, shader);
}
if (shaderObject && programObject && !shaderObject->GetInfoLog().empty()) {
programObject->AppendInfoLog(shaderObject->GetInfoLog());
}
}
DeleteShader_State(shader);
return program;
}
void GetProgramBinary(GLuint program, GLsizei bufSize, GLsizei* length, GLenum* binaryFormat, void* binary) {
(void)binaryFormat;
(void)binary;
@@ -174,6 +174,7 @@ namespace MobileGL::MG_Impl::GLImpl {
void GetUniformdv(GLuint program, GLint location, GLdouble* params);
void ValidateProgram(GLuint program);
void ProgramParameteri(GLuint program, GLenum pname, GLint value);
GLuint CreateShaderProgramv(GLenum type, GLsizei count, const GLchar* const* strings);
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);
+218 -36
View File
@@ -216,8 +216,23 @@ namespace MobileGL::MG_Impl::GLImpl {
// compressed format up front, so no texture image MobileGL holds can be compressed. Written
// as a predicate rather than a literal false so both level-parameter getters stay in step
// once compressed formats do land.
Bool IsCompressedTextureFormat(TextureInternalFormat) {
return false;
// GL 4.6 core 8.11 asks "is *this level* stored compressed", not "is the texture's internal
// format a compressed one", and here the two genuinely differ: a compressed internalformat
// handed to glTexImage2D resolves to the uncompressed storage that backs it (see
// ConvertGLEnumToTextureInternalFormat), so the texture's format enum can never answer yes.
// The only levels stored compressed are the ones glCompressedTexImage* shadowed verbatim,
// which is exactly what the per-level compressed format records.
//
// No level-count guard on purpose: TextureObject2DCube::GetMipmapLevelCount() reports face
// zero's chain only, so a count check would answer GL_NONE for a compressed image on any
// other face - precisely the per-face independence the storage layer provides. MipmapStorage's
// own getters already bounds-check per target and return GL_NONE for an unallocated level.
GLenum GetCompressedLevelFormat(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
TextureUploadTarget uploadTarget, GLint level) {
if (!textureObject || level < 0) return GL_NONE;
const auto* textureMipmapObject = MG_State::GLState::AsMipmapTexture(textureObject.get());
if (!textureMipmapObject) return GL_NONE;
return textureMipmapObject->GetMipmapCompressedFormat(uploadTarget, static_cast<Uint>(level));
}
GLint GetTextureLevelComponentParameter(TextureInternalFormat textureInternalFormat, GLenum pname) {
@@ -538,10 +553,12 @@ namespace MobileGL::MG_Impl::GLImpl {
texture->TruncateMipmapLevels(uploadTarget, 1);
}
// Compressed texture upload is not implemented yet. GL_NUM_COMPRESSED_TEXTURE_FORMATS
// reports 0, so every compressed internalformat is by definition unsupported and
// GL_INVALID_ENUM is the specified error - unlike THROW_UNIMPL_EXCEPTION, which unwinds
// a C++ exception through the C GL ABI and takes the process down.
// The compressed internalformat is not one this stack can store (see
// MG_Util::GetCompressedFormatInfo for the accepted set: the RGTC/BPTC/ETC2-EAC formats core
// GL requires). GL_INVALID_ENUM is the specified error for an unsupported compressed format -
// unlike THROW_UNIMPL_EXCEPTION, which unwinds a C++ exception through the C GL ABI and takes
// the process down. Still the only outcome for the 1D/3D and sub-image entry points, which
// have no compressed upload path yet.
void RecordUnsupportedCompressedFormat(const char* caller) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
@@ -2877,7 +2894,14 @@ namespace MobileGL::MG_Impl::GLImpl {
break;
case GL_TEXTURE_INTERNAL_FORMAT:
if (params) {
*params = (GLint)MG_Util::ConvertTextureInternalFormatToGLEnum(textureObject->GetFormat());
// A level stored compressed must report the token it was given, not the
// uncompressed format backing it (GL 4.6 core 8.11). Only glCompressedTexImage* sets
// that tag, so every level created by glTexImage*D - including one given a compressed
// internalformat - still answers with its resolved storage format.
const GLenum compressedFormat = GetCompressedLevelFormat(textureObject, textureUploadTarget, level);
*params = (compressedFormat != GL_NONE)
? (GLint)compressedFormat
: (GLint)MG_Util::ConvertTextureInternalFormatToGLEnum(textureObject->GetFormat());
}
break;
case GL_TEXTURE_SAMPLES:
@@ -2907,14 +2931,16 @@ namespace MobileGL::MG_Impl::GLImpl {
break;
case GL_TEXTURE_COMPRESSED:
if (params) {
*params = IsCompressedTextureFormat(textureObject->GetFormat()) ? GL_TRUE : GL_FALSE;
*params =
(GetCompressedLevelFormat(textureObject, textureUploadTarget, level) != GL_NONE) ? GL_TRUE
: GL_FALSE;
}
break;
case GL_TEXTURE_COMPRESSED_IMAGE_SIZE:
case GL_TEXTURE_COMPRESSED_IMAGE_SIZE: {
// GL 4.6 core 8.11: there is no compressed size to report for an image whose internal
// format is uncompressed, nor for a proxy target, and the query is INVALID_OPERATION
// rather than a zero.
if (isProxy || !IsCompressedTextureFormat(textureObject->GetFormat())) {
if (isProxy || GetCompressedLevelFormat(textureObject, textureUploadTarget, level) == GL_NONE) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
@@ -2923,9 +2949,12 @@ namespace MobileGL::MG_Impl::GLImpl {
return;
}
if (params) {
*params = 0;
const auto* textureMipmapObject = MG_State::GLState::AsMipmapTexture(textureObject.get());
*params = static_cast<GLint>(
textureMipmapObject->GetMipmapCompressedByteSize(textureUploadTarget, static_cast<Uint>(level)));
}
break;
}
default:
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetTexLevelParameteriv_State",
@@ -3000,7 +3029,14 @@ namespace MobileGL::MG_Impl::GLImpl {
break;
case GL_TEXTURE_INTERNAL_FORMAT:
if (params) {
*params = (GLfloat)MG_Util::ConvertTextureInternalFormatToGLEnum(textureObject->GetFormat());
// A level stored compressed must report the token it was given, not the
// uncompressed format backing it (GL 4.6 core 8.11). Only glCompressedTexImage* sets
// that tag, so every level created by glTexImage*D - including one given a compressed
// internalformat - still answers with its resolved storage format.
const GLenum compressedFormat = GetCompressedLevelFormat(textureObject, textureUploadTarget, level);
*params = (GLfloat)((compressedFormat != GL_NONE)
? compressedFormat
: MG_Util::ConvertTextureInternalFormatToGLEnum(textureObject->GetFormat()));
}
break;
case GL_TEXTURE_SAMPLES:
@@ -3030,13 +3066,14 @@ namespace MobileGL::MG_Impl::GLImpl {
break;
case GL_TEXTURE_COMPRESSED:
if (params) {
*params = IsCompressedTextureFormat(textureObject->GetFormat()) ? 1.0f : 0.0f;
*params =
(GetCompressedLevelFormat(textureObject, textureUploadTarget, level) != GL_NONE) ? 1.0f : 0.0f;
}
break;
case GL_TEXTURE_COMPRESSED_IMAGE_SIZE:
case GL_TEXTURE_COMPRESSED_IMAGE_SIZE: {
// See GetTexLevelParameteriv_State: uncompressed images and proxy targets have no
// compressed size to report, so GL 4.6 core 8.11 makes the query an error.
if (isProxy || !IsCompressedTextureFormat(textureObject->GetFormat())) {
if (isProxy || GetCompressedLevelFormat(textureObject, textureUploadTarget, level) == GL_NONE) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
@@ -3045,9 +3082,12 @@ namespace MobileGL::MG_Impl::GLImpl {
return;
}
if (params) {
*params = 0.0f;
const auto* textureMipmapObject = MG_State::GLState::AsMipmapTexture(textureObject.get());
*params = static_cast<GLfloat>(
textureMipmapObject->GetMipmapCompressedByteSize(textureUploadTarget, static_cast<Uint>(level)));
}
break;
}
default:
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetTexLevelParameterfv_State",
@@ -3056,14 +3096,72 @@ namespace MobileGL::MG_Impl::GLImpl {
}
}
// The half glGetCompressedTexImage and glGetCompressedTextureImage share, factored out for the
// same reason ValidateTextureImageQuery was: the by-name entry point must not drift away from
// the by-target one's rules. bufSize < 0 means "no destination-size argument" - the by-target
// form has none (GL 4.6 core 8.11 has the caller size it from GL_TEXTURE_COMPRESSED_IMAGE_SIZE),
// so only the DSA form passes a real bound.
void CopyCompressedTextureImageToClientOrPBO(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
TextureUploadTarget uploadTarget, GLint level, GLsizei bufSize,
void* pixels, const char* caller) {
if (GetCompressedLevelFormat(textureObject, uploadTarget, level) == GL_NONE) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"Texture level is not stored in a compressed format."));
return;
}
const auto* textureMipmapObject = MG_State::GLState::AsMipmapTexture(textureObject.get());
const SizeT imageSize =
textureMipmapObject->GetMipmapCompressedByteSize(uploadTarget, static_cast<Uint>(level));
const void* src = textureMipmapObject->MapMipmapCompressedImage(uploadTarget, static_cast<Uint>(level));
if (!src || imageSize == 0) return;
if (bufSize >= 0 && static_cast<SizeT>(bufSize) < imageSize) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Destination buffer is too small."));
return;
}
const auto& pixelPackBufferObject =
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
if (pixelPackBufferObject) {
if (pixelPackBufferObject->IsMapped()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Pixel pack buffer is currently mapped."));
return;
}
const SizeT offset = reinterpret_cast<SizeT>(pixels);
const SizeT bufferSize = pixelPackBufferObject->GetSize();
if (offset > bufferSize || imageSize > bufferSize - offset) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"Packing would write past the end of the pixel pack buffer."));
return;
}
pixelPackBufferObject->UploadSubData({const_cast<void*>(src), imageSize}, offset);
return;
}
// No pixel-store packing here on purpose: GL 4.6 core 8.11 says the pixel storage modes are
// ignored for a compressed image, which is also the only way the round trip stays byte-exact.
if (pixels) Memcpy(pixels, src, imageSize);
}
void GetCompressedTexImage_State(GLenum target, GLint level, void* img) {
// TODO: implement compressed readback. Reporting success while writing nothing hands
// the caller stale memory with GL_NO_ERROR; no texture can be compressed yet, and GL
// specifies GL_INVALID_OPERATION when the bound level is not compressed.
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"Texture level is not stored in a compressed format."));
const auto textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
const auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
// ValidateTextureUploadTarget records InvalidEnum itself; wrapping it in a second RecordError
// would report one failure twice.
if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return;
if (!TextureImpl::ValidateTextureLevelNumber(level)) return;
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
CopyCompressedTextureImageToClientOrPBO(textureObject, textureUploadTarget, level, -1, img, __func__);
}
void GenTextures_State(GLsizei n, GLuint* textures) {
@@ -3314,16 +3412,102 @@ namespace MobileGL::MG_Impl::GLImpl {
void CompressedTexImage2D_State(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height,
GLint border, GLsizei imageSize, const void* data) {
// ======================= Converting ================================
const auto textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
const auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
// Zero block width doubles as "internalformat is not a specific compressed format", which is
// the INVALID_ENUM case - one lookup answers both questions.
const auto compressedInfo = MG_Util::GetCompressedFormatInfo(internalformat);
// ===================== Error Checking ==============================
if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return;
if (!TextureImpl::ValidateTextureLevelNumber(level)) return;
if (!TextureImpl::ValidateTextureSizeWithTextureUploadTarget(textureUploadTarget, width, height)) return;
if (!TextureImpl::ValidateTextureSizeRange(width, height, 1)) return;
if (!TextureImpl::ValidateTextureBorderNumber(border)) return;
if (!TextureImpl::ValidateTextureLevelWithUploadTarget(textureUploadTarget, level)) return;
if (compressedInfo.blockWidth == 0) {
RecordUnsupportedCompressedFormat(__func__);
return;
}
// GL 4.6 core 8.7: imageSize must be exactly the size the format and dimensions imply,
// otherwise INVALID_VALUE. This is also the guard that keeps the copy below in bounds.
const SizeT expectedImageSize =
MG_Util::CalculateCompressedTextureImageSize(compressedInfo, {width, height, 1});
if (imageSize < 0 || static_cast<SizeT>(imageSize) != expectedImageSize) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"imageSize does not match the compressed image size."));
return;
}
// Object resolution copied from TexImage2D_State rather than routed through
// GetTextureObjectByTarget: GL 4.6 core 8.7 lets a proxy target reach glCompressedTexImage2D,
// and only CreateOrReplaceProxyTextureObject gives the proxy a fresh object to answer the
// level queries from.
auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget);
const Bool isProxy = TextureImpl::IsProxyTextureTarget(textureUploadTarget);
auto& textureObject =
isProxy ? TextureImpl::pProxyTextureManager->CreateOrReplaceProxyTextureObject(textureUploadTarget)
: bindingSlot.GetBoundObject();
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
if (!ValidateTextureMutable(textureObject, __func__)) return;
// TODO: implement compressed upload. Until then report the spec error for an
// unsupported compressed format rather than throwing - a C++ exception unwinding
// through the C GL ABI is a hard crash for the caller, while GL_INVALID_ENUM is
// exactly what GL_NUM_COMPRESSED_TEXTURE_FORMATS == 0 promises.
RecordUnsupportedCompressedFormat(__func__);
// ======================= Processing ================================
// Texel storage stays uncompressed, exactly the deviation the RGTC/BPTC/ETC2 arms of
// ConvertGLEnumToTextureInternalFormat already document: neither backend has a BC/ETC codec
// and TextureInternalFormat has no compressed enumerator, so the shadow keeps the "one
// format, N bytes per texel" layout the backend upload sizing, glGenerateMipmap's
// bytes-per-texel division and the pixel-store packer all rely on. The image therefore
// samples as zeros. The application's bytes are kept beside it so glGetCompressedTexImage can
// return the image *as stored*, which GL 4.6 core 8.11 requires and which no re-encode could
// satisfy byte for byte.
const TextureInternalFormat textureInternalFormat =
MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);
textureObject->SetInternalFormat(textureInternalFormat);
// A proxy records the format and nothing else - it must never take storage, and it must never
// be tagged compressed, or GL_TEXTURE_COMPRESSED_IMAGE_SIZE on a proxy would stop being
// INVALID_OPERATION.
if (isProxy) return;
const SizeT internalBpp =
MG_Util::GetInternalBytesPerPixel(textureInternalFormat, TexturePixelDataType::UnsignedByte);
const SizeT internalBytes = static_cast<SizeT>(width) * static_cast<SizeT>(height) * internalBpp;
auto* textureMipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level);
// AllocateStorage clears any compressed image the level used to hold, so this must run before
// SetMipmapCompressedImage re-arms it.
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, height, 1}, internalBytes});
const void* compressedBytes = data;
const auto& pixelUnpackBufferObject =
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelUnpack).GetBoundObject();
if (pixelUnpackBufferObject) {
if (pixelUnpackBufferObject->IsMapped()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"Pixel unpack buffer is currently mapped."));
return;
}
const SizeT offset = reinterpret_cast<SizeT>(data);
const SizeT bufferSize = pixelUnpackBufferObject->GetSize();
if (offset > bufferSize || expectedImageSize > bufferSize - offset) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"Unpacking would read past the end of the pixel unpack buffer."));
return;
}
compressedBytes = reinterpret_cast<const char*>(pixelUnpackBufferObject->MappedData()) + offset;
}
textureMipmapObject->SetMipmapCompressedImage(textureUploadTarget, level, internalformat, compressedBytes,
expectedImageSize);
textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, true);
}
void CompressedTexImage1D_State(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLint border,
@@ -4273,13 +4457,11 @@ namespace MobileGL::MG_Impl::GLImpl {
// texture would also fail the compressed check below.
if (!TextureImpl::ValidateTextureLevelNumber(level)) return;
// No texture MobileGL holds is compressed (see IsCompressedTextureFormat), so this is the
// only outcome today. Reporting success while writing nothing would hand the caller stale
// memory with GL_NO_ERROR - the same reasoning as GetCompressedTexImage_State.
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"Texture level is not stored in a compressed format."));
// Unlike glGetTextureImage this never asks a backend: the compressed image only ever exists
// in the CPU shadow (no backend was handed the compressed bytes at all), so the shadow is
// authoritative rather than potentially stale.
CopyCompressedTextureImageToClientOrPBO(textureObject, GetPrimaryUploadTarget(textureObject), level, bufSize,
pixels, __func__);
}
void GetTextureSubImage(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width,
@@ -8,6 +8,7 @@
#include "GL_VertexArray.h"
#include "Validators.h"
#include <MG_Backend/BackendObjects.h>
#include <MG_Impl/GLImpl/Buffer/Validators.h>
#include <MG_State/GLState/Core.h>
#include <MG_State/GLState/ErrorState/Error.h>
@@ -173,6 +174,9 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_CURRENT_VERTEX_ATTRIB:
case GL_VERTEX_ATTRIB_ARRAY_BUFFER_BINDING:
case GL_VERTEX_ATTRIB_ARRAY_INTEGER:
// Core since GL 4.1 (ARB_vertex_attrib_64bit). It was rejected while no attribute could
// ever be long; now that IsLong is real state the pname has to be accepted.
case GL_VERTEX_ATTRIB_ARRAY_LONG:
case GL_VERTEX_ATTRIB_ARRAY_DIVISOR:
case GL_VERTEX_ATTRIB_ARRAY_POINTER:
return true;
@@ -460,19 +464,38 @@ namespace MobileGL::MG_Impl::GLImpl {
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,
// The long (64-bit) attribute format: the values reach the shader as doubles, unconverted
// (GL 4.6 core 10.3.2). ValidateVertexAttribLFormat has already pinned type to GL_DOUBLE, so the
// recorded DataType is always Float64 - what IsLong adds is that this is the *unconverted* form,
// as opposed to VertexAttribFormat(GL_DOUBLE), which asks for a float conversion.
//
// Whether the backend can feed it is detected, not assumed: DirectVulkan needs shaderFloat64,
// and DirectGLES can never have it at all. A backend without it declines here, loudly - GL error
// plus a log line naming the reason - rather than accepting state no draw could honour and
// rendering garbage. The matching startup POST row is in MG_Util/SelfTest/DriverPost.cpp.
static void VertexAttribLFormatSeparate_State(const SharedPtr<MG_State::GLState::VertexArrayObject>& vao,
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."));
if (!MG_Backend::pActiveBackendObject ||
!MG_Backend::pActiveBackendObject->GetDynamicParameters().SupportsFloat64VertexAttributes) {
MGLOG_I("VertexAttribLFormat: attribute %u asked for a 64-bit (GL_DOUBLE) format, but this "
"backend has no double-precision vertex attribute support - see the "
"\"64-bit vertex attributes\" / \"shaderFloat64\" POST row for what that costs",
attribindex);
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "VertexAttribLFormat",
"64-bit vertex attributes are not supported by this backend."));
return;
}
vao->SetAttributeFormatSeparate(attribindex, size, MG_Util::ConvertGLEnumToDataType(type),
/*normalized: */ false, /*isInteger: */ false, relativeoffset,
/*isBgra: */ false, /*isLong: */ true);
}
void VertexArrayAttribFormat_State(GLuint vaobj, GLuint attribindex, GLint size, GLenum type,
@@ -915,6 +938,9 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_VERTEX_ATTRIB_ARRAY_INTEGER:
params[0] = attr->IsInteger ? 1.0f : 0.0f;
return;
case GL_VERTEX_ATTRIB_ARRAY_LONG:
params[0] = attr->IsLong ? 1.0f : 0.0f;
return;
case GL_VERTEX_ATTRIB_ARRAY_DIVISOR:
params[0] = static_cast<GLfloat>(attr->Divisor);
return;
@@ -975,6 +1001,9 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_VERTEX_ATTRIB_ARRAY_INTEGER:
params[0] = attr->IsInteger ? 1.0 : 0.0;
return;
case GL_VERTEX_ATTRIB_ARRAY_LONG:
params[0] = attr->IsLong ? 1.0 : 0.0;
return;
case GL_VERTEX_ATTRIB_ARRAY_DIVISOR:
params[0] = static_cast<GLdouble>(attr->Divisor);
return;
@@ -1031,6 +1060,9 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_VERTEX_ATTRIB_ARRAY_INTEGER:
params[0] = attr->IsInteger ? GL_TRUE : GL_FALSE;
return;
case GL_VERTEX_ATTRIB_ARRAY_LONG:
params[0] = attr->IsLong ? GL_TRUE : GL_FALSE;
return;
case GL_VERTEX_ATTRIB_ARRAY_DIVISOR:
params[0] = static_cast<GLint>(attr->Divisor);
return;
@@ -1164,8 +1196,7 @@ namespace MobileGL::MG_Impl::GLImpl {
*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;
*param = attr.IsLong ? GL_TRUE : GL_FALSE;
return;
case GL_VERTEX_ATTRIB_ARRAY_DIVISOR:
*param = static_cast<GLint>(attr.Divisor);
@@ -1259,13 +1290,13 @@ namespace MobileGL::MG_Impl::GLImpl {
void VertexAttribLFormat(GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) {
auto vao = GetBoundVertexArrayOrError("VertexAttribLFormat");
if (!vao) return;
VertexAttribLFormatSeparate_State(attribindex, size, type, relativeoffset);
VertexAttribLFormatSeparate_State(vao, 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);
VertexAttribLFormatSeparate_State(vao, attribindex, size, type, relativeoffset);
}
void VertexAttribBinding(GLuint attribindex, GLuint bindingindex) {
+53
View File
@@ -343,7 +343,60 @@ namespace MobileGL::MG_State {
return m_programState.GetCurrentProgram();
}
const SharedPtr<ProgramObject>& GLContext::GetProgramForDraw() {
static const SharedPtr<ProgramObject> nullProgram = nullptr;
const auto& currentProgram = m_programState.GetCurrentProgram();
if (currentProgram) return currentProgram;
if (m_boundProgramPipeline == 0) return nullProgram;
const auto& pipeline = GetBoundProgramPipeline();
if (!pipeline) return nullProgram;
const auto signature = pipeline->ComputeDrawProgramSignature();
if (const auto& cached = pipeline->GetCachedDrawProgram(signature)) return cached;
// Everything downstream of here - the backends, the uniform plumbing, the draw
// validation - is written against a single linked program, so the pipeline is
// flattened into one. Each stage contributes only the shaders that serve it, so a
// program bound to two stages is not pulled in twice and a program bound to a
// stage it does not implement contributes nothing.
// Deliberately not a named program: it is reachable only through the pipeline, it
// must not answer glIsProgram, and it must not consume a name the application
// could otherwise be handed. Backend registries key on the object, not the name.
auto composite = MakeShared<ProgramObject>(0u);
Bool anyStage = false;
for (SizeT stage = 0; stage < static_cast<SizeT>(ShaderStage::ShaderStageCount); ++stage) {
const auto& stageProgram = pipeline->GetStageProgram(static_cast<ShaderStage>(stage));
if (!stageProgram) continue;
for (const auto& shader : stageProgram->GetAttachedShaders()) {
if (!shader || static_cast<SizeT>(shader->GetShaderStage()) != stage) continue;
composite->AttachShader(shader);
anyStage = true;
}
}
if (!anyStage) return nullProgram;
// A pipeline with no fragment stage still rasterises, so the default fragment
// shader is wanted here even though the separable stage programs never get one.
composite->Link(true);
pipeline->SetCachedDrawProgram(signature, Move(composite));
return pipeline->GetCachedDrawProgram(signature);
}
const SharedPtr<ProgramObject>& GLContext::GetProgramForUniform() {
const auto& currentProgram = m_programState.GetCurrentProgram();
if (currentProgram) return currentProgram;
static const SharedPtr<ProgramObject> nullProgram = nullptr;
if (m_boundProgramPipeline == 0) return nullProgram;
const auto& pipeline = GetBoundProgramPipeline();
if (!pipeline) return nullProgram;
return pipeline->GetActiveProgram();
}
// RenderState
Uint GLContext::GetPipelineStateVersion() const {
return m_renderState.GetPipelineStateVersion();
}
Uint GLContext::GetRenderStateParametersVersion() const {
return m_renderState.GetVersion();
}
+8
View File
@@ -137,6 +137,12 @@ namespace MobileGL {
const SharedPtr<ShaderObject>& GetShaderObject(Uint index);
void UseProgram(Uint program);
const SharedPtr<ProgramObject>& GetCurrentProgram();
// What a draw or dispatch actually executes: the program in use, or - when
// there is none - the bound pipeline's stages composited into one program.
const SharedPtr<ProgramObject>& GetProgramForDraw();
// What glUniform* addresses: the program in use, or the bound pipeline's
// active program (GL 4.6 core 7.6.1).
const SharedPtr<ProgramObject>& GetProgramForUniform();
// 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
@@ -153,6 +159,8 @@ namespace MobileGL {
// RenderState
Uint GetRenderStateParametersVersion() const;
// Only the pipeline-relevant subset - see RenderState::m_pipelineStateVersion.
Uint GetPipelineStateVersion() const;
const RenderStateParameters& GetRenderStateParameters() const;
void SetViewport(IntVec4 viewport); // x, y, width, height
const IntVec4& GetViewport() const; // x, y, width, height
@@ -45,6 +45,13 @@ namespace MobileGL::MG_State::GLState {
Vector<SharedPtr<ShaderObject>>& GetAttachedShaders();
const Vector<SharedPtr<ShaderObject>>& GetAttachedShaders() const;
const String& GetInfoLog() const { return m_infoLog; }
// glCreateShaderProgramv folds the shader's compile log into the program's log, which
// is the only place a caller can read it from once the shader name is gone.
void AppendInfoLog(const String& text) {
if (text.empty()) return;
if (!m_infoLog.empty() && m_infoLog.back() != '\n') m_infoLog += '\n';
m_infoLog += text;
}
Int GetUniformMaxLength() const { return m_uniformNameMaxLength; }
Uint GetUniformCount() const { return m_activeUniformCount; }
Uint GetMaxUniformLocation() const { return m_maxUniformLocation; }
@@ -382,6 +389,11 @@ namespace MobileGL::MG_State::GLState {
// ARB_get_program_binary requires of it.
Bool GetBinaryRetrievableHint() const { return m_binaryRetrievableHint; }
void SetBinaryRetrievableHint(Bool hint) { m_binaryRetrievableHint = hint; }
// GL_PROGRAM_SEPARABLE (GL_ARB_separate_shader_objects): the program may supply a
// subset of the stages of a program pipeline. Only takes effect on the next link,
// which is why it is plain state here rather than something Link() consults.
Bool GetSeparable() const { return m_separable; }
void SetSeparable(Bool separable) { m_separable = separable; }
// 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() {
@@ -605,6 +617,7 @@ namespace MobileGL::MG_State::GLState {
Bool m_deleteStatus = false;
Bool m_linkStatus = false;
Bool m_binaryRetrievableHint = false;
Bool m_separable = false;
Bool m_validateStatus = true;
Uint32 m_backendStateVersion = 0;
@@ -40,9 +40,40 @@ namespace MobileGL {
Uint GetExternalIndex() const { return m_externalIndex; }
// A draw sees one program, but a pipeline holds one program per stage. The
// stages are composited into a single hidden program object, rebuilt whenever
// the stage set - or any stage program's own link - changes. The signature is
// what that "changes" means: a stage program's lifetime id pins the object and
// its backend state version pins the link generation.
using DrawProgramSignature =
Array<Uint64, static_cast<SizeT>(ShaderStage::ShaderStageCount) * 2>;
DrawProgramSignature ComputeDrawProgramSignature() const {
DrawProgramSignature signature{};
for (SizeT stage = 0; stage < static_cast<SizeT>(ShaderStage::ShaderStageCount); ++stage) {
const auto& program = m_stagePrograms[stage];
if (!program) continue;
signature[stage * 2] = program->GetLifetimeId();
signature[stage * 2 + 1] = program->GetBackendStateVersion();
}
return signature;
}
const SharedPtr<ProgramObject>& GetCachedDrawProgram(const DrawProgramSignature& signature) const {
static const SharedPtr<ProgramObject> nullProgram = nullptr;
if (!m_drawProgram || m_drawProgramSignature != signature) return nullProgram;
return m_drawProgram;
}
void SetCachedDrawProgram(const DrawProgramSignature& signature, SharedPtr<ProgramObject> program) {
m_drawProgramSignature = signature;
m_drawProgram = Move(program);
}
private:
Array<SharedPtr<ProgramObject>, static_cast<SizeT>(ShaderStage::ShaderStageCount)> m_stagePrograms{};
SharedPtr<ProgramObject> m_activeProgram;
SharedPtr<ProgramObject> m_drawProgram;
DrawProgramSignature m_drawProgramSignature{};
String m_infoLog;
const Uint m_externalIndex = 0;
Bool m_validateStatus = false;
@@ -37,6 +37,10 @@ namespace MobileGL {
return m_version;
}
Uint RenderState::GetPipelineStateVersion() const {
return m_pipelineStateVersion;
}
const RenderStateParameters& RenderState::GetAllParameters() const {
return m_parameters;
}
@@ -122,7 +126,7 @@ namespace MobileGL {
if (m_parameters.PolygonModeFront == front && m_parameters.PolygonModeBack == back) return;
m_parameters.PolygonModeFront = front;
m_parameters.PolygonModeBack = back;
++m_version;
BumpVersions();
}
GLenum RenderState::GetPolygonModeFront() const {
@@ -158,7 +162,7 @@ namespace MobileGL {
if (m_parameters.PatchVertices == vertices) return;
m_parameters.PatchVertices = vertices;
++m_version;
BumpVersions();
}
Uint RenderState::GetPatchVertices() const {
@@ -187,7 +191,7 @@ namespace MobileGL {
case CapabilityInput::capability: \
if (m_parameters.capability##Enabled == (flag)) break; \
m_parameters.capability##Enabled = (flag); \
++m_version; \
BumpVersions(); \
break;
switch (cap) {
@@ -220,7 +224,7 @@ namespace MobileGL {
blendState.Enabled = enabled;
stateChanged = true;
}
if (stateChanged) ++m_version;
if (stateChanged) BumpVersions();
break;
}
default: // not supported currently
@@ -276,7 +280,7 @@ namespace MobileGL {
if (m_parameters.BlendStates[index].Enabled == enabled) return;
m_parameters.BlendStates[index].Enabled = enabled;
++m_version;
BumpVersions();
}
Bool RenderState::IsCapabilityEnabledIndexed(CapabilityInput cap, Uint index) const {
@@ -308,7 +312,7 @@ namespace MobileGL {
stateChanged = true;
}
if (!stateChanged) return;
++m_version;
BumpVersions();
}
void RenderState::GetBlendFunc(BlendFactor& srcRGB, BlendFactor& dstRGB, BlendFactor& srcAlpha,
@@ -334,7 +338,7 @@ namespace MobileGL {
blendState.DstFactorRGB = dstRGB;
blendState.SrcFactorAlpha = srcAlpha;
blendState.DstFactorAlpha = dstAlpha;
++m_version;
BumpVersions();
}
void RenderState::GetBlendFuncIndexed(Uint index, BlendFactor& srcRGB, BlendFactor& dstRGB,
@@ -360,7 +364,7 @@ namespace MobileGL {
stateChanged = true;
}
if (!stateChanged) return;
++m_version;
BumpVersions();
}
void RenderState::GetBlendEquation(BlendEquation& color, BlendEquation& alpha) const {
@@ -379,7 +383,7 @@ namespace MobileGL {
}
blendState.ColorEquation = color;
blendState.AlphaEquation = alpha;
++m_version;
BumpVersions();
}
void RenderState::GetBlendEquationIndexed(Uint index, BlendEquation& color, BlendEquation& alpha) const {
@@ -395,7 +399,7 @@ namespace MobileGL {
if (m_parameters.LogicOp == logicOp) return;
m_parameters.LogicOp = logicOp;
++m_version;
BumpVersions();
}
LogicOperation RenderState::GetLogicOp() const {
@@ -407,7 +411,7 @@ namespace MobileGL {
if (m_parameters.DepthFunc == func) return;
m_parameters.DepthFunc = func;
++m_version;
BumpVersions();
}
DepthTestFunc RenderState::GetDepthFunc() const {
@@ -418,7 +422,7 @@ namespace MobileGL {
if (m_parameters.DepthMask == flag) return;
m_parameters.DepthMask = flag;
++m_version;
BumpVersions();
}
Bool RenderState::GetDepthMask() const {
@@ -429,10 +433,15 @@ namespace MobileGL {
StencilFaceState& state = m_parameters.StencilStates[GetStencilFaceIndex(face)];
if (state.Func == func && state.Ref == ref && state.ValueMask == mask) return;
// Only Func is baked into the pipeline; Ref and ValueMask are dynamic state
// (VK_DYNAMIC_STATE_STENCIL_REFERENCE / _COMPARE_MASK), so glStencilFunc changing
// only the reference must not evict a cached pipeline.
const Bool pipelineRelevantChange = state.Func != func;
state.Func = func;
state.Ref = ref;
state.ValueMask = mask;
++m_version;
if (pipelineRelevantChange) ++m_pipelineStateVersion;
}
void RenderState::SetStencilMask(StencilFace face, Uint32 mask) {
@@ -454,7 +463,7 @@ namespace MobileGL {
state.FailOp = fail;
state.PassDepthFailOp = depthFail;
state.PassDepthPassOp = depthPass;
++m_version;
BumpVersions();
}
const StencilFaceState& RenderState::GetStencilState(StencilFace face) const {
@@ -471,7 +480,7 @@ namespace MobileGL {
changed = true;
}
}
if (changed) ++m_version;
if (changed) BumpVersions();
}
BoolVec4 RenderState::GetColorMask() const {
@@ -482,7 +491,7 @@ namespace MobileGL {
void RenderState::SetColorMaskIndexed(Uint index, BoolVec4 mask) {
if (m_parameters.ColorMasks[index] == mask) return;
m_parameters.ColorMasks[index] = mask;
++m_version;
BumpVersions();
}
BoolVec4 RenderState::GetColorMaskIndexed(Uint index) const {
@@ -550,7 +559,7 @@ namespace MobileGL {
m_parameters.SampleCoverageValue = value;
m_parameters.SampleCoverageInvert = invert;
++m_version;
BumpVersions();
}
Float RenderState::GetSampleCoverageValue() const {
@@ -565,7 +574,7 @@ namespace MobileGL {
if (m_parameters.SampleMaskValue == mask) return;
m_parameters.SampleMaskValue = mask;
++m_version;
BumpVersions();
}
Uint32 RenderState::GetSampleMaskValue() const {
@@ -639,7 +648,7 @@ namespace MobileGL {
if (m_parameters.CullFaceModeSetting == mode) return;
m_parameters.CullFaceModeSetting = mode;
++m_version;
BumpVersions();
}
CullFaceMode RenderState::GetCullFaceMode() const {
@@ -650,7 +659,7 @@ namespace MobileGL {
if (m_parameters.FrontFaceModeSetting == mode) return;
m_parameters.FrontFaceModeSetting = mode;
++m_version;
BumpVersions();
}
FrontFaceMode RenderState::GetFrontFaceMode() const {
@@ -661,7 +670,7 @@ namespace MobileGL {
if (m_parameters.ProvokingVertexModeSetting == mode) return;
m_parameters.ProvokingVertexModeSetting = mode;
++m_version;
BumpVersions();
}
ProvokingVertexMode RenderState::GetProvokingVertexMode() const {
@@ -312,6 +312,8 @@ namespace MobileGL {
RenderState();
Uint GetVersion() const;
// Version of the pipeline-relevant subset only - see m_pipelineStateVersion.
Uint GetPipelineStateVersion() const;
const RenderStateParameters& GetAllParameters() const;
// Rasterization
@@ -418,7 +420,21 @@ namespace MobileGL {
const IntVec4& GetScissorBox() const; // x, y, width, height
private:
// Bump both: any state change invalidates the draw snapshot, and this one also
// changes the VkPipeline (or its DirectGLES equivalent).
void BumpVersions() {
++m_version;
++m_pipelineStateVersion;
}
Uint16 m_version = 0;
// Only the subset of render state that a backend bakes INTO a pipeline object.
// Viewport, scissor, depth range, blend colour, line width, polygon offset, stencil
// write mask, the clear values, hints and the point-size family are all either
// dynamic pipeline state or not pipeline state at all, so changing one of them must
// not evict a cached pipeline. Keeping one counter for both made a glViewport call
// knock the next draw off the pipeline memo AND the draw fast path.
Uint16 m_pipelineStateVersion = 0;
RenderStateParameters m_parameters;
// Pixel Store
@@ -27,11 +27,52 @@ namespace MobileGL {
m_texelSizes.reserve(std::bit_ceil(requiredLevelCount));
m_texelSizes.resize(requiredLevelCount);
m_isDirty.resize(requiredLevelCount, false);
m_compressedData.resize(requiredLevelCount);
m_compressedFormats.resize(requiredLevelCount, GL_NONE);
}
m_texelSizes[level] = input.texelSize;
auto& data = m_data[level];
data.resize(input.byteSize, 0);
// Respecifying a level drops whatever compressed image it used to hold. Without this,
// a glTexImage2D or glTexStorage2D over a level a previous glCompressedTexImage2D had
// shadowed would leave GL_TEXTURE_COMPRESSED answering true and glGetCompressedTexImage
// handing back the stale blob. Every allocation path funnels through here, so clearing
// once covers all of them; the compressed path re-arms the tag immediately afterwards
// via SetCompressedImage.
m_compressedFormats[level] = GL_NONE;
m_compressedData[level].clear();
m_compressedData[level].shrink_to_fit();
}
void MipmapStorage::SetCompressedImage(Uint level, GLenum internalFormat, const void* data, SizeT size) {
MOBILEGL_ASSERT(level < m_compressedData.size(), "SetCompressedImage: level out of range");
m_compressedFormats[level] = internalFormat;
auto& blob = m_compressedData[level];
// Zero-filled when data is null: glCompressedTexImage* with a null pointer defines the
// level's size and format but leaves its contents undefined, and zeros are the one
// reproducible answer a later glGetCompressedTexImage can give.
blob.assign(size, 0);
if (data != nullptr && size > 0) {
Memcpy(blob.data(), data, size);
}
}
GLenum MipmapStorage::GetCompressedFormat(Uint level) const {
if (level >= m_compressedFormats.size()) return GL_NONE;
return m_compressedFormats[level];
}
SizeT MipmapStorage::GetCompressedByteSize(Uint level) const {
if (level >= m_compressedData.size()) return 0;
return m_compressedData[level].size();
}
const void* MipmapStorage::MapCompressedData(Uint level) const {
if (level >= m_compressedData.size()) return nullptr;
return m_compressedData[level].data();
}
void MipmapStorage::TruncateToLevelCount(SizeT levelCount) {
@@ -40,6 +81,8 @@ namespace MobileGL {
m_data.resize(levelCount);
m_texelSizes.resize(levelCount);
m_isDirty.resize(levelCount);
m_compressedData.resize(levelCount);
m_compressedFormats.resize(levelCount);
}
void MipmapStorage::UpdateSubData(Uint level, DataPtr input) {
@@ -30,10 +30,27 @@ namespace MobileGL {
void MarkDirty(Uint level, bool dirty);
bool IsDirty(Uint level) const;
// The bytes an application handed to glCompressedTexImage*, kept verbatim beside the
// (uncompressed) texel shadow rather than in place of it. GL 4.6 core 8.11 requires
// glGetCompressedTexImage to return the image *as stored*, and no backend here has a
// BC/ETC codec, so a re-encode could never be byte-exact; at the same time m_data has
// to keep the "width * height * bytes-per-texel" layout that the backend upload
// sizing, glGenerateMipmap's bytes-per-texel division and the pixel-store packer all
// divide by. Two parallel vectors, one invariant preserved. Call order is
// AllocateLevel then SetCompressedImage - AllocateLevel clears the tag, so a plain
// glTexImage2D over the level un-compresses it.
void SetCompressedImage(Uint level, GLenum internalFormat, const void* data, SizeT size);
// GL_NONE when the level is not stored compressed.
GLenum GetCompressedFormat(Uint level) const;
SizeT GetCompressedByteSize(Uint level) const;
const void* MapCompressedData(Uint level) const;
protected:
Vector<IntVec3> m_texelSizes;
Vector<Vector<Uint8>> m_data;
Vector<bool> m_isDirty;
Vector<Vector<Uint8>> m_compressedData;
Vector<GLenum> m_compressedFormats;
};
} // namespace GLState
} // namespace MG_State
@@ -74,6 +74,27 @@ namespace MobileGL {
return m_storage[targetIndex].IsDirty(level);
}
void SetCompressedImage(Uint targetIndex, Uint level, GLenum internalFormat, const void* data,
SizeT size) {
MOBILEGL_ASSERT(targetIndex < TargetCount, "SetCompressedImage: target invalid");
m_storage[targetIndex].SetCompressedImage(level, internalFormat, data, size);
}
GLenum GetCompressedFormat(Uint targetIndex, Uint level) const {
MOBILEGL_ASSERT(targetIndex < TargetCount, "GetCompressedFormat: target invalid");
return m_storage[targetIndex].GetCompressedFormat(level);
}
SizeT GetCompressedByteSize(Uint targetIndex, Uint level) const {
MOBILEGL_ASSERT(targetIndex < TargetCount, "GetCompressedByteSize: target invalid");
return m_storage[targetIndex].GetCompressedByteSize(level);
}
const void* MapCompressedData(Uint targetIndex, Uint level) const {
MOBILEGL_ASSERT(targetIndex < TargetCount, "MapCompressedData: target invalid");
return m_storage[targetIndex].MapCompressedData(level);
}
protected:
Array<MipmapStorage, TargetCount> m_storage;
};
@@ -301,6 +301,27 @@ namespace MobileGL {
return m_textureStorage.IsDirty(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
}
void TextureObjectWithOneMipmap::SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel,
GLenum internalFormat, const void* data, SizeT size) {
m_textureStorage.SetCompressedImage(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel,
internalFormat, data, size);
}
GLenum TextureObjectWithOneMipmap::GetMipmapCompressedFormat(TextureUploadTarget uploadTarget,
Uint mipmapLevel) const {
return m_textureStorage.GetCompressedFormat(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
}
SizeT TextureObjectWithOneMipmap::GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget,
Uint mipmapLevel) const {
return m_textureStorage.GetCompressedByteSize(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
}
const void* TextureObjectWithOneMipmap::MapMipmapCompressedImage(TextureUploadTarget uploadTarget,
Uint mipmapLevel) const {
return m_textureStorage.MapCompressedData(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
}
IntVec3 TextureObjectWithOneMipmap::GetBaseSize() const {
if (m_textureStorage.GetLevelCount() == 0) {
return {0, 0, 0};
@@ -150,6 +150,18 @@ namespace MobileGL::MG_State::GLState {
virtual void* MapMipmapData(TextureUploadTarget uploadTarget, Uint mipmapLevel) = 0;
virtual void MarkStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel, Bool dirty = true) = 0;
virtual Bool IsStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel) const = 0;
// The compressed image a glCompressedTexImage* call shadowed for this level, kept verbatim
// next to the texel data rather than instead of it - see MipmapStorage. The texel shadow
// stays uncompressed and correctly sized, so nothing in the backend upload path has to know
// these exist; only glGetCompressedTexImage, glGetCompressedTextureImage and the
// GL_TEXTURE_COMPRESSED* level queries read them.
virtual void SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel,
GLenum internalFormat, const void* data, SizeT size) = 0;
// GL_NONE when this level is not stored compressed.
virtual GLenum GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const = 0;
virtual SizeT GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget, Uint mipmapLevel) const = 0;
virtual const void* MapMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel) const = 0;
};
// Cheap replacement for dynamic_cast on the hot path: TextureObjectMipmap is the
@@ -206,6 +218,11 @@ namespace MobileGL::MG_State::GLState {
void* MapMipmapData(TextureUploadTarget uploadTarget, Uint mipmapLevel) override;
void MarkStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel, Bool dirty) override;
bool IsStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
void SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel, GLenum internalFormat,
const void* data, SizeT size) override;
GLenum GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
SizeT GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
const void* MapMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
IntVec3 GetBaseSize() const override;
Bool IsComplete() const override;
@@ -55,6 +55,27 @@ namespace MobileGL {
return m_textureStorage.IsDirty(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
}
void TextureObject2DCube::SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel,
GLenum internalFormat, const void* data, SizeT size) {
m_textureStorage.SetCompressedImage(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel,
internalFormat, data, size);
}
GLenum TextureObject2DCube::GetMipmapCompressedFormat(TextureUploadTarget uploadTarget,
Uint mipmapLevel) const {
return m_textureStorage.GetCompressedFormat(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
}
SizeT TextureObject2DCube::GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget,
Uint mipmapLevel) const {
return m_textureStorage.GetCompressedByteSize(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
}
const void* TextureObject2DCube::MapMipmapCompressedImage(TextureUploadTarget uploadTarget,
Uint mipmapLevel) const {
return m_textureStorage.MapCompressedData(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
}
Uint TextureObject2DCube::GetIndexOfTextureUploadTarget(TextureUploadTarget target) const {
MOBILEGL_ASSERT(TextureUploadTarget::CubeMapPositiveX <= target &&
target <= TextureUploadTarget::CubeMapNegativeZ,
@@ -27,6 +27,12 @@ namespace MobileGL {
void* MapMipmapData(TextureUploadTarget uploadTarget, Uint mipmapLevel) override;
void MarkStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel, bool dirty) override;
bool IsStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
void SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel,
GLenum internalFormat, const void* data, SizeT size) override;
GLenum GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
SizeT GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
const void* MapMipmapCompressedImage(TextureUploadTarget uploadTarget,
Uint mipmapLevel) const override;
IntVec3 GetBaseSize() const override;
Bool IsComplete() const override;
@@ -57,7 +57,7 @@ namespace MobileGL::MG_State::GLState {
if (m_attributes[index].Size == size && m_attributes[index].Type == type &&
m_attributes[index].Normalized == normalized && m_attributes[index].Stride == stride &&
m_attributes[index].Offset == offset && m_attributes[index].IsInteger == isInteger &&
m_attributes[index].IsBgra == isBgra) {
m_attributes[index].IsBgra == isBgra && !m_attributes[index].IsLong) {
return;
}
@@ -73,6 +73,11 @@ namespace MobileGL::MG_State::GLState {
attr.Offset = offset;
attr.IsInteger = isInteger;
attr.IsBgra = isBgra;
// glVertexAttribPointer / glVertexAttribIPointer are never the long form, so they always
// take the attribute back out of it - and "only IsLong changed" is a real change that has to
// reach the backends, which is why the early-out above tests it too. Cleared inside the
// mutation block so the clear and the version bump stay atomic.
attr.IsLong = false;
BumpAttributeFormatVersion(index);
}
@@ -212,18 +217,21 @@ namespace MobileGL::MG_State::GLState {
}
void VertexArrayObject::SetAttributeFormatSeparate(Uint attribIndex, int size, DataType type, Bool normalized,
Bool isInteger, Uint relativeOffset, Bool isBgra) {
Bool isInteger, Uint relativeOffset, Bool isBgra,
Bool isLong) {
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 != isBgra || m_attributeRelativeOffset[attribIndex] != relativeOffset) {
attr.IsBgra != isBgra || attr.IsLong != isLong ||
m_attributeRelativeOffset[attribIndex] != relativeOffset) {
attr.Size = size;
attr.Type = type;
attr.Normalized = normalized;
attr.IsInteger = isInteger;
attr.IsBgra = isBgra;
attr.IsLong = isLong;
m_attributeRelativeOffset[attribIndex] = relativeOffset;
BumpAttributeFormatVersion(attribIndex);
}
@@ -23,6 +23,12 @@ namespace MobileGL {
SizeT Offset = 0;
Bool IsInteger = false;
// GL_BGRA vertex size: four components in reversed (B,G,R,A) memory order. Size stays 4.
// Set only by the long (L) format entry points. It is NOT implied by
// Type == Float64: VertexAttribFormat(GL_DOUBLE) also reads doubles from memory but
// asks for them *converted to float*, while VertexAttribLFormat keeps all 64 bits
// (GL 4.6 core 10.3.2). Backends have to tell the two apart, and it is what
// GL_VERTEX_ATTRIB_ARRAY_LONG reports.
Bool IsLong = false;
Bool IsBgra = false;
Uint Divisor = 0;
SharedPtr<BufferObject> Buffer;
@@ -88,7 +94,8 @@ 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 isBgra = false);
Bool isInteger, Uint relativeOffset, Bool isBgra = false,
Bool isLong = false);
// The binding-point view the attributes were resolved from. Kept queryable
// because glGetVertexArrayIndexed[64]iv reports it verbatim, and the resolved
@@ -837,6 +837,10 @@ namespace MobileGL::MG_Util::BackendLoader {
std::strcmp(extension, "GL_OES_texture_border_clamp") == 0) {
caps.SupportsTextureBorderClamp = true;
}
if (std::strcmp(extension, "GL_EXT_texture_cube_map_array") == 0 ||
std::strcmp(extension, "GL_OES_texture_cube_map_array") == 0) {
caps.SupportsTextureCubeMapArray = true;
}
if (std::strcmp(extension, "GL_EXT_base_instance") == 0) {
caps.SupportsBaseInstance = true;
}
@@ -1021,6 +1025,7 @@ namespace MobileGL::MG_Util::BackendLoader {
// 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;
caps.SupportsTextureCubeMapArray = true;
}
if (caps.SupportsTextureFilterAnisotropy) {
GLfloat maxTextureMaxAnisotropy = 1.0f;
@@ -1045,6 +1045,8 @@ namespace MobileGL {
// 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_TEXTURE_CUBE_MAP_ARRAY: ES 3.2 core, or EXT/OES_texture_cube_map_array before it.
Bool SupportsTextureCubeMapArray = 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;
@@ -198,6 +198,30 @@ namespace MobileGL::MG_Util::BackendLoader {
VkPhysicalDeviceFeatures supportedFeatures{};
vkGetPhysicalDeviceFeatures(physicalDevice, &supportedFeatures);
caps.SupportsWideLines = supportedFeatures.wideLines == VK_TRUE;
caps.SupportsShaderFloat64 = supportedFeatures.shaderFloat64 == VK_TRUE;
caps.SupportsImageCubeArray = supportedFeatures.imageCubeArray == VK_TRUE;
{
// Probe the formats a colour render target actually uses. A driver that refuses the flag
// for one of them refuses per-slice attachment for that format only, which
// VkTextureManager detects and records at image creation; this field just says whether
// the capability is worth offering at all.
static constexpr VkFormat k3DSliceProbeFormats[] = {VK_FORMAT_R8G8B8A8_UNORM,
VK_FORMAT_R8G8B8A8_SRGB};
Bool all2DArrayCompatible = true;
for (const VkFormat probeFormat : k3DSliceProbeFormats) {
VkImageFormatProperties probeProperties{};
const VkResult probeResult = vkGetPhysicalDeviceImageFormatProperties(
physicalDevice, probeFormat, VK_IMAGE_TYPE_3D, VK_IMAGE_TILING_OPTIMAL,
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT |
VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT,
VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT, &probeProperties);
if (probeResult != VK_SUCCESS) {
all2DArrayCompatible = false;
break;
}
}
caps.Supports2DArrayCompatible3DImages = all2DArrayCompatible;
}
caps.SupportsVertexPipelineStoresAndAtomics =
supportedFeatures.vertexPipelineStoresAndAtomics == VK_TRUE;
caps.SupportsFragmentStoresAndAtomics = supportedFeatures.fragmentStoresAndAtomics == VK_TRUE;
@@ -288,6 +312,9 @@ namespace MobileGL::MG_Util::BackendLoader {
caps.ViewportSubpixelBits = static_cast<Int>(properties.limits.viewportSubPixelBits);
FillFragmentInterpolationLimits(caps, properties.limits);
caps.SupportsWideLines = false;
caps.SupportsShaderFloat64 = false;
caps.SupportsImageCubeArray = false;
caps.Supports2DArrayCompatible3DImages = false;
// This helper only receives properties, not VkPhysicalDeviceFeatures. Leave optional
// stage writes disabled rather than inferring them from descriptor limits alone.
caps.SupportsVertexPipelineStoresAndAtomics = false;
@@ -74,6 +74,22 @@ namespace MobileGL {
Float MaxFragmentInterpolationOffset = 0.4375f;
Int FragmentInterpolationOffsetBits = 4;
Bool SupportsWideLines = false;
// VkPhysicalDeviceFeatures::shaderFloat64. Any module declaring OpCapability Float64
// needs it, which includes every 64-bit vertex attribute: the attribute itself arrives
// as 32-bit words, but the bitcast result and everything computed from it is Float64.
Bool SupportsShaderFloat64 = false;
// VkPhysicalDeviceFeatures::imageCubeArray. Required before a
// VK_IMAGE_VIEW_TYPE_CUBE_ARRAY view may be created at all
// (VUID-VkImageViewCreateInfo-viewType-01004), which is every cube map array texture -
// both its sampled view and its full view.
Bool SupportsImageCubeArray = false;
// VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT on a 3D colour image, i.e. whether one z slice
// of a GL_TEXTURE_3D texture can be named by a 2D view and attached to a framebuffer.
// Vulkan 1.1 core, but per format+usage - this is an OPTIMISTIC summary probed over the
// common colour attachment formats. The authoritative answer is taken per format at
// image creation in VkTextureManager, which withdraws the flag and remembers the verdict
// when a driver refuses it.
Bool Supports2DArrayCompatible3DImages = false;
// Storage-image descriptors are limited per stage by
// maxPerStageDescriptorStorageImages, but writes/atomics outside compute additionally
// require these core Vulkan features to be enabled on the logical device.
@@ -525,5 +525,50 @@ namespace MobileGL {
return s;
}
CompressedFormatInfo GetCompressedFormatInfo(GLenum internalFormat) {
// Every format here is 4x4-blocked; only the bytes per block differ (8 for the one- and
// two-channel RGTC/EAC and the 1-bit-alpha ETC2 forms, 16 for BPTC and the full-alpha
// ETC2/EAC and two-channel EAC forms). The generic compressed formats (GL_COMPRESSED_RGBA
// and friends) are deliberately absent: GL lets the implementation pick, MobileGL picks
// uncompressed, and glCompressedTexImage* must reject them because there is no defined
// block layout to hand it. The accepted set is exactly the set
// ConvertGLEnumToTextureInternalFormat can back with uncompressed storage, so this call
// can never accept a format whose texel shadow cannot be allocated.
switch (internalFormat) {
case GL_COMPRESSED_RED_RGTC1:
case GL_COMPRESSED_SIGNED_RED_RGTC1:
case GL_COMPRESSED_RGB8_ETC2:
case GL_COMPRESSED_SRGB8_ETC2:
case GL_COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2:
case GL_COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2:
case GL_COMPRESSED_R11_EAC:
case GL_COMPRESSED_SIGNED_R11_EAC:
return {4, 4, 8};
case GL_COMPRESSED_RG_RGTC2:
case GL_COMPRESSED_SIGNED_RG_RGTC2:
case GL_COMPRESSED_RGBA_BPTC_UNORM:
case GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM:
case GL_COMPRESSED_RGB_BPTC_SIGNED_FLOAT:
case GL_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT:
case GL_COMPRESSED_RGBA8_ETC2_EAC:
case GL_COMPRESSED_SRGB8_ALPHA8_ETC2_EAC:
case GL_COMPRESSED_RG11_EAC:
case GL_COMPRESSED_SIGNED_RG11_EAC:
return {4, 4, 16};
default:
return {};
}
}
SizeT CalculateCompressedTextureImageSize(const CompressedFormatInfo& info, IntVec3 size) {
if (info.blockWidth == 0 || info.blockHeight == 0) return 0;
const SizeT width = static_cast<SizeT>(std::max<Int>(size.x(), 0));
const SizeT height = static_cast<SizeT>(std::max<Int>(size.y(), 0));
const SizeT depth = static_cast<SizeT>(std::max<Int>(size.z(), 1));
const SizeT blocksX = (width + info.blockWidth - 1) / info.blockWidth;
const SizeT blocksY = (height + info.blockHeight - 1) / info.blockHeight;
return blocksX * blocksY * depth * info.blockByteSize;
}
} // namespace MG_Util
} // namespace MobileGL
+18
View File
@@ -24,5 +24,23 @@ namespace MobileGL {
SizeT CalculateInputTextureImageSize(TextureInputFormat inputFormat, TexturePixelDataType pixelDataType,
IntVec3 size);
ComponentSizes GetComponentSizesForInternalFormat(TextureInternalFormat internal);
// A specific compressed internal format described the only two ways the CPU shadow needs it:
// the texel footprint of one block and the bytes that block occupies. Kept as a GLenum query
// rather than a TextureInternalFormat one on purpose - TextureInternalFormat has no
// compressed enumerator (a compressed format resolves to the uncompressed storage backing
// it), so by the time the enum has been converted the block geometry is gone.
struct CompressedFormatInfo {
// Zero means "internalformat is not one of the specific compressed formats core GL
// requires" - which is exactly the glCompressedTexImage* INVALID_ENUM case, so callers
// get the format test and the block geometry from one lookup.
Uint blockWidth = 0;
Uint blockHeight = 0;
SizeT blockByteSize = 0;
};
CompressedFormatInfo GetCompressedFormatInfo(GLenum internalFormat);
// Blocks are counted rounded up, exactly as GL 4.6 core 8.7 sizes a compressed image, so a
// 4x4 BPTC image is one 16-byte block and a 1x1 one still is.
SizeT CalculateCompressedTextureImageSize(const CompressedFormatInfo& info, IntVec3 size);
} // namespace MG_Util
} // namespace MobileGL
+106
View File
@@ -309,6 +309,24 @@ namespace MobileGL::MG_Util::SelfTest {
"sampling outside a GL_CLAMP_TO_BORDER texture reads the driver's default "
"border instead of the requested colour");
}
if (caps.SupportsTextureCubeMapArray) {
builder.Pass("Texture cube map array",
"supported (GL_TEXTURE_CUBE_MAP_ARRAY textures get real storage and can be "
"attached to a framebuffer)");
} else {
builder.Warn("Texture cube map array",
"not supported (pre-ES 3.2 without GL_EXT/OES_texture_cube_map_array); a cube "
"map array texture gets no driver storage at all, so sampling one reads nothing "
"and rendering to one does not reach the screen");
}
// Reported rather than probed: this one cannot come out any other way. OpenGL ES has no
// double-precision vertex format and ESSL has no fp64 type, so there is no driver and no
// extension that could make it work - the row exists so the loss is named at startup
// instead of discovered as an unexplained GL_INVALID_OPERATION at draw setup.
builder.Warn("64-bit vertex attributes",
"not supported on any GLES driver (ES has no GL_DOUBLE vertex format and ESSL has "
"no fp64 type); glVertexAttribLFormat / glVertexArrayAttribLFormat report "
"GL_INVALID_OPERATION - use the Vulkan backend if the application needs them");
if (glesFuncs.glPatchParameteri != nullptr) {
builder.Pass("Tessellation patch parameters",
"glPatchParameteri present (GL_PATCH_VERTICES reaches the driver)");
@@ -1401,6 +1419,44 @@ namespace MobileGL::MG_Util::SelfTest {
} else {
builder.Warn("dualSrcBlend", "unsupported; GL_SRC1_* dual-source blend factors hard-fail at draw");
}
{
VkImageFormatProperties sliceProbe{};
const Bool sliceCapable =
vkGetPhysicalDeviceImageFormatProperties(
physicalDevice, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_TYPE_3D, VK_IMAGE_TILING_OPTIMAL,
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT |
VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT,
VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT, &sliceProbe) == VK_SUCCESS;
if (sliceCapable) {
builder.Pass("2D-array-compatible 3D images",
"supported for the common colour attachment formats (one z slice of a "
"GL_TEXTURE_3D texture can be attached to a framebuffer and cleared and read "
"back on its own; a format that refuses the flag is detected at image "
"creation and declines per-slice attachment)");
} else {
builder.Warn("2D-array-compatible 3D images",
"VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT unavailable for colour attachments; "
"glFramebufferTextureLayer on a GL_TEXTURE_3D texture is declined for every "
"slice past the first");
}
}
if (features.imageCubeArray == VK_TRUE) {
builder.Pass("imageCubeArray",
"GL_TEXTURE_CUBE_MAP_ARRAY textures get a Vulkan image and can be sampled and "
"attached to a framebuffer per layer");
} else {
builder.Warn("imageCubeArray",
"unsupported; a GL_TEXTURE_CUBE_MAP_ARRAY texture gets no image at all, so sampling "
"one reads nothing and glFramebufferTextureLayer on one is declined");
}
if (features.shaderFloat64 == VK_TRUE) {
builder.Pass("shaderFloat64",
"GLSL double/dvec/dmat and 64-bit vertex attributes (glVertexAttribLFormat) supported");
} else {
builder.Warn("shaderFloat64",
"unsupported; any shader declaring a double fails to create a shader module, and "
"glVertexAttribLFormat reports GL_INVALID_OPERATION instead of feeding the attribute");
}
Bool shaderDrawParameters = false;
if (vkGetPhysicalDeviceFeatures2Fn != nullptr && properties.apiVersion >= VK_API_VERSION_1_1) {
@@ -1422,6 +1478,56 @@ namespace MobileGL::MG_Util::SelfTest {
"unavailable; shaders using gl_DrawID/gl_BaseInstance will not work");
}
Bool provokingVertexLast = false;
Bool transformFeedbackPreservesProvokingVertex = false;
Bool provokingVertexModePerPipeline = false;
Bool transformFeedbackPreservesTriangleFanProvokingVertex = false;
if (vkGetPhysicalDeviceFeatures2Fn != nullptr &&
HasVkExtension(deviceExtensions, VK_EXT_PROVOKING_VERTEX_EXTENSION_NAME)) {
VkPhysicalDeviceProvokingVertexFeaturesEXT provokingVertexFeatures{};
provokingVertexFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROVOKING_VERTEX_FEATURES_EXT;
VkPhysicalDeviceFeatures2 features2{};
features2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2;
features2.pNext = &provokingVertexFeatures;
vkGetPhysicalDeviceFeatures2Fn(physicalDevice, &features2);
provokingVertexLast = provokingVertexFeatures.provokingVertexLast == VK_TRUE;
transformFeedbackPreservesProvokingVertex =
provokingVertexFeatures.transformFeedbackPreservesProvokingVertex == VK_TRUE;
if (vkGetPhysicalDeviceProperties2Fn != nullptr) {
VkPhysicalDeviceProvokingVertexPropertiesEXT provokingVertexProperties{};
provokingVertexProperties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROVOKING_VERTEX_PROPERTIES_EXT;
VkPhysicalDeviceProperties2 properties2{};
properties2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2;
properties2.pNext = &provokingVertexProperties;
vkGetPhysicalDeviceProperties2Fn(physicalDevice, &properties2);
provokingVertexModePerPipeline =
provokingVertexProperties.provokingVertexModePerPipeline == VK_TRUE;
transformFeedbackPreservesTriangleFanProvokingVertex =
provokingVertexProperties.transformFeedbackPreservesTriangleFanProvokingVertex == VK_TRUE;
}
}
if (provokingVertexLast) {
builder.Pass("provokingVertexLast",
"supported; flat varyings take GL's last vertex and transform feedback records "
"strip/fan triangles in GL's vertex order");
} else {
builder.Warn("provokingVertexLast",
"unsupported; flat-shaded varyings take a primitive's first vertex instead of GL's "
"last, and transform feedback records TRIANGLE_STRIP/TRIANGLE_FAN triangles rotated "
"(e.g. 0,1,2 / 1,3,2 instead of 0,1,2 / 2,1,3)");
}
if (provokingVertexLast && !transformFeedbackPreservesProvokingVertex) {
builder.Warn("transformFeedbackPreservesProvokingVertex",
"unsupported; the captured vertex order for strips/fans is not guaranteed by the "
"spec even though the flat-shading convention is correct");
}
if (provokingVertexLast && transformFeedbackPreservesProvokingVertex &&
!transformFeedbackPreservesTriangleFanProvokingVertex && !provokingVertexModePerPipeline) {
builder.Warn("transformFeedbackPreservesTriangleFanProvokingVertex",
"unsupported and per-pipeline modes unavailable; the transform-feedback "
"provoking-vertex guarantee is left off so GL_TRIANGLE_FAN pipelines stay legal");
}
Bool primitiveTopologyListRestart = false;
if (vkGetPhysicalDeviceFeatures2Fn != nullptr &&
HasVkExtension(deviceExtensions, VK_EXT_PRIMITIVE_TOPOLOGY_LIST_RESTART_EXTENSION_NAME)) {
@@ -18,6 +18,7 @@
#include "SpirvPasses/DecomposeWorkgroupVec3Pass.h"
#include "SpirvPasses/DecoratePositionInvariantPass.h"
#include "SpirvPasses/LowerDrawParametersPass.h"
#include "SpirvPasses/PackDoubleVertexInputsPass.h"
#include "SpirvPasses/RebaseInstanceIndexPass.h"
#include "SpirvPasses/NormalizeRectCoordinatesPass.h"
#include "SpirvPasses/StripUboMemberRelaxedPrecisionPass.h"
@@ -327,6 +328,18 @@ namespace MobileGL {
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
}
bool ShaderCompiler::PackDoubleVertexInputsForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary) {
using namespace spvtools;
OptimizerOptions options;
options.set_run_validator(false);
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
optimizer.RegisterPass(PackDoubleVertexInputsPass::CreatePackDoubleVertexInputsPass());
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
}
bool ShaderCompiler::StripUboMemberRelaxedPrecisionForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary) {
using namespace spvtools;
@@ -54,6 +54,13 @@ namespace MobileGL {
static bool LowerRectImages(const Vector<Uint32>& inputBinary, Vector<uint32_t>& outputBinary);
static bool RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary);
// Re-declares 64-bit float vertex inputs as their 32-bit unsigned word pair
// (double -> uvec2, dvec2 -> uvec4) and bitcasts them back to double at entry, so no
// VK_FORMAT_R64*_SFLOAT is needed - lavapipe advertises none of them for vertex
// buffers. Vertex stage, DirectVulkan only; pairs with the Float64 case in
// VertexInputStateFactory::ToVkVertexFormat.
static bool PackDoubleVertexInputsForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary);
// Adds the Invariant decoration to every Position builtin output. GL apps
// routinely rely on cross-program position invariance for multi-pass
// equality depth tests (e.g. GEQUAL re-draws of the same geometry), and
@@ -0,0 +1,211 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.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 "PackDoubleVertexInputsPass.h"
#include "spirv.hpp"
#include "source/opt/def_use_manager.h"
#include "source/opt/instruction.h"
#include "source/opt/ir_context.h"
#include "source/opt/module.h"
#include "source/opt/types.h"
#include "source/util/make_unique.h"
#include <memory>
#include <vector>
namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
namespace {
using spvtools::opt::IRContext;
using spvtools::opt::Instruction;
using spvtools::opt::Operand;
namespace analysis = spvtools::opt::analysis;
// Component count of a 64-bit float input, or 0 if the type is not one.
Uint32 DoubleComponentCount(const analysis::Type* type) {
if (type == nullptr) return 0;
if (const auto* scalar = type->AsFloat()) {
return scalar->width() == 64 ? 1u : 0u;
}
if (const auto* vector = type->AsVector()) {
const auto* element = vector->element_type()->AsFloat();
if (element == nullptr || element->width() != 64) return 0;
return vector->element_count();
}
return 0;
}
} // namespace
spvtools::opt::Pass::Status PackDoubleVertexInputsPass::Process() {
auto* irContext = context();
auto entryPoints = irContext->module()->entry_points();
if (entryPoints.begin() == entryPoints.end()) return Status::SuccessWithoutChange;
Instruction* entryPoint = &*entryPoints.begin();
if (static_cast<spv::ExecutionModel>(entryPoint->GetSingleWordInOperand(0)) !=
spv::ExecutionModel::Vertex) {
return Status::SuccessWithoutChange;
}
auto* defUseMgr = irContext->get_def_use_mgr();
auto* typeMgr = irContext->get_type_mgr();
struct Target {
Instruction* variable = nullptr;
Uint32 doubleTypeId = 0;
Uint32 componentCount = 0;
Uint32 packedTypeId = 0;
Uint32 packedPointerTypeId = 0;
Uint32 privatePointerTypeId = 0;
};
std::vector<Target> targets;
for (Instruction& inst : irContext->types_values()) {
if (inst.opcode() != spv::Op::OpVariable) continue;
if (static_cast<spv::StorageClass>(inst.GetSingleWordInOperand(0)) !=
spv::StorageClass::Input) {
continue;
}
Instruction* pointerType = defUseMgr->GetDef(inst.type_id());
if (pointerType == nullptr) continue;
const Uint32 pointeeTypeId = pointerType->GetSingleWordInOperand(1);
const Uint32 components = DoubleComponentCount(typeMgr->GetType(pointeeTypeId));
if (components == 0) continue;
if (components > 2) {
// 6 or 8 uint32 components has no single vertex format, and GL spreads such
// an input over two attribute locations. Left alone; the vertex-input
// factory declines the matching attribute for the same reason.
MGLOG_E("PackDoubleVertexInputsPass: vertex input %%%u is a %u-component 64-bit "
"float; only double and dvec2 inputs can be packed",
inst.result_id(), components);
continue;
}
targets.push_back({&inst, pointeeTypeId, components});
}
if (targets.empty()) return Status::SuccessWithoutChange;
// Entry block insertion point: after the block's leading OpVariable run, which
// SPIR-V requires to stay at the top of a function's first block.
const Uint32 entryFunctionId = entryPoint->GetSingleWordInOperand(1);
spvtools::opt::Function* entryFunction = nullptr;
for (auto& function : *irContext->module()) {
if (function.result_id() == entryFunctionId) {
entryFunction = &function;
break;
}
}
if (entryFunction == nullptr || entryFunction->begin() == entryFunction->end()) {
return Status::SuccessWithoutChange;
}
auto& entryBlock = *entryFunction->begin();
auto insertPoint = entryBlock.begin();
while (insertPoint != entryBlock.end() && insertPoint->opcode() == spv::Op::OpVariable) {
++insertPoint;
}
if (insertPoint == entryBlock.end()) return Status::SuccessWithoutChange;
const Uint32 uintTypeId = typeMgr->GetUIntTypeId();
const analysis::Integer* uintType = typeMgr->GetType(uintTypeId)->AsInteger();
// Every type instruction has to exist before any variable that names it: the
// types-and-variables section is walked in order and a forward reference to a type is
// invalid SPIR-V. GetTypeInstruction/FindPointerToType append, and the variables are
// appended (or re-appended) below, so all three lookups run first for every target.
for (auto& target : targets) {
analysis::Vector packedVectorType(uintType, target.componentCount * 2u);
target.packedTypeId = typeMgr->GetTypeInstruction(&packedVectorType);
target.packedPointerTypeId =
typeMgr->FindPointerToType(target.packedTypeId, spv::StorageClass::Input);
target.privatePointerTypeId =
typeMgr->FindPointerToType(target.doubleTypeId, spv::StorageClass::Private);
}
for (const auto& target : targets) {
Instruction* variable = target.variable;
const Uint32 oldVariableId = variable->result_id();
const Uint32 packedTypeId = target.packedTypeId;
const Uint32 packedPointerTypeId = target.packedPointerTypeId;
const Uint32 packedVariableId = irContext->TakeNextId();
irContext->AddGlobalValue(spvtools::MakeUnique<Instruction>(
irContext, spv::Op::OpVariable, packedPointerTypeId, packedVariableId,
std::initializer_list<Operand>{
{SPV_OPERAND_TYPE_STORAGE_CLASS,
{static_cast<Uint32>(spv::StorageClass::Input)}}}));
// The interface decorations belong to whatever is actually the Input now.
std::vector<Instruction*> deadDecorations;
for (auto& annotation : irContext->annotations()) {
if (annotation.opcode() != spv::Op::OpDecorate) continue;
if (annotation.GetSingleWordInOperand(0) != oldVariableId) continue;
const auto decoration =
static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(1));
if (decoration == spv::Decoration::Location ||
decoration == spv::Decoration::Component ||
decoration == spv::Decoration::RelaxedPrecision) {
annotation.SetInOperand(0, {packedVariableId});
} else {
deadDecorations.push_back(&annotation);
}
}
for (auto* annotation : deadDecorations) {
irContext->KillInst(annotation);
}
// Demote the original to a Private global: every existing OpLoad /
// OpAccessChain on it stays valid and keeps its double type. It is also moved to
// the end of the section, because the pointer-to-Private type it now names was
// appended above and a variable may not forward-reference its own type.
variable->SetResultType(target.privatePointerTypeId);
variable->SetInOperand(0, {static_cast<Uint32>(spv::StorageClass::Private)});
variable->RemoveFromList();
irContext->AddGlobalValue(std::unique_ptr<Instruction>(variable));
// SPIR-V 1.3 lists only Input/Output in the entry-point interface.
std::vector<Operand> interfaceOperands;
for (Uint32 i = 0; i < entryPoint->NumInOperands(); ++i) {
const Operand& operand = entryPoint->GetInOperand(i);
if (i >= 3 && operand.type == SPV_OPERAND_TYPE_ID &&
entryPoint->GetSingleWordInOperand(i) == oldVariableId) {
interfaceOperands.push_back({SPV_OPERAND_TYPE_ID, {packedVariableId}});
continue;
}
interfaceOperands.push_back(operand);
}
entryPoint->SetInOperands(std::move(interfaceOperands));
const Uint32 loadedId = irContext->TakeNextId();
const Uint32 bitcastId = irContext->TakeNextId();
insertPoint = insertPoint.InsertBefore(spvtools::MakeUnique<Instruction>(
irContext, spv::Op::OpLoad, packedTypeId, loadedId,
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {packedVariableId}}}));
++insertPoint;
insertPoint = insertPoint.InsertBefore(spvtools::MakeUnique<Instruction>(
irContext, spv::Op::OpBitcast, target.doubleTypeId, bitcastId,
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {loadedId}}}));
++insertPoint;
insertPoint = insertPoint.InsertBefore(spvtools::MakeUnique<Instruction>(
irContext, spv::Op::OpStore, 0, 0,
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {oldVariableId}},
{SPV_OPERAND_TYPE_ID, {bitcastId}}}));
++insertPoint;
}
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
return Status::SuccessWithChange;
}
spvtools::Optimizer::PassToken PackDoubleVertexInputsPass::CreatePackDoubleVertexInputsPass() {
return spvtools::Optimizer::PassToken(MakeUnique<PackDoubleVertexInputsPass>());
}
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
@@ -0,0 +1,43 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.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 "source/opt/pass.h"
#include "spirv-tools/optimizer.hpp"
#include <Includes.h>
namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
// Re-declares every 64-bit floating-point *vertex input* as the 32-bit unsigned word
// pair that holds the same bytes (double -> uvec2, dvec2 -> uvec4), demotes the
// original variable to a Private global, and seeds it once at the top of the entry
// point with an OpBitcast of the new input. Everything downstream keeps loading the
// same id and the same double type, so no other instruction is rewritten.
//
// Why not just use VK_FORMAT_R64*_SFLOAT: those formats are optional, and lavapipe
// reports zero bufferFeatures for all four of them, so a 64-bit vertex fetch is
// impossible there even though shaderFloat64 is supported. The word-pair form needs no
// format capability at all and is bit-exact, so it is applied unconditionally rather
// than as a fallback - which also keeps it in lockstep with
// VertexInputStateFactory::ToVkVertexFormat, since both branch on nothing but "is this
// a 64-bit vertex input".
//
// DirectVulkan only. The 64-bit *arithmetic* still needs the Float64 capability, i.e.
// an enabled VkPhysicalDeviceFeatures::shaderFloat64.
class PackDoubleVertexInputsPass : public spvtools::opt::Pass {
public:
const char* name() const override { return "mobilegl-pack-double-vertex-inputs"; }
Status Process() override;
static spvtools::Optimizer::PassToken CreatePackDoubleVertexInputsPass();
};
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL