[Merge] (ShaderTranspiler, GLState, DirectGLES): land dev GL43 wave2/wave3 under the translation cache

This commit is contained in:
2026-08-20 18:03:06 -04:00
79 changed files with 6766 additions and 458 deletions
+32 -2
View File
@@ -14,6 +14,7 @@ namespace MobileGL {
namespace MG_State::GLState {
class FramebufferObject;
class ITextureObject;
class RenderbufferObject;
}
enum class BackendType {
@@ -24,6 +25,19 @@ namespace MobileGL {
};
namespace MG_Backend {
// One endpoint of a glCopyImageSubData. GL 4.6 core 18.3.2 accepts GL_RENDERBUFFER
// alongside the ten whole-image texture targets, and a renderbuffer name lives in a
// namespace of its own - so an endpoint is a sum type, not an ITextureObject. At most
// one of the two pointers is set; neither is set when the name named nothing, which is
// the INVALID_VALUE the frontend validator reports.
struct CopyImageEndpoint {
SharedPtr<MG_State::GLState::ITextureObject> Texture;
SharedPtr<MG_State::GLState::RenderbufferObject> Renderbuffer;
Bool IsRenderbuffer() const { return Renderbuffer != nullptr; }
Bool Exists() const { return Texture != nullptr || Renderbuffer != nullptr; }
};
enum class FormatCapability : Uint64 {
Creatable = 1ull << 0,
@@ -160,9 +174,9 @@ namespace MobileGL {
GLsizei height, GLint border);
void (*CopyTexSubImage2D)(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y,
GLsizei width, GLsizei height);
void (*CopyImageSubData)(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
void (*CopyImageSubData)(const CopyImageEndpoint& src,
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
const CopyImageEndpoint& dst,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
void (*GenerateMipmap)(GLenum target);
@@ -326,6 +340,22 @@ namespace MobileGL {
Int MaxVertexAttribs = 16;
Int MaxComputeShaderStorageBlocks = 8;
Int MaxCombinedShaderStorageBlocks = 32;
// Per-stage GL_MAX_*_SHADER_STORAGE_BLOCKS. Zero is a legal answer for the four
// non-compute, non-fragment stages and these defaults are the spec minimums, not
// placeholders: GL 4.6 table 23.64 and ES 3.2 table 21.44 both set the minimum for
// vertex, tessellation control, tessellation evaluation and geometry at 0, and only
// fragment (8 in GL, 4 in ES) and compute are guaranteed to have any. Every real ARM
// GLES driver takes that allowance - a Mali-G925 reports 0 for all four - so a
// backend that cannot honour a graphics-stage storage block MUST report 0 here
// rather than a hopeful number. Advertising a non-zero count the driver will refuse
// does not make the block work; it only moves the failure from an honest
// "unsupported" at query time to a backend link error the frontend never surfaces,
// after which every draw with that program silently renders nothing.
Int MaxVertexShaderStorageBlocks = 0;
Int MaxTessControlShaderStorageBlocks = 0;
Int MaxTessEvaluationShaderStorageBlocks = 0;
Int MaxGeometryShaderStorageBlocks = 0;
Int MaxFragmentShaderStorageBlocks = 8;
Int MaxComputeUniformBlocks = 12;
Int MaxComputeWorkGroupInvocations = 128;
Int MaxShaderStorageBufferBindings = 8;
@@ -1251,9 +1251,31 @@ namespace MobileGL::MG_Backend::DirectGLES {
static_cast<Int>(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS));
m_dynamicParameters.MaxComputeShaderStorageBlocks = m_GLESCapabilities.MaxComputeShaderStorageBlocks;
m_dynamicParameters.MaxCombinedShaderStorageBlocks = m_GLESCapabilities.MaxCombinedShaderStorageBlocks;
// Per-stage storage-block counts, forwarded from the host driver rather than invented.
// A stage the driver cannot serve reports 0, which is a legal answer everywhere these
// limits appear (GL 4.6 table 23.64, ES 3.2 table 21.44 - the minimum is 0 for every
// graphics stage except fragment) and is the only answer that lets an application take
// its own fallback instead of building a program the driver will refuse to link. The
// stage limit cannot exceed the combined limit or the number of binding points there
// are to bind buffers to, so clamp to both.
const auto clampStageStorageBlocks = [this](Int stageLimit) {
return std::min({std::max(stageLimit, 0), std::max(m_dynamicParameters.MaxCombinedShaderStorageBlocks, 0),
std::max(m_dynamicParameters.MaxShaderStorageBufferBindings, 0)});
};
m_dynamicParameters.MaxShaderStorageBufferBindings = m_GLESCapabilities.MaxShaderStorageBufferBindings;
m_dynamicParameters.MaxVertexShaderStorageBlocks =
clampStageStorageBlocks(m_GLESCapabilities.MaxVertexShaderStorageBlocks);
m_dynamicParameters.MaxTessControlShaderStorageBlocks =
clampStageStorageBlocks(m_GLESCapabilities.MaxTessControlShaderStorageBlocks);
m_dynamicParameters.MaxTessEvaluationShaderStorageBlocks =
clampStageStorageBlocks(m_GLESCapabilities.MaxTessEvaluationShaderStorageBlocks);
m_dynamicParameters.MaxGeometryShaderStorageBlocks =
clampStageStorageBlocks(m_GLESCapabilities.MaxGeometryShaderStorageBlocks);
m_dynamicParameters.MaxFragmentShaderStorageBlocks =
clampStageStorageBlocks(m_GLESCapabilities.MaxFragmentShaderStorageBlocks);
m_dynamicParameters.MaxComputeUniformBlocks = m_GLESCapabilities.MaxComputeUniformBlocks;
m_dynamicParameters.MaxComputeWorkGroupInvocations = m_GLESCapabilities.MaxComputeWorkGroupInvocations;
m_dynamicParameters.MaxShaderStorageBufferBindings = m_GLESCapabilities.MaxShaderStorageBufferBindings;
// (MaxShaderStorageBufferBindings is assigned above, before the per-stage clamp reads it.)
// This is the number glGetIntegerv(GL_MAX_TEXTURE_BUFFER_SIZE) hands the application, and
// on a host without buffer textures it is knowingly a floor MobileGL cannot honour rather
// than a driver answer (m_GLESCapabilities.MaxTextureBufferSizeIsDriverReported says
+378 -62
View File
@@ -395,6 +395,50 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
}
void SyncAtomicCounterBuffers(const Vector<Int>& glBindings, Int esslBindingTop) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
const SizeT pointCount = MG_State::pGLContext->GetBufferBindingPointCount(BufferTarget::AtomicCounter);
for (const Int glBinding : glBindings) {
if (glBinding < 0 || static_cast<SizeT>(glBinding) >= pointCount) continue;
const Int esslBinding = esslBindingTop - glBinding;
// Already diagnosed once when the block was transpiled; nothing was bound to it
// there either, so there is nothing to unbind here.
if (esslBinding < 0) continue;
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::AtomicCounter,
static_cast<Uint>(glBinding));
auto& obj = point.GetBoundObject();
if (!obj) {
BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, static_cast<Uint>(esslBinding), 0);
continue;
}
auto* backendResource = EnsureBufferResource(obj);
if (!backendResource || backendResource->id == 0) {
MGLOG_E_ONCE("No backend buffer found for atomic counter binding point %d.", glBinding);
continue;
}
const auto& range = point.GetRange();
if (range.start == 0 && range.end >= obj->GetSize()) {
BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, static_cast<Uint>(esslBinding),
backendResource->id);
} else {
const auto start = std::min(range.start, obj->GetSize());
const auto end = std::min(range.end, obj->GetSize());
BindBufferRangeCached(GL_SHADER_STORAGE_BUFFER, static_cast<Uint>(esslBinding),
backendResource->id, static_cast<GLintptr>(start),
static_cast<GLsizeiptr>(end - start));
}
// The whole point of a counter is that the shader INCREMENTS it, and every
// conformance case reads the result back with glMapBufferRange or
// glGetBufferSubData - which serve the frontend's CPU shadow until the buffer is
// flagged (BufferObject::SyncGpuWrites), exactly as for a storage buffer.
obj->MarkGpuWritten();
}
}
void SyncBoundBuffer(BufferTarget target, GLenum glTarget) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
@@ -1394,12 +1438,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
}
// Highest image unit that has ever been given a texture, plus one. Maintained by the
// single funnel below, so it is a sound "no draw in this context can be reading an image"
// test: nothing reaches an image unit without going through SyncImageTextureBinding.
// Almost every program (every Minecraft draw) leaves it at zero, which is what keeps the
// draw-path staleness check below at one integer test.
static Uint g_imageUnitHighWaterMark = 0;
void SyncImageTextureBinding(Uint unit) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(unit));
TrackWritableImageBufferUnit(unit, IsWritableImageBufferTexture(imageBinding));
if (imageBinding.Texture && unit + 1 > g_imageUnitHighWaterMark) {
g_imageUnitHighWaterMark = unit + 1;
}
if (!imageBinding.Texture) {
g_GLESFuncs.glBindImageTexture(unit, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
return;
@@ -1453,6 +1507,40 @@ namespace MobileGL::MG_Backend::DirectGLES {
SyncImageTextureBinding(unit);
}
}
// What the draw path last swept the image units against. A draw never swept them at all:
// an image unit was established once, eagerly, by glBindImageTexture and never revisited.
// That is stale the moment the texture behind it is re-specified with a new size or
// format, because ES 3.1 only allows IMMUTABLE storage on an image unit
// (SyncTextureObjectToBackend's imageBindableStorageRequired), immutable storage cannot be
// redefined, and so the re-spec MINTS A NEW ES TEXTURE NAME - leaving the unit pointing at
// the deleted one and imageSize() reporting the old dimensions
// (KHR-GL43.shader_image_size.advanced-changeSize).
static Uint64 g_imageSweepContextId = 0;
static Uint64 g_imageSweepSamplingGeneration = 0;
static Uint g_imageSweepBackendContextGeneration = 0;
static Bool g_imageSweepValid = false;
// The sweep is a glBindImageTexture per unit, so it must not run per draw: the gate is the
// frontend's sampling-resolution generation, which TextureObjectBase::BumpShapeVersion
// moves on exactly the shape and format changes that can force the re-mint. Deliberately
// NOT the backend-side re-mint counter (g_attachmentBackendIdGeneration's sibling would be
// the obvious choice): a texture that is bound ONLY to an image unit is re-minted inside
// this very sweep, so a backend-side trigger would be bumped after the gate had already
// declined to run it.
void SyncImageTextureBindingsForDraw(const DrawTextureSyncKeys& keys) {
if (g_imageUnitHighWaterMark == 0) return;
if (g_imageSweepValid && g_imageSweepContextId == keys.contextId &&
g_imageSweepSamplingGeneration == keys.samplingGeneration &&
g_imageSweepBackendContextGeneration == g_backendContextGeneration) {
return;
}
SyncImageTextureBindings();
g_imageSweepContextId = keys.contextId;
g_imageSweepSamplingGeneration = keys.samplingGeneration;
g_imageSweepBackendContextGeneration = g_backendContextGeneration;
g_imageSweepValid = true;
}
} // namespace TextureImpl
namespace FramebufferImpl {
@@ -2396,6 +2484,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
syncBit & DrawSyncBit::IndirectBuffer);
VertexArrayImpl::SyncCurrentVAO(currentVAO, vaoTwin);
TextureImpl::SyncNeccessaryTextures(textureKeys);
// A draw reads and writes through its image units too, so the unit bindings have to be
// as current as the sampled ones. Gated (see the sweep): a program with no image binding
// pays one integer test, and one with images re-issues them only when a texture shape
// moved under them.
TextureImpl::SyncImageTextureBindingsForDraw(textureKeys);
// A draw writes through its image units too - the conformance case that found this
// stores into a buffer texture from the FRAGMENT stage, not from a dispatch.
TextureImpl::MarkWritableImageBufferTexturesGpuWritten();
@@ -2915,6 +3008,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
}
// Atomic counter buffers. Bound here rather than beside the storage-buffer sync
// in SyncNeccessaryBuffers because the reserved slot the transpiled ESSL reads
// them at is PROGRAM state: it is `top - GL binding` for the counter blocks THIS
// program declares, and no other program's blocks live there. Both the draw and
// the dispatch path reach this, which is what a compute-shader counter needs.
if (!backendProgram.GetAtomicCounterBindings().empty()) {
BufferImpl::SyncAtomicCounterBuffers(backendProgram.GetAtomicCounterBindings(),
backendProgram.GetAtomicCounterEsslBindingTop());
}
{
#ifdef TRACY_ENABLE
ZoneScopedNC("BindSamplerUnit", TRACY_ZONECOLOR_BACKEND);
@@ -5683,15 +5786,27 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_GLESFuncs.glDispatchComputeIndirect(indirect);
}
// An atomic counter is a shader storage block by the time it reaches the ES driver (glslang
// lowers every atomic_uint onto one), so an application that asks only for the counter
// barrier is asking about memory the driver knows as storage-buffer memory. Ordering one
// does not oblige a driver to order the other, so the counter bit implies the storage bit
// here - which is what the lowering costs and the only place it can be paid.
static GLbitfield LowerAtomicCounterBarrierBits(GLbitfield barriers) {
if ((barriers & GL_ATOMIC_COUNTER_BARRIER_BIT) != 0) {
barriers |= GL_SHADER_STORAGE_BARRIER_BIT;
}
return barriers;
}
void MemoryBarrier(GLbitfield barriers) {
g_GLESFuncs.glMemoryBarrier(barriers);
g_GLESFuncs.glMemoryBarrier(LowerAtomicCounterBarrierBits(barriers));
if (g_GLESCapabilities.IsAngleRenderer) {
g_GLESFuncs.glFlush();
}
}
void MemoryBarrierByRegion(GLbitfield barriers) {
g_GLESFuncs.glMemoryBarrierByRegion(barriers);
g_GLESFuncs.glMemoryBarrierByRegion(LowerAtomicCounterBarrierBits(barriers));
}
// One endpoint of a glCopyImageSubData, expressed the way the ES driver stores it.
@@ -5708,27 +5823,87 @@ namespace MobileGL::MG_Backend::DirectGLES {
// The 1D-array case is not just a rename: GL addresses its layers with y/height while the
// ES 2D array that backs it addresses them with z/depth, so the two axes swap with the
// target.
//
// GL_RENDERBUFFER is the exception that must NOT be translated: ES 3.2 core (and
// GL_EXT_copy_image) take it as a srcTarget/dstTarget verbatim, while
// ConvertGLEnumToTextureTarget answers Unknown for it and the translation below would hand
// the driver GL_UNKNOWN_MGL.
struct GLESCopyImageEndpoint {
GLenum target = GL_TEXTURE_2D;
// Exactly one of the two is set. The backend object is kept rather than its id, because
// the id is only stable until the OTHER endpoint syncs (a sync can re-mint a texture),
// so it is read at the point of use.
SharedPtr<TextureImpl::BackendTextureObject> texture;
SharedPtr<RenderbufferImpl::BackendRenderbufferObject> renderbuffer;
GLint x = 0;
GLint y = 0;
GLint z = 0;
Bool IsRenderbuffer() const { return renderbuffer != nullptr; }
GLuint Name() const {
if (renderbuffer) return renderbuffer->GetBackendRenderbufferId();
return texture ? texture->GetBackendTextureId() : 0u;
}
};
static GLESCopyImageEndpoint MakeGLESCopyImageEndpoint(GLenum appTarget, GLint x, GLint y, GLint z) {
const TextureTarget stateTarget = MG_Util::ConvertGLEnumToTextureTarget(appTarget);
GLESCopyImageEndpoint endpoint{};
endpoint.target = TextureImpl::ConvertTextureTargetToBackendGLEnum(stateTarget);
if (stateTarget == TextureTarget::Texture1DArray) {
endpoint.x = x;
endpoint.y = 0;
endpoint.z = y;
return endpoint;
// The renderbuffer twin of TextureImpl::SyncTextureObjectToBackend: the same
// find-or-create-then-sync the framebuffer attachment walk does (see SyncAttachmentObject),
// reachable from a path that has a renderbuffer but no framebuffer.
static SharedPtr<RenderbufferImpl::BackendRenderbufferObject> SyncRenderbufferObjectToBackend(
const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbufferObject) {
if (!renderbufferObject) return nullptr;
SharedPtr<RenderbufferImpl::BackendRenderbufferObject> backendRenderbufferObject;
if (auto* slot = RenderbufferImpl::g_backendRenderbufferObjects.Find(renderbufferObject.get())) {
backendRenderbufferObject = *slot;
} else {
auto& newSlot = RenderbufferImpl::g_backendRenderbufferObjects.GetOrCreate(renderbufferObject);
if (!newSlot) {
newSlot = MakeShared<RenderbufferImpl::BackendRenderbufferObject>();
}
backendRenderbufferObject = newSlot;
}
endpoint.x = x;
endpoint.y = y;
endpoint.z = z;
return endpoint;
backendRenderbufferObject->SyncToBackend(renderbufferObject);
return backendRenderbufferObject;
}
static Bool MakeGLESCopyImageEndpoint(const CopyImageEndpoint& endpoint, GLenum appTarget, GLint x, GLint y,
GLint z, GLESCopyImageEndpoint& out) {
if (endpoint.IsRenderbuffer()) {
out.renderbuffer = SyncRenderbufferObjectToBackend(endpoint.Renderbuffer);
if (!out.renderbuffer) return false;
out.target = GL_RENDERBUFFER;
out.x = x;
out.y = y;
out.z = z;
return true;
}
// BY VALUE, not by reference. SyncTextureObjectToBackend hands back a reference to a
// slot inside the backend texture registry, and the second call mutates that very map:
// GetOrCreate indexes it (an insert relocates entries - by rehashing, and also by
// robin-hood displacement well under the load factor), and Find drops any
// entry whose state object has expired - which, with the map open-addressed and erasing
// by shifting the probe cluster backwards, relocates entries other than the erased one.
// Either way a reference taken by the first call is stale by the time the second returns,
// and it is read four more times below. Copying the SharedPtr costs two refcount bumps on
// a path that is already doing a texture copy.
// An endpoint that named nothing is the frontend validator's INVALID_VALUE and never
// reaches here - but the assertion that says so is compiled out of a release build, and
// SyncTextureObjectToBackend would register a null state object.
if (!endpoint.Texture) return false;
out.texture = TextureImpl::SyncTextureObjectToBackend(endpoint.Texture);
if (!out.texture) return false;
const TextureTarget stateTarget = MG_Util::ConvertGLEnumToTextureTarget(appTarget);
out.target = TextureImpl::ConvertTextureTargetToBackendGLEnum(stateTarget);
if (stateTarget == TextureTarget::Texture1DArray) {
out.x = x;
out.y = 0;
out.z = y;
return true;
}
out.x = x;
out.y = y;
out.z = z;
return true;
}
// The region extent swaps the same two axes for a 1D array, and does so for whichever side
@@ -5744,85 +5919,172 @@ namespace MobileGL::MG_Backend::DirectGLES {
std::swap(height, depth);
}
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
static TextureInternalFormat GetCopyImageEndpointFormat(const CopyImageEndpoint& endpoint) {
if (endpoint.IsRenderbuffer()) return endpoint.Renderbuffer->GetInternalFormat();
return endpoint.Texture ? endpoint.Texture->GetFormat() : TextureInternalFormat::Unknown;
}
// Whether this endpoint's CPU shadow can be addressed texel-exactly by the mirror below: one
// upload target (so not a cube map, whose six chains the z axis selects between) and layers on
// the z axis (GL_TEXTURE_1D_ARRAY carries them on y).
static Bool CanMirrorCopyImageShadow(const SharedPtr<MG_State::GLState::ITextureObject>& texture) {
if (!texture) return false;
if (texture->GetTarget() == TextureTarget::Texture1DArray) return false;
return texture->GetUploadTargets().size() == 1;
}
// glCopyImageSubData is defined as a raw texel-block move, so for a destination whose CPU
// shadow has to stay authoritative - a packed format with redundant encodings, where a GPU
// readback can only answer with RE-ENCODED words (see the verbatim branch in GetTexImage) -
// the same move is replayed on the shadow. Nothing is marked dirty: the driver copy already
// put these texels on the GPU, and flagging the level would only schedule a redundant upload
// back over them.
//
// Declined, leaving the shadow exactly as it was, for every shape whose bytes this cannot
// address exactly - a renderbuffer (no shadow at all), a cube or 1D-array endpoint, a level
// whose shadow is missing or not a plain texel grid, a region outside either level, or a
// self-copy within one level, where the row copies could overlap.
static void MirrorCopyImageIntoDestinationShadow(const CopyImageEndpoint& srcEndpoint, GLint srcLevel, GLint srcX,
GLint srcY, GLint srcZ, const CopyImageEndpoint& dstEndpoint,
GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei width, GLsizei height, GLsizei depth) {
if (!CanMirrorCopyImageShadow(srcEndpoint.Texture) || !CanMirrorCopyImageShadow(dstEndpoint.Texture)) return;
if (srcEndpoint.Texture == dstEndpoint.Texture && srcLevel == dstLevel) return;
if (width <= 0 || height <= 0 || depth <= 0) return;
if (srcLevel < 0 || dstLevel < 0 || srcX < 0 || srcY < 0 || srcZ < 0 || dstX < 0 || dstY < 0 || dstZ < 0) {
return;
}
auto* srcMipmap = MG_State::GLState::AsMipmapTexture(srcEndpoint.Texture.get());
auto* dstMipmap = MG_State::GLState::AsMipmapTexture(dstEndpoint.Texture.get());
if (!srcMipmap || !dstMipmap) return;
const auto srcUploadTarget = srcEndpoint.Texture->GetUploadTargets()[0];
const auto dstUploadTarget = dstEndpoint.Texture->GetUploadTargets()[0];
const IntVec3 srcSize = srcMipmap->GetMipmapTexelSize(srcUploadTarget, static_cast<Uint>(srcLevel));
const IntVec3 dstSize = dstMipmap->GetMipmapTexelSize(dstUploadTarget, static_cast<Uint>(dstLevel));
const SizeT srcSlices = static_cast<SizeT>(std::max(srcSize.z(), 1));
const SizeT dstSlices = static_cast<SizeT>(std::max(dstSize.z(), 1));
if (srcSize.x() <= 0 || srcSize.y() <= 0 || dstSize.x() <= 0 || dstSize.y() <= 0) return;
const SizeT srcTexels = static_cast<SizeT>(srcSize.x()) * static_cast<SizeT>(srcSize.y()) * srcSlices;
const SizeT dstTexels = static_cast<SizeT>(dstSize.x()) * static_cast<SizeT>(dstSize.y()) * dstSlices;
const SizeT srcBytes = srcMipmap->GetMipmapByteSize(srcUploadTarget, static_cast<Uint>(srcLevel));
const SizeT dstBytes = dstMipmap->GetMipmapByteSize(dstUploadTarget, static_cast<Uint>(dstLevel));
// A shadow that is not exactly texels x texelSize bytes is one this cannot index (a
// compressed blob, or a level whose allocation disagrees with its recorded extent).
const SizeT texelBytes = srcTexels == 0 ? 0 : srcBytes / srcTexels;
if (texelBytes == 0 || srcBytes != srcTexels * texelBytes || dstTexels == 0 ||
dstBytes != dstTexels * texelBytes) {
return;
}
if (static_cast<SizeT>(srcX) + width > static_cast<SizeT>(srcSize.x()) ||
static_cast<SizeT>(srcY) + height > static_cast<SizeT>(srcSize.y()) ||
static_cast<SizeT>(srcZ) + depth > srcSlices ||
static_cast<SizeT>(dstX) + width > static_cast<SizeT>(dstSize.x()) ||
static_cast<SizeT>(dstY) + height > static_cast<SizeT>(dstSize.y()) ||
static_cast<SizeT>(dstZ) + depth > dstSlices) {
return;
}
const auto* srcBase = static_cast<const Uint8*>(
srcMipmap->MapMipmapData(srcUploadTarget, static_cast<Uint>(srcLevel)));
auto* dstBase = static_cast<Uint8*>(dstMipmap->MapMipmapData(dstUploadTarget, static_cast<Uint>(dstLevel)));
if (!srcBase || !dstBase) return;
const SizeT rowBytes = static_cast<SizeT>(width) * texelBytes;
for (GLsizei slice = 0; slice < depth; ++slice) {
for (GLsizei row = 0; row < height; ++row) {
const SizeT srcOffset = ((static_cast<SizeT>(srcZ + slice) * static_cast<SizeT>(srcSize.y()) +
static_cast<SizeT>(srcY + row)) *
static_cast<SizeT>(srcSize.x()) +
static_cast<SizeT>(srcX)) *
texelBytes;
const SizeT dstOffset = ((static_cast<SizeT>(dstZ + slice) * static_cast<SizeT>(dstSize.y()) +
static_cast<SizeT>(dstY + row)) *
static_cast<SizeT>(dstSize.x()) +
static_cast<SizeT>(dstX)) *
texelBytes;
Memcpy(dstBase + dstOffset, srcBase + srcOffset, rowBytes);
}
}
MGLOG_D("CopyImageSubData: mirrored %dx%dx%d texels into the destination's CPU shadow", width, height,
depth);
}
void CopyImageSubData(const CopyImageEndpoint& srcEndpoint,
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
const CopyImageEndpoint& dstEndpoint,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
// BY VALUE, not by reference. SyncTextureObjectToBackend hands back a reference to a
// slot inside the backend texture registry, and the second call mutates that very map:
// GetOrCreate indexes it (an insert relocates entries - by rehashing, and also by
// robin-hood displacement well under the load factor), and Find drops any
// entry whose state object has expired - which, with the map open-addressed and erasing
// by shifting the probe cluster backwards, relocates entries other than the erased one.
// Either way a reference taken by the first call is stale by the time the second returns,
// and it is read four more times below. Copying the SharedPtr costs two refcount bumps on
// a path that is already doing a texture copy.
const SharedPtr<TextureImpl::BackendTextureObject> srcBackendTexture =
TextureImpl::SyncTextureObjectToBackend(srcTexture);
const SharedPtr<TextureImpl::BackendTextureObject> dstBackendTexture =
TextureImpl::SyncTextureObjectToBackend(dstTexture);
GLESCopyImageEndpoint src{};
GLESCopyImageEndpoint dst{};
// The DirectVulkan half of this entry point died exactly here, on a texture whose sync
// produced nothing - and it died in a release build, where the MOBILEGL_ASSERT that was
// supposed to catch it expands to nothing. The four GetBackendTextureId() calls below
// are the same dereference. The frontend validator is what keeps this unreachable and
// what reports the error the application is owed; declining is only how a future gap up
// there stops being a crash. See the level guard in VulkanRenderer::CopyImageSubData.
if (!srcBackendTexture || !dstBackendTexture) {
MGLOG_E_ONCE("%s: source or destination texture failed to sync; declining the copy", __func__);
// supposed to catch it expands to nothing. The four Name() calls below are the same
// dereference. The frontend validator is what keeps this unreachable and what reports
// the error the application is owed; declining is only how a future gap up there stops
// being a crash. See the level guard in VulkanRenderer::CopyImageSubData.
if (!MakeGLESCopyImageEndpoint(srcEndpoint, srcTarget, srcX, srcY, srcZ, src) ||
!MakeGLESCopyImageEndpoint(dstEndpoint, dstTarget, dstX, dstY, dstZ, dst)) {
MGLOG_E_ONCE("%s: source or destination image failed to sync; declining the copy", __func__);
return;
}
const GLESCopyImageEndpoint src = MakeGLESCopyImageEndpoint(srcTarget, srcX, srcY, srcZ);
const GLESCopyImageEndpoint dst = MakeGLESCopyImageEndpoint(dstTarget, dstX, dstY, dstZ);
GLsizei copyHeight = srcHeight;
GLsizei copyDepth = srcDepth;
ApplyGLESCopyImageExtent(srcTarget, dstTarget, copyHeight, copyDepth);
const Bool srcIsDepth = MG_Util::IsDepthFormatInternalFormat(srcTexture->GetFormat());
const Bool dstIsDepth = MG_Util::IsDepthFormatInternalFormat(dstTexture->GetFormat());
const Bool srcStencil = MG_Util::IsStencilFormatInternalFormat(srcTexture->GetFormat());
const Bool dstStencil = MG_Util::IsStencilFormatInternalFormat(dstTexture->GetFormat());
if (srcIsDepth || dstIsDepth || srcStencil || dstStencil) {
const TextureInternalFormat srcFormat = GetCopyImageEndpointFormat(srcEndpoint);
const TextureInternalFormat dstFormat = GetCopyImageEndpointFormat(dstEndpoint);
// Both emulation fallbacks below are written against TEXTURE ids and texture targets, so
// an endpoint that is a renderbuffer takes the native ES copy - which accepts
// GL_RENDERBUFFER on both sides - and reports rather than mis-dispatches if the driver
// turns it down.
const Bool anyRenderbuffer = src.IsRenderbuffer() || dst.IsRenderbuffer();
const Bool srcIsDepth = MG_Util::IsDepthFormatInternalFormat(srcFormat);
const Bool dstIsDepth = MG_Util::IsDepthFormatInternalFormat(dstFormat);
const Bool srcStencil = MG_Util::IsStencilFormatInternalFormat(srcFormat);
const Bool dstStencil = MG_Util::IsStencilFormatInternalFormat(dstFormat);
if (!anyRenderbuffer && (srcIsDepth || dstIsDepth || srcStencil || dstStencil)) {
MOBILEGL_ASSERT(srcIsDepth && dstIsDepth && !srcStencil && !dstStencil,
"DirectGLES CopyImageSubData only supports depth-only image copies.");
MOBILEGL_ASSERT(src.target == GL_TEXTURE_2D && dst.target == GL_TEXTURE_2D,
"DirectGLES depth CopyImageSubData only supports GL_TEXTURE_2D.");
MOBILEGL_ASSERT(src.z == 0 && dst.z == 0 && copyDepth == 1,
"DirectGLES depth CopyImageSubData only supports single-layer copies.");
BlitDepthTexture2D(srcBackendTexture->GetBackendTextureId(), srcLevel, src.x, src.y, srcWidth, copyHeight,
dstBackendTexture->GetBackendTextureId(), dstLevel, dst.x, dst.y, srcWidth, copyHeight);
BlitDepthTexture2D(src.Name(), srcLevel, src.x, src.y, srcWidth, copyHeight,
dst.Name(), dstLevel, dst.x, dst.y, srcWidth, copyHeight);
return;
}
if (srcTexture->GetFormat() == TextureInternalFormat::R32F ||
dstTexture->GetFormat() == TextureInternalFormat::R32F) {
if (!anyRenderbuffer &&
(srcFormat == TextureInternalFormat::R32F || dstFormat == TextureInternalFormat::R32F)) {
// The single glGetError below decides the fallback dispatch, and
// ErrorLopper::Clear is compiled out at the default log level - drain
// with the always-live helper so a stale flag cannot misroute a
// succeeded native copy into the 2D-only fallback.
ClearGLErrors();
g_GLESFuncs.glCopyImageSubData(srcBackendTexture->GetBackendTextureId(), src.target, srcLevel, src.x, src.y, src.z,
dstBackendTexture->GetBackendTextureId(), dst.target, dstLevel, dst.x, dst.y, dst.z,
g_GLESFuncs.glCopyImageSubData(src.Name(), src.target, srcLevel, src.x, src.y, src.z,
dst.Name(), dst.target, dstLevel, dst.x, dst.y, dst.z,
srcWidth, copyHeight, copyDepth);
const GLenum copyImageError = g_GLESFuncs.glGetError();
if (copyImageError == GL_NO_ERROR) {
return;
}
MOBILEGL_ASSERT(IsColorOnlyFormat(srcTexture->GetFormat()) && IsColorOnlyFormat(dstTexture->GetFormat()),
MOBILEGL_ASSERT(IsColorOnlyFormat(srcFormat) && IsColorOnlyFormat(dstFormat),
"DirectGLES CopyImageSubData only supports color-only or depth-only copies.");
MOBILEGL_ASSERT(src.target == GL_TEXTURE_2D && dst.target == GL_TEXTURE_2D,
"DirectGLES color CopyImageSubData only supports GL_TEXTURE_2D.");
MOBILEGL_ASSERT(src.z == 0 && dst.z == 0 && copyDepth == 1,
"DirectGLES color CopyImageSubData only supports single-layer copies.");
CopyR32FTexture2D(srcBackendTexture->GetBackendTextureId(), srcLevel, src.x, src.y, srcWidth, copyHeight,
dstBackendTexture->GetBackendTextureId(), dst.target, dstLevel, dst.x, dst.y);
CopyR32FTexture2D(src.Name(), srcLevel, src.x, src.y, srcWidth, copyHeight,
dst.Name(), dst.target, dstLevel, dst.x, dst.y);
return;
}
ClearGLErrors();
g_GLESFuncs.glCopyImageSubData(srcBackendTexture->GetBackendTextureId(), src.target, srcLevel, src.x, src.y, src.z,
dstBackendTexture->GetBackendTextureId(), dst.target, dstLevel, dst.x, dst.y, dst.z,
g_GLESFuncs.glCopyImageSubData(src.Name(), src.target, srcLevel, src.x, src.y, src.z,
dst.Name(), dst.target, dstLevel, dst.x, dst.y, dst.z,
srcWidth, copyHeight, copyDepth);
// Every error condition glCopyImageSubData has was already ruled out by the frontend
// validator, so a driver error here is an internal invariant violation, not something
@@ -5838,6 +6100,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
MG_Util::ConvertGLEnumToString(dst.target).c_str(),
MG_Util::ConvertGLEnumToString(dstTarget).c_str());
MOBILEGL_ASSERT(false, "glCopyImageSubData failed after frontend validation accepted the request.");
return;
}
// The copy landed on the GPU. For a destination whose readback cannot be bit-exact the
// CPU shadow is what glGetTexImage answers from, so it has to follow the same move -
// otherwise it hands back whatever the level held before this copy.
if (MG_Util::PixelStoreProcessor::HasRedundantPackedEncoding(dstFormat)) {
MirrorCopyImageIntoDestinationShadow(srcEndpoint, srcLevel, srcX, srcY, srcZ, dstEndpoint, dstLevel,
dstX, dstY, dstZ, srcWidth, srcHeight, srcDepth);
}
}
@@ -7687,9 +7957,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
tempFB, GL_READ_FRAMEBUFFER, backendTexId,
backendAttachTarget == GL_UNKNOWN_MGL ? target : backendAttachTarget, level,
/*withStencil=*/format == GL_DEPTH_STENCIL);
} else if (backendAttachTarget == GL_TEXTURE_3D || backendAttachTarget == GL_TEXTURE_2D_ARRAY) {
// ES cannot attach 3D/array textures through glFramebufferTexture2D; read layer 0. Reads
// of deeper slices are served from the CPU shadow instead (see the shadow-first branch).
} else if (backendAttachTarget == GL_TEXTURE_3D || backendAttachTarget == GL_TEXTURE_2D_ARRAY ||
backendAttachTarget == GL_TEXTURE_CUBE_MAP_ARRAY) {
// ES cannot attach 3D/array textures through glFramebufferTexture2D; layer 0 here, and
// the deeper slices one at a time in the per-layer loop below. A CUBE MAP ARRAY is in
// this list for the same reason its layer-faces are addressed as array layers:
// glFramebufferTexture2D has no target token for it, so the 2D attach it used to take
// left the scratch FBO incomplete and every read fell through to the (stale) CPU
// shadow - which is exactly the all-zero result the conformance suite saw.
ScratchFBOImpl::EnsureColorAttachmentLayer(tempFB, GL_READ_FRAMEBUFFER, backendTexId, level, 0);
} else {
ScratchFBOImpl::EnsureColorAttachment2D(
@@ -7742,6 +8017,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
auto size = textureMipmapObject->GetMipmapTexelSize(MG_Util::ConvertGLEnumToTextureUploadTarget(target), level);
// GL_TEXTURE_1D_ARRAY keeps its LAYERS in the state-side height (that is what
// glTexImage2D(GL_TEXTURE_1D_ARRAY, w, layers) means), while the ES texture behind it is a
// 2D array of height 1 with the layers in depth - GetBackendUploadSize performs exactly
// that swap on the way in. Everything below addresses the ES image, so the same swap has
// to happen here: without it the readback asked layer 0 for a `layers`-row rectangle it
// does not have, and every layer but the first came back undefined (all zeroes on Adreno,
// KHR-GL4x.shader_image_load_store.basic-allTargets-*).
const Bool oneDimensionalArray = textureObject->GetTarget() == TextureTarget::Texture1DArray;
if (oneDimensionalArray) {
size = TextureImpl::GetBackendUploadSize(TextureTarget::Texture1DArray, size);
}
MGLOG_D("GetTexImage: mip level %d size = %dx%d", level, size.x(), size.y());
// Prefer the client-format conversion for every convertible combination: the "native" ES pairs
@@ -7755,12 +8042,40 @@ namespace MobileGL::MG_Backend::DirectGLES {
TextureImpl::BackendTextureFormatAddsAlpha(textureObject->GetFormat(), textureObject->GetTarget());
// GL_PACK_IMAGE_HEIGHT/GL_PACK_SKIP_IMAGES only apply to 3D/array image
// readbacks (cube-map arrays address as arrays); 2D targets must ignore
// them (GL 3.3 section 6.1.4).
const Bool applyPackImageParams = backendAttachTarget == GL_TEXTURE_3D ||
backendAttachTarget == GL_TEXTURE_2D_ARRAY ||
backendAttachTarget == GL_TEXTURE_CUBE_MAP_ARRAY;
// them (GL 3.3 section 6.1.4). A 1D ARRAY is one of those 2D targets: GL hands it back
// as a single two-dimensional image whose ROWS are the layers, so the layer stride is
// one packed row and the image parameters do not enter into it - even though the ES
// texture underneath is an array and is read one layer at a time.
const Bool applyPackImageParams = !oneDimensionalArray &&
(backendAttachTarget == GL_TEXTURE_3D ||
backendAttachTarget == GL_TEXTURE_2D_ARRAY ||
backendAttachTarget == GL_TEXTURE_CUBE_MAP_ARRAY);
const GLsizei sliceCount = std::max(size.z(), 1);
const Bool multiSlice = size.z() > 1;
// glGetTexImage answers with the STORED texels, and for a packed format whose encoding
// is not unique the GPU route below cannot: it reads GL_RGBA/GL_FLOAT and re-encodes,
// which canonicalizes an RGB9_E5 shared exponent (0xf8fc0000 -> 0xe7e00000 - the same
// value 8064, different words), and the conformance suite compares the words
// ("CopyImageSubData modified contents of source image"). The scratch FBO does NOT
// decide this for us: Adreno reports an RGB9_E5 colour attachment complete, so the
// shadow branch further down was unreachable. Serve the verbatim-word pairs from the
// shadow first and keep the GPU attempts as the fallback for a level the shadow never
// received. Every other format still prefers the GPU, so a rendered-into texture is
// unaffected; RGB9_E5 is not colour-renderable, so its shadow stays authoritative -
// and the one path that GPU-writes it, CopyImageSubData, mirrors itself into the
// shadow for exactly this reason.
const Bool verbatimPackedShadowRead =
MG_Util::PixelStoreProcessor::HasRedundantPackedEncoding(textureObject->GetFormat()) &&
MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer(
textureObject->GetFormat(), MG_Util::ConvertGLEnumToTextureInputFormat(format),
MG_Util::ConvertGLEnumToTexturePixelDataType(type));
if (verbatimPackedShadowRead &&
GetTexImageViaShadowConversion(textureMipmapObject,
MG_Util::ConvertGLEnumToTextureUploadTarget(target), level, size.x(),
size.y(), sliceCount, format, type, pixels, applyPackImageParams)) {
MGLOG_D("GetTexImage: finished via shadow conversion (verbatim packed words)");
return;
}
// A multi-slice read used to go to the CPU shadow outright, on the grounds that the
// scratch FBO can only expose one layer at a time. But the shadow only holds what was
// uploaded, so every slice that was rendered to came back stale - which is exactly what
@@ -7768,7 +8083,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Attach the layers one at a time instead and read each off the GPU, keeping the shadow
// for the formats the FBO cannot represent at all.
if (multiSlice && tempFBOComplete &&
(backendAttachTarget == GL_TEXTURE_3D || backendAttachTarget == GL_TEXTURE_2D_ARRAY)) {
(backendAttachTarget == GL_TEXTURE_3D || backendAttachTarget == GL_TEXTURE_2D_ARRAY ||
backendAttachTarget == GL_TEXTURE_CUBE_MAP_ARRAY)) {
// Each slice is packed as its own 2D image, so the per-slice call must not apply
// GL_PACK_SKIP_IMAGES / GL_PACK_IMAGE_HEIGHT itself - this walks the destination
// over them, using the same layout StoreWideRowsToClient computes.
+2 -2
View File
@@ -76,9 +76,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
GLsizei height, GLint border);
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
GLsizei height);
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
void CopyImageSubData(const CopyImageEndpoint& src,
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
const CopyImageEndpoint& dst,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
void GenerateMipmap(GLenum target);
+204 -13
View File
@@ -46,6 +46,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
constexpr const char* INDIRECT_PARAMS_BLOCK_NAME = "mg_IndirectParams";
constexpr const char* ZERO_BASED_INSTANCE_ID_NAME = "mg_ZeroBasedInstanceID";
// ES has no atomic-counter buffers: glslang lowers every atomic_uint onto a synthesized
// storage block, so one GL counter BUFFER costs one of the driver's shader-storage binding
// points. Those slots are taken from the TOP of the range downwards - below the one
// mg_IndirectParams already reserves - so an application binding its own SSBOs from 0 upwards
// never meets them, and the slot for GL binding N is `this - N` in every stage of the
// program without any shared state. Negative when the driver has no room left at all.
static Int AtomicCounterEsslBindingTop() {
return g_GLESCapabilities.MaxShaderStorageBufferBindings - 2;
}
static Bool IsAngleLlvmpipeRenderer() {
return g_GLESCapabilities.IsAngleLlvmpipeRenderer;
}
@@ -145,6 +155,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
[] { std::atexit(+[] { g_processTeardown = true; }); });
}
Bool VertexStageStorageBlockUsable(Int maxVertexShaderStorageBlocks) {
// One block is all the indirect-params view needs, so this is a >= 1 test and not a
// budget calculation. Negative is treated as unusable rather than clamped: a driver
// that leaves the out-param untouched is telling us nothing, and guessing "yes" here
// is what produces an unlinkable program.
return maxVertexShaderStorageBlocks >= 1;
}
static Bool CanUseVertexStageStorageBlock() {
return VertexStageStorageBlockUsable(g_GLESCapabilities.MaxVertexShaderStorageBlocks);
}
String EmulateBaseInstanceInVertexShader(String source, GLenum shaderType) {
if (shaderType != GL_VERTEX_SHADER || source.find("gl_BaseInstance") == String::npos) {
return source;
@@ -217,10 +239,48 @@ namespace MobileGL::MG_Backend::DirectGLES {
const Int paramsBinding = g_GLESCapabilities.MaxShaderStorageBufferBindings > 0
? g_GLESCapabilities.MaxShaderStorageBufferBindings - 1
: 0;
// The whole indirect half of this machinery is a storage block read from the VERTEX
// stage, and a storage block in the vertex stage is optional in both APIs: the
// minimum for GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS is 0 (GL 4.6 table 23.64, ES 3.2
// table 21.44) and ARM's GLES driver takes that allowance - a Mali-G925 reports 0.
// Emitting the block anyway does not make it work; it makes the program UNLINKABLE
// ("The number of vertex shader storage blocks (1) is greater than the maximum
// number allowed (0)"), and because the frontend's LINK_STATUS is glslang's and not
// the driver's, the application never learns: every draw with that program silently
// renders nothing. Dropping just the indirect half costs strictly less.
const Bool canReadIndirectParamsFromVertexStage = CanUseVertexStageStorageBlock();
String machinery;
if (source.find(String("uniform highp int ") + BASE_INSTANCE_UNIFORM_NAME + ";") == String::npos) {
machinery += String("uniform highp int ") + BASE_INSTANCE_UNIFORM_NAME + ";\n";
}
if (!canReadIndirectParamsFromVertexStage) {
// Degraded, but contained and loud. gl_BaseInstance collapses to the plain
// mg_BaseInstance uniform, which the non-indirect draw entry points do set
// correctly - so ordinary instanced draws are unaffected. What is lost is the
// per-command baseInstance of an INDIRECT draw, which lives in the (possibly
// GPU-written) command buffer and can only be read through this block: those
// draws now see the last uniform value rather than their own command's. No
// alternative path is attempted, deliberately - there is nowhere else in the
// vertex stage to read a GPU-written buffer from.
//
// MGLOG_E_ONCE, not _D: this silently changes rendering for exactly the
// workloads (Create/Flywheel indirect instancing) whose bug reports are
// impossible to read without it, and once per process is bounded.
MGLOG_E_ONCE("gl_BaseInstance: this driver reports GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS = %d, so the "
"%s storage block an indirect draw's baseInstance must be read through cannot be "
"declared in the vertex stage. Dropping indirect baseInstance support: non-indirect "
"draws are correct, indirect draws will see a stale per-command baseInstance.",
g_GLESCapabilities.MaxVertexShaderStorageBlocks, INDIRECT_PARAMS_BLOCK_NAME);
if (rebaseInstanceId) {
// Without the block there is no per-command baseInstance to subtract, and
// the uniform is the same value the define below resolves to, so rebasing
// by it would cancel the base out of gl_InstanceID twice.
machinery += String("#define ") + ZERO_BASED_INSTANCE_ID_NAME + " gl_InstanceID\n";
}
machinery += String("#define ") + BASE_INSTANCE_LOWERED_NAME + " (" + BASE_INSTANCE_UNIFORM_NAME + ")";
source.replace(pos, declaration.size(), machinery);
break;
}
machinery += String("uniform highp int ") + BASE_INSTANCE_WORD_INDEX_UNIFORM_NAME + ";\n";
machinery += String("layout(std430, binding = ") + std::to_string(paramsBinding) +
") readonly buffer " + INDIRECT_PARAMS_BLOCK_NAME +
@@ -1718,14 +1778,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
m_syncedAttributeVersions[attribIndex].BufferVersion;
if (!needsSyncFormat && !needsSyncBuffer && !needsSyncBaseInstance) 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.
// This is where a 64-bit array actually stops. glVertexAttribLFormat is a legal call
// in a GL 4.3 context and the frontend RECORDS its format (the state queries have to
// answer), so IsLong does arrive here - what this backend cannot do is FEED it:
// SupportsFloat64VertexAttributes is false because ES has no GL_DOUBLE vertex format
// and ESSL has no fp64 type, and 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.
//
// IsLong is not the only way a 64-bit array gets here: glVertexAttribFormat
// with GL_DOUBLE asks for doubles in memory CONVERTED to float, so it is not
@@ -2437,6 +2499,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
return packedData.data();
}
// "Some level of this texture holds an image", which is all the sync gate below actually
// needs to know. Deliberately weaker than ITextureObject::IsComplete(): that predicate also
// answers whether the texture SAMPLES as complete, so it must keep rejecting a chain with
// undefined lower levels - but such a texture still has to be uploaded, or the level that
// IS defined never reaches the driver at all.
static Bool HasAnyDefinedMipmapLevel(const MG_State::GLState::ITextureObject* stateTextureObject) {
const auto* mipmapObject = MG_State::GLState::AsMipmapTexture(stateTextureObject);
if (mipmapObject == nullptr) return false;
const auto levelCount = mipmapObject->GetMipmapLevelCount();
for (const auto& uploadTarget : stateTextureObject->GetUploadTargets()) {
for (Uint level = 0; level < levelCount; ++level) {
const auto levelTexelSize = mipmapObject->GetMipmapTexelSize(uploadTarget, level);
if (levelTexelSize.x() > 0 && levelTexelSize.y() > 0 && levelTexelSize.z() > 0) {
return true;
}
}
}
return false;
}
void BackendTextureObject::SyncMipmapsToBackend(
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {
if (!stateTextureObject) {
@@ -2484,8 +2566,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
// 3. Size changed
// 4. Mipmap levels changed
if (!stateTextureObject->IsComplete()) {
MGLOG_D("Texture object with ID: %u is not complete, skipping sync.",
// IsComplete() is the sampling predicate, and it calls a chain whose lower levels are
// undefined incomplete - which is what a top-down build (upload level N, then level 0)
// and ARB_clear_texture's conformance cases both produce. Bailing out on that shape
// left the backend name with no levels whatsoever, so the level that WAS defined could
// never be sampled or read back. Sync whenever some level holds an image; the per-level
// loops below skip the degenerate ones individually.
if (!stateTextureObject->IsComplete() && !HasAnyDefinedMipmapLevel(stateTextureObject.get())) {
MGLOG_D("Texture object with ID: %u has no defined image level, skipping sync.",
stateTextureObject->GetExternalIndex());
return;
}
@@ -2587,6 +2675,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
for (auto& uploadTarget : uploadTargets) {
for (SizeT level = m_prevTextureInfo.mipmapLevels; level < mipmapCount; ++level) {
auto levelTexelSize = textureMipmapObject->GetMipmapTexelSize(uploadTarget, level);
// A level the application never defined reads back as {0, 0, 0}; now that a
// sparse chain is synced rather than skipped whole, leave those undefined on
// the driver instead of giving the name a 0x0 image at that index.
if (levelTexelSize.x() <= 0 || levelTexelSize.y() <= 0 || levelTexelSize.z() <= 0) {
textureMipmapObject->MarkStorageDirty(uploadTarget, level, false);
continue;
}
auto levelByteSize = textureMipmapObject->GetMipmapByteSize(uploadTarget, level);
bool levelDirty = textureMipmapObject->IsStorageDirty(uploadTarget, level);
auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget);
@@ -2814,6 +2909,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
for (auto& uploadTarget : uploadTargets) {
for (SizeT level = 0; level < mipmapCount; ++level) {
auto levelTexelSize = textureMipmapObject->GetMipmapTexelSize(uploadTarget, level);
// See the append-mips loop: an undefined level stays undefined on the
// driver rather than becoming a 0x0 image.
if (levelTexelSize.x() <= 0 || levelTexelSize.y() <= 0 ||
levelTexelSize.z() <= 0) {
textureMipmapObject->MarkStorageDirty(uploadTarget, level, false);
continue;
}
auto levelByteSize = textureMipmapObject->GetMipmapByteSize(uploadTarget, level);
bool levelDirty = textureMipmapObject->IsStorageDirty(uploadTarget, level);
auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget);
@@ -4665,6 +4767,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
ImageFormatBakeInputs CollectImageFormatBakeInputs(
const MG_State::GLState::ProgramObject& stateProgramObject) {
ImageFormatBakeInputs inputs;
// A format GLSL ES cannot spell on a driver with no GL_NV_image_formats to spell it
// with. There is no legal ESSL for such a shader at all, so the stage will not
// compile and the program is lost - a failure that used to leave nothing behind but
// a draw that rendered nothing. Recorded and reported ONCE per program build rather
// than per uniform: an image array reaches this decision once per element.
String unspellableUniform;
String unspellableFormat;
Uint unspellableCount = 0;
const auto recordUnspellableFormat = [&](const String& uniformName, String formatSpelling) {
if (unspellableCount == 0) {
unspellableUniform = uniformName;
unspellableFormat = Move(formatSpelling);
}
++unspellableCount;
};
const Uint maxUniformLoc = stateProgramObject.GetMaxUniformLocation();
for (Uint loc = 0; loc <= maxUniformLoc; ++loc) {
const auto& name = stateProgramObject.GetUniformName(loc);
@@ -4676,6 +4794,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
// still needs the extension directive to survive the ES compiler.
if (!IsCoreEsslLayoutFormat(static_cast<glslang::TLayoutFormat>(type.layoutFormat))) {
inputs.needsExtendedImageFormats = true;
if (!g_GLESCapabilities.SupportsExtendedImageFormats) {
// From the OWNED TypeFacts, not from a live TType: the reflection
// snapshot already carries the declared layout format, and there is
// no glslang object to ask on a translation-cache L1 hit.
recordUnspellableFormat(
name, glslang::TQualifier::getLayoutFormatString(
static_cast<glslang::TLayoutFormat>(type.layoutFormat)));
}
}
continue;
}
@@ -4701,6 +4827,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
MGLOG_D("Image uniform '%s' has no declared format and its unit %d holds 0x%x, which GLSL ES "
"core cannot spell and this driver has no GL_NV_image_formats for.",
name.c_str(), unit, boundFormat);
recordUnspellableFormat(
name, MG_Util::ShaderTranspiler::ShaderCompiler::EsslImageFormatSpelling(boundFormat));
continue;
}
inputs.needsExtendedImageFormats = true;
@@ -4743,6 +4871,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
for (const auto& name : textCompleted) {
inputs.glFormatByUniformName.erase(name);
}
// Unlatched MGLOG_E, like the transpile- and link-failure diagnostics in SyncToBackend:
// one line per failing program build, and naming the uniform and the format is the
// whole diagnostic value. Left as a log rather than a link failure on purpose - the
// frontend has already reported LINK_STATUS = true and GL cannot retract it, and the
// program stays queryable exactly as the "linked but not drawable" exit leaves it.
if (unspellableCount != 0) {
MGLOG_E("Image format '%s' on uniform '%s' has no GLSL ES spelling and this driver does not expose "
"GL_NV_image_formats%s; the stage using it cannot compile and the program will draw "
"nothing.",
unspellableFormat.empty() ? "(none)" : unspellableFormat.c_str(), unspellableUniform.c_str(),
unspellableCount > 1 ? " (and it is not the only image uniform affected)" : "");
}
return inputs;
}
@@ -4789,8 +4929,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
const Vector<unsigned int>& spirvCode, const GLenum glShaderType,
const std::set<String>& xfbCaptureBlockNames, const ImageFormatBakeInputs& imageFormatBake,
const UnorderedMap<String, Int>& storageBlockBindingOverrides,
const Bool enableSpirvValidation, String& outSource,
std::set<String>& outFlattenedXfbBlockNames, String& outError) const {
const Int atomicCounterEsslBindingTop, const Bool enableSpirvValidation, String& outSource,
std::set<String>& outFlattenedXfbBlockNames, Vector<Int>& outAtomicCounterGlBindings,
String& outError) const {
// ESSL cannot express gl_DrawID/gl_BaseInstance/gl_BaseVertex; demote them to
// plain globals (mg_*) before handing the module to SPIRV-Cross.
Vector<unsigned int> loweredSpirv;
@@ -5021,6 +5162,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
spvcSession.SetShaderStorageBlockBinding(storageBlockBindingOverrides);
}
// Atomic counters, same mechanism for the same reason. glslang already turned
// every atomic_uint into a member of gl_AtomicCounterBlock_<N> and let the IO
// mapper pick that block's binding, which has no relation to the GL binding point
// N the application bound its counter buffer to - and can alias an SSBO the
// application binds itself. Move each block to its reserved slot and record N, so
// the draw path knows which GL_ATOMIC_COUNTER_BUFFER points to re-issue as
// storage-buffer bindings.
//
// BOTH HALVES ARE MEMO STATE. `atomicCounterEsslBindingTop` decides the binding
// this prints into the ESSL, so it is in the L2 key; `outAtomicCounterGlBindings`
// is an OUTPUT this stage produces and the draw path consumes, so it is in the L2
// payload. A hit that replayed only the text would leave the bindings empty and
// every counter buffer unbound - the same class of silent loss the flattened XFB
// block names would have been.
spvcSession.SetAtomicCounterBlockBindings(atomicCounterEsslBindingTop,
outAtomicCounterGlBindings);
const char* result = nullptr;
spvcSession.Compile(&result);
@@ -5077,6 +5235,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
// this build current - the draw path compares the signature and rebuilds on a change.
const auto& storageBlockBindingOverrides = stateProgramObject->GetShaderStorageBlockBindingOverrides();
m_shaderStorageBlockBindingSignature = ComputeShaderStorageBlockBindingSignature(*stateProgramObject);
// Rebuilt by the transpile loop below, one entry per atomic-counter block it finds.
// The top is snapshotted here so every stage of this program - and the draw path
// reading it afterwards - resolves the same slot for the same GL binding.
m_atomicCounterGlBindings.clear();
m_atomicCounterEsslBindingTop = AtomicCounterEsslBindingTop();
// The same shape again for image FORMATS: what a format-less image declaration
// compiles to depends on live glBindImageTexture state, so the pairs it was built
// against are recorded here and compared per draw (ImageUnitFormatsStillMatch).
@@ -5195,6 +5358,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
esslKeyInputs.glFormatByUniformName = &imageFormatBake.glFormatByUniformName;
esslKeyInputs.storageBlockBindingOverrides = &storageBlockBindingOverrides;
esslKeyInputs.esslVersion = ResolveBackendEsslVersion();
esslKeyInputs.atomicCounterEsslBindingTop = m_atomicCounterEsslBindingTop;
esslKeyInputs.enableSpirvValidation = enableSpirvValidation;
auto& esslCache = MG_Util::ShaderTranspiler::GetEsslTranslationCache();
@@ -5204,17 +5368,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
std::set<String> stageFlattenedXfbBlockNames;
// Per stage, and NOT m_atomicCounterGlBindings directly: on a miss the
// transpile appends to this, on a hit the payload supplies it, and only then
// is it folded into the program-wide vector. Pointing the transpile straight
// at the member would have made the miss path and the hit path disagree about
// who owns the append.
Vector<Int> stageAtomicCounterGlBindings;
const MG_Util::ShaderTranspiler::EsslTranslationResultPtr esslHit =
esslCacheKey.Valid() ? esslCache.Find(esslCacheKey) : nullptr;
if (esslHit) {
source = esslHit->essl;
stageFlattenedXfbBlockNames = esslHit->flattenedXfbBlockNames;
stageAtomicCounterGlBindings = esslHit->atomicCounterGlBindings;
} else {
String transpileError;
if (!TranspileSpirvToEssl(spirvCode, glShaderType, xfbCaptureBlockNames,
imageFormatBake, storageBlockBindingOverrides,
m_atomicCounterEsslBindingTop,
enableSpirvValidation, source,
stageFlattenedXfbBlockNames, transpileError)) {
stageFlattenedXfbBlockNames,
stageAtomicCounterGlBindings, transpileError)) {
// MGLOG_E, unlatched, like the compile- and link-failure diagnostics
// below: one line per failing stage is bounded by program count and
// naming the stage is the entire diagnostic value. A stage that never
@@ -5234,6 +5407,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
auto payload = MakeShared<MG_Util::ShaderTranspiler::EsslTranslationResult>();
payload->essl = source;
payload->flattenedXfbBlockNames = stageFlattenedXfbBlockNames;
payload->atomicCounterGlBindings = stageAtomicCounterGlBindings;
const SizeT payloadBytes =
MG_Util::ShaderTranspiler::EsslTranslationResultBytes(*payload);
esslCache.Insert(
@@ -5248,6 +5422,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
// the transpile so a cache HIT contributes its names too.
flattenedXfbBlockNames.insert(stageFlattenedXfbBlockNames.begin(),
stageFlattenedXfbBlockNames.end());
// Same rule for the atomic-counter bindings this stage declared, and for the
// same reason: the loop below de-duplicates across stages, so a hit that
// contributed nothing would silently drop a counter buffer the draw path has
// to bind.
m_atomicCounterGlBindings.insert(m_atomicCounterGlBindings.end(),
stageAtomicCounterGlBindings.begin(),
stageAtomicCounterGlBindings.end());
// Position in the chain is arbitrary: this is the only header-level rewrite, it
// edits #extension directives and never the body, and the replacement is the
@@ -5374,6 +5555,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
MGLOG_D("Processed shader source length: %zu", source.length());
}
// A counter buffer declared by several stages was recorded once per stage; the draw
// path binds per GL binding point, so collapse the duplicates here rather than
// re-issuing the same glBindBufferBase two or three times every draw.
if (!m_atomicCounterGlBindings.empty()) {
std::sort(m_atomicCounterGlBindings.begin(), m_atomicCounterGlBindings.end());
m_atomicCounterGlBindings.erase(
std::unique(m_atomicCounterGlBindings.begin(), m_atomicCounterGlBindings.end()),
m_atomicCounterGlBindings.end());
}
// Transform feedback capture runs on the real driver (see XfbImpl in
// DirectGLES.cpp), so the capture set has to be declared on the backend
// program before it links. SPIRV-Cross keeps user output names verbatim in
+29 -2
View File
@@ -21,6 +21,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
String EmulateBaseInstanceInVertexShader(String source, GLenum shaderType);
String PromoteDrawParameterGlobalsToUniforms(String source, GLenum shaderType);
// Whether a vertex shader may declare a storage block at all, given what the host driver
// reports for GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS. Pure, and separated from the capability
// global purely so the decision can be tested without one.
//
// The indirect half of the gl_BaseInstance lowering in PromoteDrawParameterGlobalsToUniforms
// is the only thing that needs this, and it needs exactly one block. A driver reporting 0 is
// conformant - the minimum is 0 in GL 4.6 table 23.64 and ES 3.2 table 21.44 - and ARM's
// GLES driver does report 0, so this is a live path, not a defensive one.
Bool VertexStageStorageBlockUsable(Int maxVertexShaderStorageBlocks);
// True once the process has entered exit(): past that point the EGL library and
// the driver may already be unloaded, so a backend twin's destructor must not
// call into g_GLESFuncs (the observed crash is a jump through an unmapped driver
@@ -386,6 +396,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
void BindBufferBaseCached(GLenum glTarget, Uint index, Uint id);
void BindBufferRangeCached(GLenum glTarget, Uint index, Uint id, GLintptr offset, GLsizeiptr size);
void InvalidateIndexedBufferBindingCache();
// Re-issues the GL_ATOMIC_COUNTER_BUFFER binding points a program's shaders declare as
// GL_SHADER_STORAGE_BUFFER bindings at the reserved slots the transpiled ESSL was built
// against (BackendProgramObjectImpl::GetAtomicCounterBindings /
// GetAtomicCounterEsslBindingTop). ES has no counter-buffer target at all, so without
// this the shader reads a storage block nobody ever bound a buffer to and the buffer the
// application bound never reaches the driver.
void SyncAtomicCounterBuffers(const Vector<Int>& glBindings, Int esslBindingTop);
// Buffer-storage pool maintenance. TrimBufferPool evicts over-budget entries
// (called once per frame from Present); ClearBufferPool drops all pooled ids
// without glDeleteBuffers (called when the ES context is going away).
@@ -1164,6 +1181,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
// qualifier, so the overrides are baked into the source). A mismatch means the
// program is stale exactly like the clamp masks above.
Uint64 GetShaderStorageBlockBindingSignature() const { return m_shaderStorageBlockBindingSignature; }
// GL atomic-counter binding points the transpiled stages declare (sorted, unique),
// and the top of the reserved shader-storage range their counter blocks were
// transpiled against - the slot for GL binding N is `top - N`. Empty for every
// program that uses no atomic counter, which is what keeps the per-draw cost of the
// counter sync at one empty-vector test.
const Vector<Int>& GetAtomicCounterBindings() const { return m_atomicCounterGlBindings; }
Int GetAtomicCounterEsslBindingTop() const { return m_atomicCounterEsslBindingTop; }
Bool HasGlobalUboBlock() const { return m_globalUboBackendBlockIndex >= 0; }
const Vector<Int>& GetUniformBlockBackendIndices() const { return m_uniformBlockBackendIndices; }
@@ -1222,9 +1246,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
const std::set<String>& xfbCaptureBlockNames,
const ImageFormatBakeInputs& imageFormatBake,
const UnorderedMap<String, Int>& storageBlockBindingOverrides,
Bool enableSpirvValidation, String& outSource,
Int atomicCounterEsslBindingTop, Bool enableSpirvValidation,
String& outSource,
std::set<String>& outFlattenedXfbBlockNames,
String& outError) const;
Vector<Int>& outAtomicCounterGlBindings, String& outError) const;
Uint m_backendProgramId = 0;
// GL name of the frontend program this was last synced from; diagnostics only, so
@@ -1244,6 +1269,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
Uint m_fragColorBroadcastCount = 1;
// 0 is the signature of an empty override set, i.e. what almost every program has.
Uint64 m_shaderStorageBlockBindingSignature = 0;
Vector<Int> m_atomicCounterGlBindings;
Int m_atomicCounterEsslBindingTop = -1;
Bool m_isInitialized = false;
Bool m_backendProgramUsable = false;
+19 -3
View File
@@ -822,9 +822,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
// A rebuilt declaration. Keeps SPIRV-Cross's own word order (`uniform readonly
// highp image2D`) so the image-rebinding regex in Managers.cpp still matches what
// comes out of here, whichever order the two passes end up running in.
//
// `forceCoherent` is for the SPLIT pair only. GLSL guarantees that a write through
// one image variable is visible to a read through a DIFFERENT one only when both are
// declared coherent, and the split turns a same-variable read-after-write - which
// desktop GLSL orders by construction, so the source almost never says `coherent` -
// into exactly that cross-variable shape. Without it the driver may serve the load
// from a cache that never saw the store through the writeonly half.
String BuildImageDeclaration(const ImageUniformDecl& decl, const char* memoryQualifier,
const String& variableName) {
const String& variableName, Bool forceCoherent = false) {
String out = "layout(" + decl.layout + ") uniform ";
if (forceCoherent && !ContainsIdentifier(decl.qualifiers, "coherent")) {
out += "coherent ";
}
out += memoryQualifier;
out += ' ';
if (!decl.qualifiers.empty()) {
@@ -1008,9 +1018,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
decl.writeName = MakeImageWriteAliasName(decl.name, glslCode, takenAliases);
takenAliases.push_back(decl.writeName);
decl.split = true;
// Both halves carry `coherent`; see BuildImageDeclaration. The
// single-declaration cases below stay as they were - nothing aliases them, so
// there is no visibility to restore and no reason to pay for the cache
// behaviour.
edits.push_back({decl.declStart, decl.declLength,
BuildImageDeclaration(decl, "readonly", decl.name) + "\n" +
BuildImageDeclaration(decl, "writeonly", decl.writeName)});
BuildImageDeclaration(decl, "readonly", decl.name, /*forceCoherent=*/true) +
"\n" +
BuildImageDeclaration(decl, "writeonly", decl.writeName,
/*forceCoherent=*/true)});
} else if (decl.stored) {
edits.push_back({decl.declStart, decl.declLength,
BuildImageDeclaration(decl, "writeonly", decl.name)});
+12 -5
View File
@@ -196,11 +196,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
// * loaded only -> add `readonly`
// * stored only -> add `writeonly`
// * both -> emit TWO declarations on the same binding and of the
// same type, `readonly <name>` and `writeonly
// <IMAGE_WRITE_ALIAS_PREFIX><name>`, and point every
// imageStore at the second one. Several image variables
// may share an image unit as long as they have the same
// type and format, which is exactly what the pair is.
// same type, `coherent readonly <name>` and `coherent
// writeonly <IMAGE_WRITE_ALIAS_PREFIX><name>`, and point
// every imageStore at the second one. Several image
// variables may share an image unit as long as they have
// the same type and format, which is exactly what the pair
// is.
//
// The `coherent` on both halves of the pair is load-bearing, not decoration: GLSL only
// guarantees a write through one image variable is visible to a read through a DIFFERENT
// one when both are coherent, and the split is what makes a same-variable
// read-after-write cross-variable. The single-declaration repairs above do not get it -
// nothing aliases them.
//
// Budget note: the split DOUBLES the image-uniform count of the stage it fires in, so
// a driver advertising a tight GL_MAX_{FRAGMENT,VERTEX,...}_IMAGE_UNIFORMS can turn a
@@ -847,6 +847,38 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_dynamicParameters.MaxShaderStorageBufferBindings =
clampLimit("GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS", m_vulkanCaps.MaxShaderStorageBufferBindings,
kMaxAdvertisedBufferBlocks);
// Per-stage GL_MAX_*_SHADER_STORAGE_BLOCKS. Vulkan has one descriptor limit for every
// stage (maxPerStageDescriptorStorageBuffers, which is what MaxComputeShaderStorageBlocks
// carries), so the stage limits differ only by whether the stage can have blocks at all.
//
// Deliberately NOT gated on vertexPipelineStoresAndAtomics, unlike the per-stage image
// uniforms below. That gate reads as the obvious one and is wrong here in practice: a
// Mali-G925-Immortalis reports vertexPipelineStoresAndAtomics=false (supported AND
// enabled) and yet runs all 433 KHR-GL43.constant_expressions.*_tess_* cases correctly
// through this backend - those write their result through a storage block declared in a
// tessellation stage. Gating would report 0 and turn 433 passing cases into
// "unsupported", removing function that demonstrably works.
//
// The asymmetry with DirectGLES is real and is the point. There, 0 prevents a program
// the driver refuses outright at link time; the honest limit converts a silent
// wrong-render into a capability an application can route around. Here there is no such
// failure to prevent, so the limit stays at what the device can address. If a Vulkan
// device is ever found that genuinely rejects such a pipeline, the gate belongs at
// pipeline creation where the rejection is observable, not on a feature bit this driver
// reports inaccurately.
{
const Int maxPerStageStorageBlocks =
std::min(std::max(m_dynamicParameters.MaxComputeShaderStorageBlocks, 0),
std::min(std::max(m_dynamicParameters.MaxCombinedShaderStorageBlocks, 0),
std::max(m_dynamicParameters.MaxShaderStorageBufferBindings, 0)));
m_dynamicParameters.MaxVertexShaderStorageBlocks = maxPerStageStorageBlocks;
m_dynamicParameters.MaxTessControlShaderStorageBlocks = maxPerStageStorageBlocks;
m_dynamicParameters.MaxTessEvaluationShaderStorageBlocks = maxPerStageStorageBlocks;
// The one hard capability in the set: no geometry stage means no blocks in it.
m_dynamicParameters.MaxGeometryShaderStorageBlocks =
m_vulkanCaps.SupportsGeometryShader ? maxPerStageStorageBlocks : 0;
m_dynamicParameters.MaxFragmentShaderStorageBlocks = maxPerStageStorageBlocks;
}
m_dynamicParameters.MaxTextureBufferSize = clampLimit(
"GL_MAX_TEXTURE_BUFFER_SIZE", m_vulkanCaps.MaxTextureBufferSize, kMaxAdvertisedTextureBufferSize);
m_dynamicParameters.TextureBufferOffsetAlignment = m_vulkanCaps.TextureBufferOffsetAlignment;
@@ -632,15 +632,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::CopyTexSubImage2D called with null GL context");
pVulkanRenderer->CopyTexSubImage2D(target, level, xoffset, yoffset, x, y, width, height);
}
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
void CopyImageSubData(const CopyImageEndpoint& src,
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
const CopyImageEndpoint& dst,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::CopyImageSubData called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::CopyImageSubData called with null GL context");
pVulkanRenderer->CopyImageSubData(srcTexture, srcTarget, srcLevel, srcX, srcY, srcZ,
dstTexture, dstTarget, dstLevel, dstX, dstY, dstZ,
pVulkanRenderer->CopyImageSubData(src, srcTarget, srcLevel, srcX, srcY, srcZ,
dst, dstTarget, dstLevel, dstX, dstY, dstZ,
srcWidth, srcHeight, srcDepth);
}
void GenerateMipmap(GLenum target) {
@@ -82,9 +82,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
GLsizei height, GLint border);
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
GLsizei height);
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
void CopyImageSubData(const CopyImageEndpoint& src,
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
const CopyImageEndpoint& dst,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
void GenerateMipmap(GLenum target);
@@ -8,6 +8,7 @@
#include "VertexInputStateFactory.h"
#include "MG_Util/Converters/MGToStr/DataTypeConverter.h"
#include <MG_Backend/BackendObjects.h>
#include <utility>
namespace MobileGL::MG_Backend::DirectVulkan {
@@ -330,6 +331,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// 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.
//
// ... as long as the shader half still runs. It does not when the backend has declared
// no 64-bit vertex attribute support: DemoteFloat64Pass has already narrowed every
// `dvec` input to a `vec` by then, so PackDoubleVertexInputsPass finds nothing to pack
// and a UINT-formatted attribute would be fed to a float input - garbage with no
// diagnostic anywhere. Declining here drops the array instead (the caller skips
// UNDEFINED attributes and reports them through unsupportedAttribMask), which is what
// DirectGLES does for the same state. The frontend RECORDS the format either way, so
// this gate is the only thing standing between a legal glVertexAttribLFormat and a
// mismatched pipeline.
if (MG_Backend::pActiveBackendObject == nullptr ||
!MG_Backend::pActiveBackendObject->GetDynamicParameters().SupportsFloat64VertexAttributes) {
return VK_FORMAT_UNDEFINED;
}
if (!isLong || isInteger || normalized) return VK_FORMAT_UNDEFINED;
switch (size) {
case 1: return VK_FORMAT_R32G32_UINT;
@@ -1494,6 +1494,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const auto* mipTexture = MG_State::GLState::AsMipmapTexture(&texture);
const Uint32 mipLevelCount = mipTexture != nullptr ? mipTexture->GetMipmapLevelCount() : 0u;
return resource.syncedContentVersion != texture.GetContentVersion() ||
resource.syncedShapeVersion != texture.GetShapeVersion() ||
resource.syncedTextureParamsVersion != texture.GetTextureParamsVersion() ||
resource.syncedMipLevelCount != mipLevelCount;
}
@@ -1593,11 +1594,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool VkTextureManager::SyncTexture(MG_State::GLState::ITextureObject &texture,
TextureResource &outResource) {
// Cross-draw fast path: if the resource is already built and neither the texture's
// pixel content (bumped in MarkStorageDirty) nor its params changed since the last
// sync, there is nothing to re-check or re-upload - skip CheckMipmapCompleteness,
// SyncTextureResource, SyncTextureViews and the per-level dirty scan. Layout is
// maintained separately by the transition path, so the resource still reflects truth.
// pixel content (bumped in MarkStorageDirty), its SHAPE (bumped in BumpShapeVersion)
// nor its params changed since the last sync, there is nothing to re-check or
// re-upload - skip CheckMipmapCompleteness, SyncTextureResource, SyncTextureViews and
// the per-level dirty scan. Layout is maintained separately by the transition path, so
// the resource still reflects truth. The shape version is NOT redundant with the
// content one: glTexImage2D(..., nullptr) re-specifies a level's size or format
// without dirtying a texel, which is exactly how a re-specified image-unit texture used
// to keep reporting its old imageSize().
const Uint64 syncingContentVersion = texture.GetContentVersion();
const Uint64 syncingShapeVersion = texture.GetShapeVersion();
const auto* syncingMipTexture = MG_State::GLState::AsMipmapTexture(&texture);
const Uint32 syncingMipLevelCount =
syncingMipTexture != nullptr ? syncingMipTexture->GetMipmapLevelCount() : 0u;
@@ -1609,6 +1615,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_storageImageTextures.find(MakeTextureIdentity(&texture)) != m_storageImageTextures.end();
if (outResource.image != VK_NULL_HANDLE && !storageUpgradePending &&
outResource.syncedContentVersion == syncingContentVersion &&
outResource.syncedShapeVersion == syncingShapeVersion &&
outResource.syncedTextureParamsVersion == texture.GetTextureParamsVersion() &&
outResource.syncedMipLevelCount == syncingMipLevelCount) {
return true;
@@ -1629,6 +1636,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return false;
}
// From here down the size is VULKAN geometry, not GL's: a 1D array's layer count moves
// out of the height it occupies GL-side and into z, which is the slot
// TryResolveTextureShapeInfo reads arrayLayers from and the only one that leaves
// extent.height at the 1 a VK_IMAGE_TYPE_1D image is required to have.
texelSize = ToVulkanLevelExtent(texture.GetTarget(), texelSize);
if (!SyncTextureResource(texture, uploadTarget, texelSize, byteSize, mipLevelCount, outResource)) {
MGLOG_D("%s: SyncTextureResource failed", __func__);
return false;
@@ -1660,6 +1673,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (!hasDirtyMipLevel) {
outResource.syncedContentVersion = syncingContentVersion;
outResource.syncedMipLevelCount = syncingMipLevelCount;
outResource.syncedShapeVersion = syncingShapeVersion;
return true;
}
@@ -1669,6 +1683,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
outResource.syncedContentVersion = syncingContentVersion;
outResource.syncedMipLevelCount = syncingMipLevelCount;
outResource.syncedShapeVersion = syncingShapeVersion;
return true;
}
@@ -2536,7 +2551,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
uploadItem.target = target;
uploadItem.level = level;
uploadItem.baseArrayLayer = ResolveUploadArrayLayer(target);
uploadItem.texelSize = texelSize;
// Vulkan geometry, like the image this stages into (see SyncTexture): a 1D
// array's layers move from y to z, where the copy loop's depthSelectsArrayLayer
// branch turns them into layerCount. The shadow needs no repacking to follow -
// one layer of a 1D array IS one row of `width` texels, so the tight-packed
// per-layer copy the swapped size describes reads the same bytes in the same
// order as the row-major level it replaces.
uploadItem.texelSize = ToVulkanLevelExtent(mipmapTexture.GetTarget(), texelSize);
uploadItem.source = source;
uploadItem.offset = stagingSize;
uploadItem.uploadByteSize = byteSize;
@@ -2574,6 +2595,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
uploadItem.uploadByteSize = rectTexels * uploadItem.texelBytes;
}
// The boxes came out of the shadow in GL coordinates, where a 1D
// array's layer is the y. They have to follow texelSize across to z or
// they would address rows of an image that now has exactly one, and
// the staging walk would read the wrong bytes for them. Every byte
// count computed above is a product of the three extents, so moving
// the axes leaves all of them alone - and an OFFSET lands on a zero y,
// not on the extent's one, which is why this is spelled out rather than
// handed to ToVulkanLevelExtent.
if (mipmapTexture.GetTarget() == TextureTarget::Texture1DArray) {
uploadItem.regionLo = {uploadItem.regionLo.x(), 0, uploadItem.regionLo.y()};
uploadItem.regionSize = {uploadItem.regionSize.x(), 1,
uploadItem.regionSize.y()};
for (auto& rect : uploadItem.rects) {
rect.lo = {rect.lo.x(), 0, rect.lo.y()};
rect.hi = {rect.hi.x(), 1, rect.hi.y()};
}
}
}
}
if (formatInfo.expandRgbToRgba) {
@@ -22,6 +22,25 @@ class ITextureObject;
namespace MobileGL::MG_Backend::DirectVulkan {
enum class SamplerNumericDomain : Uint8;
// A GL 1D-ARRAY level keeps its LAYER COUNT in the state-side HEIGHT: that is what
// glTexImage2D(GL_TEXTURE_1D_ARRAY, width, layers) means, and the frontend records the level
// as {width, layers, 1} (see GL_Texture.cpp's AllocateStorage and the completeness walk in
// TextureObject.cpp, which shrinks only x down the chain). Vulkan packs it the other way: a
// 1D array is a VK_IMAGE_TYPE_1D image whose extent.height MUST be 1 and whose layers live in
// arrayLayers - i.e. in the slot this backend reads out of z. So every place that turns a GL
// level size into Vulkan image geometry has to move the count across first, and every GL-space
// sub-box that rides along with it has to move its y the same way. DirectGLES performs the
// identical remap onto the ES 2D array it maps 1D arrays to (GetBackendUploadSize).
//
// Applied to nothing else: a 2D array, a cube array and a 3D texture all already carry their
// depth/layer count in z, which is where the Vulkan side expects it.
inline IntVec3 ToVulkanLevelExtent(TextureTarget stateTarget, const IntVec3& glTexelSize) {
if (stateTarget == TextureTarget::Texture1DArray) {
return {glTexelSize.x(), 1, glTexelSize.y()};
}
return glTexelSize;
}
class VkTextureManager {
public:
// Monotonic epoch bumped whenever a texture VkImage is (re)created. The render-pass
@@ -206,6 +225,12 @@ public:
// as defense-in-depth: any path that grows the level set (which resizes the sampled view)
// busts the skip even if it failed to bump the content version.
Uint32 syncedMipLevelCount = 0;
// Snapshot of ITextureObject::GetShapeVersion() at the last successful sync. The content
// version alone does NOT cover a re-specification: glTexImage2D(..., nullptr) on an
// already-defined level changes its size or format and dirties no texel, so it moves the
// shape version and nothing else. Without this in the early-out key the image, its views
// and therefore imageSize() all keep answering with the texture's PREVIOUS shape.
Uint64 syncedShapeVersion = 0;
TextureResource() = default;
TextureResource(const TextureResource&) = delete;
@@ -237,6 +262,7 @@ public:
std::swap(this->lastRecordingGeneration, that.lastRecordingGeneration);
std::swap(this->syncedContentVersion, that.syncedContentVersion);
std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount);
std::swap(this->syncedShapeVersion, that.syncedShapeVersion);
}
void Reset() {
@@ -300,6 +326,7 @@ public:
syncedTextureParamsVersion = 0;
syncedContentVersion = 0;
syncedMipLevelCount = 0;
syncedShapeVersion = 0;
}
~TextureResource() {
@@ -8869,7 +8869,7 @@ void main() {
// A mixed 2D-array <-> 3D pair is legal because maintenance1 - core since Vulkan 1.1 -
// relaxed the old "layerCounts must match" rule into "the 3D side's extent.depth must
// equal the array side's layerCount".
struct CopyImageEndpoint {
struct CopyImageSliceMapping {
// True for a VK_IMAGE_TYPE_3D image, i.e. slices ride the z axis, not the layer axis.
Bool slicesAreDepth = false;
// The GL z offset, kept in whichever field this endpoint's image type reads it from.
@@ -8883,13 +8883,35 @@ void main() {
Int32 OffsetZ() const { return slicesAreDepth ? static_cast<Int32>(baseSlice) : 0; }
};
Bool TryResolveCopyImageEndpoint(TextureTarget target,
const VkTextureManager::TextureResource& resource, Uint32 mipLevel,
GLint glZ, GLsizei glDepth, CopyImageEndpoint& outEndpoint) {
// The Vulkan image one glCopyImageSubData endpoint names, after the two object kinds GL
// 4.6 core 18.3.2 allows have been collapsed onto the fields this copy reads. A
// renderbuffer is a single-level, single-layer 2D image, so its shape answers are
// constants rather than a mip walk. `trackedLayout` points AT the owning resource's own
// layout field - both resource maps are node-based, so the pointer survives the further
// lookups the clear materialization below makes.
struct CopyImageVkImage {
Bool isRenderbuffer = false;
VkImage image = VK_NULL_HANDLE;
VkImageLayout* trackedLayout = nullptr;
VkImageAspectFlags aspect = VK_IMAGE_ASPECT_NONE;
Uint32 mipLevels = 1;
VkExtent2D extent = {0, 0};
Uint32 depth = 1;
Uint32 arrayLayers = 1;
};
Bool TryResolveCopyImageSliceMapping(TextureTarget target, const CopyImageVkImage& image, Uint32 mipLevel,
GLint glZ, GLsizei glDepth, CopyImageSliceMapping& outMapping) {
if (glZ < 0 || glDepth <= 0) {
return false;
}
const Uint32 baseSlice = static_cast<Uint32>(glZ);
if (image.isRenderbuffer) {
// A renderbuffer holds one 2D image and nothing else; GL still requires the
// z/depth pair and it can only name that one slice.
outMapping = {};
return baseSlice == 0 && glDepth == 1;
}
switch (target) {
case TextureTarget::Texture1D:
case TextureTarget::Texture2D:
@@ -8897,12 +8919,12 @@ void main() {
case TextureTarget::Texture2DMultisample:
// Not layered at all: GL still requires the z/depth pair, and it can only name the
// one slice these targets have.
outEndpoint = {};
outMapping = {};
return baseSlice == 0 && glDepth == 1;
case TextureTarget::Texture3D:
outEndpoint.slicesAreDepth = true;
outEndpoint.baseSlice = baseSlice;
outEndpoint.availableSlices = std::max(1u, resource.depth >> mipLevel);
outMapping.slicesAreDepth = true;
outMapping.baseSlice = baseSlice;
outMapping.availableSlices = std::max(1u, image.depth >> mipLevel);
return true;
case TextureTarget::Texture2DArray:
case TextureTarget::Texture2DMultisampleArray:
@@ -8911,9 +8933,9 @@ void main() {
// A cube map is an array of six faces here (see TryResolveTextureShapeInfo), and GL
// numbers its faces on the same z axis an array texture numbers its layers, so both
// arrive as a plain layer range.
outEndpoint.slicesAreDepth = false;
outEndpoint.baseSlice = baseSlice;
outEndpoint.availableSlices = resource.arrayLayers;
outMapping.slicesAreDepth = false;
outMapping.baseSlice = baseSlice;
outMapping.availableSlices = image.arrayLayers;
return true;
default:
// GL_TEXTURE_1D_ARRAY carries its layers on the Y axis (srcY/srcHeight), which
@@ -8923,15 +8945,20 @@ void main() {
return false;
}
}
Uint CopyImageEndpointName(const CopyImageEndpoint& endpoint) {
if (endpoint.IsRenderbuffer()) return endpoint.Renderbuffer->GetExternalIndex();
return endpoint.Texture ? endpoint.Texture->GetExternalIndex() : 0u;
}
} // namespace
void VulkanRenderer::CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
void VulkanRenderer::CopyImageSubData(const CopyImageEndpoint& srcEndpoint,
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
const CopyImageEndpoint& dstEndpoint,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
MOBILEGL_ASSERT(srcTexture != nullptr && dstTexture != nullptr,
"CopyImageSubData requires valid source and destination textures.");
MOBILEGL_ASSERT(srcEndpoint.Exists() && dstEndpoint.Exists(),
"CopyImageSubData requires valid source and destination images.");
// The frontend already declines a zero or negative extent, so anything else here is a
// caller MobileGL wrote - but it still reaches vkCmdCopyImage in a release build, and a
// zero extent.depth is as invalid as a zero width.
@@ -8948,9 +8975,9 @@ void main() {
// and an overlap check). Refused outright, and refused for real rather than through an
// assertion the release build drops: recording the pair anyway is a validation error and,
// on a tiler, a copy whose source has already been overwritten.
if (srcTexture.get() == dstTexture.get()) {
MGLOG_E_ONCE("%s: in-place copy on textureId=%d is not supported; declining the copy", __func__,
srcTexture->GetExternalIndex());
if (srcEndpoint.Texture == dstEndpoint.Texture && srcEndpoint.Renderbuffer == dstEndpoint.Renderbuffer) {
MGLOG_E_ONCE("%s: in-place copy on objectId=%u is not supported; declining the copy", __func__,
CopyImageEndpointName(srcEndpoint));
return;
}
@@ -8963,8 +8990,42 @@ void main() {
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
}
auto* srcResource = m_textureManager->SyncTextureAndGetDescriptor(*srcTexture);
auto* dstResource = m_textureManager->SyncTextureAndGetDescriptor(*dstTexture);
// One resolver for both object kinds. The texture arm is the same
// SyncTextureAndGetDescriptor the copy always used; the renderbuffer arm goes through the
// render-pass manager, which is where a renderbuffer's VkImage lives.
const auto resolveImage = [this](const CopyImageEndpoint& endpoint, CopyImageVkImage& out) {
if (endpoint.IsRenderbuffer()) {
auto* resource = m_renderPassManager->GetOrCreateRenderbufferResource(endpoint.Renderbuffer);
if (resource == nullptr) return false;
out.isRenderbuffer = true;
out.image = resource->image;
out.trackedLayout = &resource->layout;
out.aspect = resource->aspect;
out.mipLevels = 1;
out.extent = resource->extent;
out.depth = 1;
out.arrayLayers = 1;
return out.image != VK_NULL_HANDLE;
}
// An endpoint that named nothing is the frontend validator's INVALID_VALUE and never
// reaches here - but the assertion that says so is compiled out of a release build.
if (endpoint.Texture == nullptr) return false;
auto* resource = m_textureManager->SyncTextureAndGetDescriptor(*endpoint.Texture);
if (resource == nullptr) return false;
out.isRenderbuffer = false;
out.image = resource->image;
out.trackedLayout = &resource->layout;
out.aspect = resource->aspect;
out.mipLevels = resource->mipLevels;
out.extent = resource->extent;
out.depth = resource->depth;
out.arrayLayers = resource->arrayLayers;
return true;
};
CopyImageVkImage srcImage{};
CopyImageVkImage dstImage{};
const Bool srcResolved = resolveImage(srcEndpoint, srcImage);
const Bool dstResolved = resolveImage(dstEndpoint, dstImage);
// Real checks, not MOBILEGL_ASSERT: the assertions this replaces compile to nothing in
// a release build, which is where both observed failures happened - a null resource
// dereferenced right below (lavapipe) and a mip level the VkImage does not have handed
@@ -8980,29 +9041,29 @@ void main() {
// The frontend validator (ValidateTextureLevelExists) is what produces the
// GL_INVALID_VALUE the application is actually owed. This guard exists so the next gap
// up there declines a copy instead of taking the process down.
if (srcResource == nullptr || dstResource == nullptr) {
MGLOG_E_ONCE("%s: source or destination texture failed to sync; declining the copy", __func__);
if (!srcResolved || !dstResolved) {
MGLOG_E_ONCE("%s: source or destination image failed to sync; declining the copy", __func__);
return;
}
if (srcLevel < 0 || dstLevel < 0 || static_cast<Uint32>(srcLevel) >= srcResource->mipLevels ||
static_cast<Uint32>(dstLevel) >= dstResource->mipLevels) {
if (srcLevel < 0 || dstLevel < 0 || static_cast<Uint32>(srcLevel) >= srcImage.mipLevels ||
static_cast<Uint32>(dstLevel) >= dstImage.mipLevels) {
MGLOG_E_ONCE("%s: mip level out of range (src %d of %u, dst %d of %u); declining the copy", __func__,
srcLevel, srcResource->mipLevels, dstLevel, dstResource->mipLevels);
srcLevel, srcImage.mipLevels, dstLevel, dstImage.mipLevels);
return;
}
const VkImageAspectFlags copyAspectMask =
srcResource->aspect & dstResource->aspect &
srcImage.aspect & dstImage.aspect &
(VK_IMAGE_ASPECT_COLOR_BIT | VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT);
MOBILEGL_ASSERT(copyAspectMask != 0 &&
(srcResource->aspect & copyAspectMask) == srcResource->aspect &&
(dstResource->aspect & copyAspectMask) == dstResource->aspect,
(srcImage.aspect & copyAspectMask) == srcImage.aspect &&
(dstImage.aspect & copyAspectMask) == dstImage.aspect,
"CopyImageSubData source and destination aspects are incompatible.");
const Uint32 srcMipLevel = static_cast<Uint32>(srcLevel);
const Uint32 dstMipLevel = static_cast<Uint32>(dstLevel);
const Uint32 srcMipWidth = std::max(1u, srcResource->extent.width >> srcMipLevel);
const Uint32 srcMipHeight = std::max(1u, srcResource->extent.height >> srcMipLevel);
const Uint32 dstMipWidth = std::max(1u, dstResource->extent.width >> dstMipLevel);
const Uint32 dstMipHeight = std::max(1u, dstResource->extent.height >> dstMipLevel);
const Uint32 srcMipWidth = std::max(1u, srcImage.extent.width >> srcMipLevel);
const Uint32 srcMipHeight = std::max(1u, srcImage.extent.height >> srcMipLevel);
const Uint32 dstMipWidth = std::max(1u, dstImage.extent.width >> dstMipLevel);
const Uint32 dstMipHeight = std::max(1u, dstImage.extent.height >> dstMipLevel);
// Promoted for the same reason as the level range above, and it is the same bug class:
// a VkImageCopy whose region runs past the image is an out-of-bounds promise to the
// driver, and the frontend does not check the region at all (there is a CTS sibling,
@@ -9025,10 +9086,10 @@ void main() {
// here: every target whose slices this function can address on one of the two Vulkan axes.
// A refusal has to be a real decline, not an assertion - the assertion compiled to nothing
// in a release build and the unsupported shape reached vkCmdCopyImage anyway.
CopyImageEndpoint srcEndpoint;
CopyImageEndpoint dstEndpoint;
if (!TryResolveCopyImageEndpoint(srcTextureTarget, *srcResource, srcMipLevel, srcZ, srcDepth, srcEndpoint) ||
!TryResolveCopyImageEndpoint(dstTextureTarget, *dstResource, dstMipLevel, dstZ, srcDepth, dstEndpoint)) {
CopyImageSliceMapping srcSlices;
CopyImageSliceMapping dstSlices;
if (!TryResolveCopyImageSliceMapping(srcTextureTarget, srcImage, srcMipLevel, srcZ, srcDepth, srcSlices) ||
!TryResolveCopyImageSliceMapping(dstTextureTarget, dstImage, dstMipLevel, dstZ, srcDepth, dstSlices)) {
MGLOG_E_ONCE("%s: unsupported target pair src=%s dst=%s (srcZ=%d dstZ=%d depth=%d); declining the copy",
__func__, MG_Util::ConvertTextureTargetToString(srcTextureTarget).c_str(),
MG_Util::ConvertTextureTargetToString(dstTextureTarget).c_str(), srcZ, dstZ, srcDepth);
@@ -9039,40 +9100,53 @@ void main() {
// shrinks) and a 3D texture by the selected level's depth (which every level halves), so
// both come from the endpoint that resolved them.
const Uint32 copySliceCount = static_cast<Uint32>(srcDepth);
if (srcEndpoint.baseSlice + copySliceCount > srcEndpoint.availableSlices ||
dstEndpoint.baseSlice + copySliceCount > dstEndpoint.availableSlices) {
if (srcSlices.baseSlice + copySliceCount > srcSlices.availableSlices ||
dstSlices.baseSlice + copySliceCount > dstSlices.availableSlices) {
MGLOG_E_ONCE("%s: slice range outside image bounds (srcZ=%d of %u, dstZ=%d of %u, depth=%d); "
"declining the copy",
__func__, srcZ, srcEndpoint.availableSlices, dstZ, dstEndpoint.availableSlices, srcDepth);
__func__, srcZ, srcSlices.availableSlices, dstZ, dstSlices.availableSlices, srcDepth);
return;
}
const Bool clearReady = MaterializePendingClearForTexture(frame.commandBuffer, *srcTexture);
MOBILEGL_ASSERT(clearReady, "%s: failed to materialize pending clear for source textureId=%d",
__func__, srcTexture->GetExternalIndex());
const auto materializeClear = [this, &frame](const CopyImageEndpoint& endpoint) {
if (endpoint.IsRenderbuffer()) {
return MaterializePendingClearForRenderbuffer(frame.commandBuffer, endpoint.Renderbuffer);
}
return MaterializePendingClearForTexture(frame.commandBuffer, *endpoint.Texture);
};
const Bool clearReady = materializeClear(srcEndpoint);
MOBILEGL_ASSERT(clearReady, "%s: failed to materialize pending clear for source objectId=%u",
__func__, CopyImageEndpointName(srcEndpoint));
// A clear still parked on the destination would otherwise materialize AFTER this copy and
// wipe the texels it just wrote.
const Bool dstClearReady = MaterializePendingClearForTexture(frame.commandBuffer, *dstTexture);
MOBILEGL_ASSERT(dstClearReady, "%s: failed to materialize pending clear for destination textureId=%d",
__func__, dstTexture->GetExternalIndex());
const Bool dstClearReady = materializeClear(dstEndpoint);
MOBILEGL_ASSERT(dstClearReady, "%s: failed to materialize pending clear for destination objectId=%u",
__func__, CopyImageEndpointName(dstEndpoint));
const VkImageLayout srcOriginalLayout = srcResource->layout;
const VkImageLayout dstOriginalLayout = dstResource->layout;
const VkImageLayout srcOriginalLayout = *srcImage.trackedLayout;
const VkImageLayout dstOriginalLayout = *dstImage.trackedLayout;
// A layout of UNDEFINED means nothing has ever been written to the image, which on the
// SOURCE side is glTexStorage without an upload: legal GL, and the texels it copies are
// undefined by the same spec sentence that lets the application ask. Both sides therefore
// take the same shape - transition the whole image out of UNDEFINED and settle it on a
// real layout afterwards, since UNDEFINED is not a layout a barrier may transition BACK to.
const auto resolveRestoreLayout = [copyAspectMask](VkImageLayout originalLayout) {
// A renderbuffer settles on its ATTACHMENT layout instead: it is never sampled, and that is
// the layout MaterializePendingClearForRenderbuffer leaves it in.
const auto resolveRestoreLayout = [copyAspectMask](VkImageLayout originalLayout, Bool isRenderbuffer) {
if (originalLayout != VK_IMAGE_LAYOUT_UNDEFINED) {
return originalLayout;
}
return (copyAspectMask & (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) != 0
? VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL
: VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
const Bool depthStencil =
(copyAspectMask & (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) != 0;
if (isRenderbuffer) {
return depthStencil ? VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL
: VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
}
return depthStencil ? VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL
: VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
};
const VkImageLayout srcRestoreLayout = resolveRestoreLayout(srcOriginalLayout);
const VkImageLayout dstRestoreLayout = resolveRestoreLayout(dstOriginalLayout);
const VkImageLayout srcRestoreLayout = resolveRestoreLayout(srcOriginalLayout, srcImage.isRenderbuffer);
const VkImageLayout dstRestoreLayout = resolveRestoreLayout(dstOriginalLayout, dstImage.isRenderbuffer);
VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
VkAccessFlags srcAccessMask = 0;
@@ -9083,15 +9157,15 @@ void main() {
// [baseSlice, baseSlice + depth) the slice mapping above hands the copy.
if (srcOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
Bool srcReady = VkTextureManager::TransitionImageLayout(
frame.commandBuffer, srcResource->image, srcResource->layout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
frame.commandBuffer, srcImage.image, *srcImage.trackedLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT,
srcResource->aspect, 0, srcResource->mipLevels);
srcImage.aspect, 0, srcImage.mipLevels);
MOBILEGL_ASSERT(srcReady, "%s: failed to transition undefined source image", __func__);
srcCopyLayout = srcResource->layout;
srcCopyLayout = *srcImage.trackedLayout;
} else {
Bool srcReady = VkTextureManager::TransitionImageLayout(
frame.commandBuffer, srcResource->image, srcCopyLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
frame.commandBuffer, srcImage.image, srcCopyLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, copyAspectMask, srcMipLevel, 1);
MOBILEGL_ASSERT(srcReady, "%s: failed to transition source image", __func__);
@@ -9103,15 +9177,15 @@ void main() {
VkImageLayout dstCopyLayout = dstOriginalLayout;
if (dstOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
Bool dstReady = VkTextureManager::TransitionImageLayout(
frame.commandBuffer, dstResource->image, dstResource->layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
frame.commandBuffer, dstImage.image, *dstImage.trackedLayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
dstStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
dstAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT,
dstResource->aspect, 0, dstResource->mipLevels);
dstImage.aspect, 0, dstImage.mipLevels);
MOBILEGL_ASSERT(dstReady, "%s: failed to transition undefined destination image", __func__);
dstCopyLayout = dstResource->layout;
dstCopyLayout = *dstImage.trackedLayout;
} else {
Bool dstReady = VkTextureManager::TransitionImageLayout(
frame.commandBuffer, dstResource->image, dstCopyLayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
frame.commandBuffer, dstImage.image, dstCopyLayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
dstStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
dstAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT, copyAspectMask, dstMipLevel, 1);
MOBILEGL_ASSERT(dstReady, "%s: failed to transition destination image", __func__);
@@ -9121,18 +9195,18 @@ void main() {
// on extent.depth as soon as either endpoint IS: a 3D image's subresource is always the
// single layer (0, 1) and its slices are counted by the depth of the copy extent. With two
// non-3D endpoints both layer counts carry it and extent.depth stays 1.
const Bool copyCrossesDepthAxis = srcEndpoint.slicesAreDepth || dstEndpoint.slicesAreDepth;
const Bool copyCrossesDepthAxis = srcSlices.slicesAreDepth || dstSlices.slicesAreDepth;
VkImageCopy copyRegion{};
copyRegion.srcSubresource.aspectMask = copyAspectMask;
copyRegion.srcSubresource.mipLevel = srcMipLevel;
copyRegion.srcSubresource.baseArrayLayer = srcEndpoint.BaseArrayLayer();
copyRegion.srcSubresource.layerCount = srcEndpoint.slicesAreDepth ? 1u : copySliceCount;
copyRegion.srcOffset = {srcX, srcY, srcEndpoint.OffsetZ()};
copyRegion.srcSubresource.baseArrayLayer = srcSlices.BaseArrayLayer();
copyRegion.srcSubresource.layerCount = srcSlices.slicesAreDepth ? 1u : copySliceCount;
copyRegion.srcOffset = {srcX, srcY, srcSlices.OffsetZ()};
copyRegion.dstSubresource.aspectMask = copyAspectMask;
copyRegion.dstSubresource.mipLevel = dstMipLevel;
copyRegion.dstSubresource.baseArrayLayer = dstEndpoint.BaseArrayLayer();
copyRegion.dstSubresource.layerCount = dstEndpoint.slicesAreDepth ? 1u : copySliceCount;
copyRegion.dstOffset = {dstX, dstY, dstEndpoint.OffsetZ()};
copyRegion.dstSubresource.baseArrayLayer = dstSlices.BaseArrayLayer();
copyRegion.dstSubresource.layerCount = dstSlices.slicesAreDepth ? 1u : copySliceCount;
copyRegion.dstOffset = {dstX, dstY, dstSlices.OffsetZ()};
copyRegion.extent = {static_cast<Uint32>(srcWidth), static_cast<Uint32>(srcHeight),
copyCrossesDepthAxis ? copySliceCount : 1u};
MGLOG_D("CopyImageSubData: src(target=%s level=%u layer=%u+%u z=%d) -> dst(target=%s level=%u layer=%u+%u "
@@ -9143,8 +9217,8 @@ void main() {
copyRegion.dstSubresource.baseArrayLayer, copyRegion.dstSubresource.layerCount,
copyRegion.dstOffset.z, srcWidth, srcHeight, copyRegion.extent.depth);
vkCmdCopyImage(frame.commandBuffer,
srcResource->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
dstResource->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
srcImage.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
dstImage.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
1, &copyRegion);
VkPipelineStageFlags srcRestoreStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
@@ -9152,14 +9226,14 @@ void main() {
GetImageTransitionDestinationState(srcRestoreLayout, srcRestoreStageMask, srcRestoreAccessMask);
if (srcOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
Bool srcRestored = VkTextureManager::TransitionImageLayout(
frame.commandBuffer, srcResource->image, srcResource->layout, srcRestoreLayout,
frame.commandBuffer, srcImage.image, *srcImage.trackedLayout, srcRestoreLayout,
VK_PIPELINE_STAGE_TRANSFER_BIT, srcRestoreStageMask,
VK_ACCESS_TRANSFER_READ_BIT, srcRestoreAccessMask,
srcResource->aspect, 0, srcResource->mipLevels);
srcImage.aspect, 0, srcImage.mipLevels);
MOBILEGL_ASSERT(srcRestored, "%s: failed to restore undefined source image layout", __func__);
} else {
Bool srcRestored = VkTextureManager::TransitionImageLayout(
frame.commandBuffer, srcResource->image, srcCopyLayout, srcRestoreLayout,
frame.commandBuffer, srcImage.image, srcCopyLayout, srcRestoreLayout,
VK_PIPELINE_STAGE_TRANSFER_BIT, srcRestoreStageMask,
VK_ACCESS_TRANSFER_READ_BIT, srcRestoreAccessMask, copyAspectMask, srcMipLevel, 1);
MOBILEGL_ASSERT(srcRestored, "%s: failed to restore source image layout", __func__);
@@ -9170,14 +9244,14 @@ void main() {
GetImageTransitionDestinationState(dstRestoreLayout, dstRestoreStageMask, dstRestoreAccessMask);
if (dstOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
Bool dstRestored = VkTextureManager::TransitionImageLayout(
frame.commandBuffer, dstResource->image, dstResource->layout, dstRestoreLayout,
frame.commandBuffer, dstImage.image, *dstImage.trackedLayout, dstRestoreLayout,
VK_PIPELINE_STAGE_TRANSFER_BIT, dstRestoreStageMask,
VK_ACCESS_TRANSFER_WRITE_BIT, dstRestoreAccessMask,
dstResource->aspect, 0, dstResource->mipLevels);
dstImage.aspect, 0, dstImage.mipLevels);
MOBILEGL_ASSERT(dstRestored, "%s: failed to restore undefined destination image layout", __func__);
} else {
Bool dstRestored = VkTextureManager::TransitionImageLayout(
frame.commandBuffer, dstResource->image, dstCopyLayout, dstRestoreLayout,
frame.commandBuffer, dstImage.image, dstCopyLayout, dstRestoreLayout,
VK_PIPELINE_STAGE_TRANSFER_BIT, dstRestoreStageMask,
VK_ACCESS_TRANSFER_WRITE_BIT, dstRestoreAccessMask, copyAspectMask, dstMipLevel, 1);
MOBILEGL_ASSERT(dstRestored, "%s: failed to restore destination image layout", __func__);
@@ -9739,7 +9813,7 @@ void main() {
VkImageAspectFlags imageAspect, Uint32 mipLevel,
Uint32 baseArrayLayer, GLint x, GLint y, GLsizei width,
GLsizei height, GLenum format, GLenum type, void* pixels,
Bool defaultFramebufferOrientation) {
Bool defaultFramebufferOrientation, Uint32 sourceLayerCount) {
const Bool wantDepth = format != GL_STENCIL_INDEX;
const Bool wantStencil = format != GL_DEPTH_COMPONENT;
auto& frame = m_frameContext.GetCurrent();
@@ -9818,6 +9892,10 @@ void main() {
if (!mapped) return;
}
// See the header: a stack of one-row layers and a single multi-row layer copy out to the
// same tightly-packed bytes, so only the region's shape splits the two cases.
const Uint32 copyLayerCount = std::max<Uint32>(sourceLayerCount, 1u);
const Uint32 copyRowCount = copyLayerCount > 1u ? 1u : copyExtent.height;
VkBufferImageCopy regions[2]{};
Uint32 regionCount = 0;
if (wantDepth) {
@@ -9826,9 +9904,9 @@ void main() {
region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
region.imageSubresource.mipLevel = mipLevel;
region.imageSubresource.baseArrayLayer = baseArrayLayer;
region.imageSubresource.layerCount = 1;
region.imageSubresource.layerCount = copyLayerCount;
region.imageOffset = {copyOffset.x, copyOffset.y, 0};
region.imageExtent = {copyExtent.width, copyExtent.height, 1};
region.imageExtent = {copyExtent.width, copyRowCount, 1};
}
if (wantStencil) {
auto& region = regions[regionCount++];
@@ -9836,9 +9914,9 @@ void main() {
region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_STENCIL_BIT;
region.imageSubresource.mipLevel = mipLevel;
region.imageSubresource.baseArrayLayer = baseArrayLayer;
region.imageSubresource.layerCount = 1;
region.imageSubresource.layerCount = copyLayerCount;
region.imageOffset = {copyOffset.x, copyOffset.y, 0};
region.imageExtent = {copyExtent.width, copyExtent.height, 1};
region.imageExtent = {copyExtent.width, copyRowCount, 1};
}
vkCmdCopyImageToBuffer(frame.commandBuffer, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, readback.GetHandle(),
regionCount, regions);
@@ -10073,9 +10151,17 @@ void main() {
? static_cast<Uint32>(textureUploadTarget) -
static_cast<Uint32>(TextureUploadTarget::CubeMapPositiveX)
: 0;
// A 1D array's levelSize.y() is its LAYER count, and those layers are the rows
// GL wants back - but in Vulkan they are array layers of a one-row image, not
// rows of layer 0, so the read has to be told which of the two it is looking at.
const Uint32 sourceLayers =
textureObject->GetTarget() == TextureTarget::Texture1DArray
? static_cast<Uint32>(std::max<Int>(levelSize.y(), 1))
: 1u;
ReadDepthStencilImageToClient(resource->image, resource->format, &resource->layout, resource->aspect,
static_cast<Uint32>(level), arrayLayer, 0, 0, levelSize.x(),
levelSize.y(), format, type, pixels);
levelSize.y(), format, type, pixels,
/*defaultFramebufferOrientation=*/false, sourceLayers);
} else {
MGLOG_E_ONCE("DirectVulkan::GetTexImage skipped: color query of a non-color texture");
}
@@ -10093,12 +10179,19 @@ void main() {
// destination layout (GL 3.3 section 6.1.4).
const auto imageTextureTarget = textureObject->GetTarget();
const Bool is3dImage = imageTextureTarget == TextureTarget::Texture3D;
const Bool isArrayImage = imageTextureTarget == TextureTarget::Texture1DArray ||
const Bool is1dArrayImage = imageTextureTarget == TextureTarget::Texture1DArray;
const Bool isArrayImage = is1dArrayImage ||
imageTextureTarget == TextureTarget::Texture2DArray ||
imageTextureTarget == TextureTarget::TextureCubeMapArray;
const GLsizei depthSlices = is3dImage ? std::max<GLsizei>(texelSize.z(), 1) : 1;
const GLsizei arrayLayers = isArrayImage ? static_cast<GLsizei>(resource->arrayLayers) : 1;
const GLsizei sliceCount = std::max<GLsizei>(depthSlices * arrayLayers, 1);
// A 1D array level comes back as ONE two-dimensional image whose rows are its layers
// (GL 4.6 core 8.11.4), so its layers are already counted by `height` above and must not
// multiply the slice count the way a 2D-array's or a cube-array's do. Vulkan still keeps
// them in arrayLayers on a one-row image, which is what the copy region below says - the
// two describe the same tightly-packed bytes.
const GLsizei sliceCount =
std::max<GLsizei>(depthSlices * (is1dArrayImage ? 1 : arrayLayers), 1);
if (bufSize >= 0) {
const Int dstChannels = GetReadbackChannelCount(format);
if ((type == GL_UNSIGNED_BYTE || type == GL_FLOAT) && dstChannels > 0) {
@@ -10151,7 +10244,8 @@ void main() {
copyRegion.imageSubresource.mipLevel = static_cast<Uint32>(level);
copyRegion.imageSubresource.baseArrayLayer = 0;
copyRegion.imageSubresource.layerCount = static_cast<Uint32>(arrayLayers);
copyRegion.imageExtent = {static_cast<Uint32>(width), static_cast<Uint32>(height),
copyRegion.imageExtent = {static_cast<Uint32>(width),
is1dArrayImage ? 1u : static_cast<Uint32>(height),
static_cast<Uint32>(depthSlices)};
vkCmdCopyImageToBuffer(frame.commandBuffer, resource->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
readback.GetHandle(), 1, &copyRegion);
@@ -23,6 +23,7 @@
#include "VkTimerQueryManager.h"
#include "MG_Util/Math/VectorTypes.h"
#include <Includes.h>
#include <MG_Backend/BackendObject.h>
#include <vk_mem_alloc.h>
#include "../VkIncludes.h"
@@ -197,9 +198,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
GLbitfield mask, GLenum filter);
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset,
GLint x, GLint y, GLsizei width, GLsizei height);
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
void CopyImageSubData(const CopyImageEndpoint& srcEndpoint,
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
const CopyImageEndpoint& dstEndpoint,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
void GenerateMipmap(GLenum target);
@@ -216,10 +217,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// depth/stencil image, which this renderer stores display-side-up: the copy rect then
// has to be mapped out of GL's bottom-origin space and the copied rows re-oriented on
// the way back, exactly as the colour ReadPixels path does.
// `sourceLayerCount` above 1 says the `height` rows the client is owed are stored as that
// many ARRAY LAYERS of a one-row image rather than as rows of one layer - the shape a GL
// 1D array has in Vulkan. The two produce byte-identical tightly-packed readbacks, so
// only the copy region differs; everything after it is written against `height`.
void ReadDepthStencilImageToClient(VkImage image, VkFormat vkFormat, VkImageLayout* trackedLayout,
VkImageAspectFlags imageAspect, Uint32 mipLevel, Uint32 baseArrayLayer,
GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type,
void* pixels, Bool defaultFramebufferOrientation = false);
void* pixels, Bool defaultFramebufferOrientation = false,
Uint32 sourceLayerCount = 1);
// Same-extent depth blit between images of different depth formats: host
// round-trip with a per-texel re-encode (see BlitNamedFramebuffer).
Bool BlitDepthAcrossFormats(FrameContext::FrameData& frame, VkImage srcImage, VkFormat srcFormat,
+2 -1
View File
@@ -44,4 +44,5 @@ add_subdirectory(Program)
add_subdirectory(Buffer)
add_subdirectory(Driver)
add_subdirectory(Container)
add_subdirectory(ShaderCache)
add_subdirectory(ShaderCache)
add_subdirectory(Transpile)
@@ -0,0 +1,20 @@
cmake_minimum_required(VERSION 3.24)
# Deliberately NOT a google-benchmark target: the interesting quantity is a per-stage
# breakdown of one program build, which needs its own clock around sub-steps that share
# set-up, and a plain main() keeps the output a table this can be read straight out of.
add_executable(
TranspileProfile
TranspileProfile.cpp
)
target_include_directories(TranspileProfile PRIVATE
${MGL_ROOT}/include
${MGL_ROOT}/MobileGL
${MGL_ROOT}/3rdparty/SPIRV-Reflect
)
target_link_libraries(
TranspileProfile PRIVATE
${LINK_LIBRARIES}
)
File diff suppressed because it is too large Load Diff
@@ -13,6 +13,7 @@
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
#include <MG_Util/Converters/MGToGL/BufferEnumConverter.h>
#include <MG_Util/Converters/MGToStr/BufferEnumConverter.h>
#include <MG_Util/ShaderTranspiler/Types.h>
namespace MobileGL::MG_Impl::GLImpl::BufferImpl {
Bool ValidateBufferTarget(BufferTarget target) {
@@ -67,6 +68,13 @@ namespace MobileGL::MG_Impl::GLImpl::BufferImpl {
// binding points in GL 3.3 (no ARB_transform_feedback3).
pointCount = std::min<SizeT>(pointCount, 4);
}
if (target == BufferTarget::AtomicCounter) {
// GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS, which is NOT the state layer's array
// size: a counter buffer reaches a shader only as a lowered storage block, so the
// reserved range is the ceiling, and glGetIntegerv advertises the same number.
pointCount = std::min<SizeT>(
pointCount, static_cast<SizeT>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTER_BUFFER_BINDINGS));
}
return pointCount;
}
} // namespace
@@ -326,10 +326,23 @@ namespace MobileGL::MG_Impl::GLImpl {
}
}
// GL 4.6 core 10.9: inside a conditional block whose predicate did not pass, the drawing
// commands, Clear, ClearBuffer* and the compute dispatches are DISCARDED. The gate sits on the
// wrappers that ISSUE the backend call rather than at the top of each entry point, so that
// everything a real driver would still do inside the block - argument validation and the
// errors it raises - happens exactly as it does outside one, and only the command itself is
// dropped. It is deliberately not on the frontend's transform-feedback accounting either:
// that mirrors what the capture stage would have written, and a conditional block around a
// capturing draw has no test coverage in either direction.
static Bool ConditionalRenderDiscardsCommand() {
return MG_State::pGLContext->ConditionalRenderDiscardsCommands();
}
void Clear_Backend(GLbitfield mask) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.Clear(mask);
}
@@ -337,6 +350,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.DrawElements(mode, count, type, indices);
}
@@ -345,6 +359,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElements(mode, count, type, indices, drawcount);
}
@@ -353,6 +368,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsBaseVertex(mode, count, type, indices, drawcount,
basevertex);
}
@@ -361,6 +377,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.DrawArrays(mode, first, count);
}
@@ -368,6 +385,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.MultiDrawArrays(mode, first, count, drawcount);
}
@@ -376,6 +394,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.DrawElementsBaseVertex(mode, count, type, indices, basevertex);
}
@@ -384,6 +403,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirect(mode, type, indirect, drawcount, stride);
}
@@ -391,6 +411,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirect(mode, indirect, drawcount, stride);
}
@@ -399,6 +420,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirectCount(mode, type, indirect, drawcount,
maxdrawcount, stride);
}
@@ -408,6 +430,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirectCount(mode, indirect, drawcount, maxdrawcount,
stride);
}
@@ -417,6 +440,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.DrawRangeElementsBaseVertex(mode, start, end, count, type, indices,
basevertex);
}
@@ -426,6 +450,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.DrawRangeElements(mode, start, end, count, type, indices);
}
@@ -435,6 +460,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseVertexBaseInstance(
mode, count, type, indices, instancecount, basevertex, baseinstance);
}
@@ -444,6 +470,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseVertex(mode, count, type, indices, instancecount,
basevertex);
}
@@ -453,6 +480,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseInstance(mode, count, type, indices,
instancecount, baseinstance);
}
@@ -462,6 +490,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstanced(mode, count, type, indices, instancecount);
}
@@ -469,6 +498,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.DrawElementsIndirect(mode, type, indirect);
}
void DrawArraysInstancedBaseInstance_Backend(GLenum mode, GLint first, GLsizei count, GLsizei instancecount,
@@ -476,6 +506,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.DrawArraysInstancedBaseInstance(mode, first, count, instancecount,
baseinstance);
}
@@ -484,6 +515,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.DrawArraysInstanced(mode, first, count, instancecount);
}
@@ -491,6 +523,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.DrawArraysIndirect(mode, indirect);
}
@@ -519,6 +552,9 @@ namespace MobileGL::MG_Impl::GLImpl {
return;
}
}
// GL 4.3 added both dispatches to the conditional-render set (GL 4.6 core 10.9), which is
// exactly what KHR-GL43.compute_shader.conditional-dispatching checks.
if (ConditionalRenderDiscardsCommand()) return;
dispatchCompute(numGroupsX, numGroupsY, numGroupsZ);
}
@@ -570,6 +606,7 @@ namespace MobileGL::MG_Impl::GLImpl {
return;
}
if (!ValidateCurrentProgramForCompute(__func__)) return;
if (ConditionalRenderDiscardsCommand()) return;
dispatchComputeIndirect(indirect);
}
@@ -725,8 +725,8 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, LoadName, GLuint name) DECLARE_GL_FUNCTION_S
DECLARE_GL_FUNCTION_STUB_HEAD(void, PushName, GLuint name) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PushName, name)
DECLARE_GL_FUNCTION_STUB_HEAD(void, PopName) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PopName)
DECLARE_GL_FUNCTION_HEAD(void, ClampColor, GLenum target, GLenum clamp) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClampColor, target, clamp)
DECLARE_GL_FUNCTION_STUB_HEAD(void, BeginConditionalRender, GLuint id, GLenum mode) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BeginConditionalRender, id, mode)
DECLARE_GL_FUNCTION_STUB_HEAD(void, EndConditionalRender, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, EndConditionalRender)
DECLARE_GL_FUNCTION_HEAD(void, BeginConditionalRender, GLuint id, GLenum mode) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BeginConditionalRender, id, mode)
DECLARE_GL_FUNCTION_HEAD(void, EndConditionalRender) DECLARE_GL_FUNCTION_END_NO_RETURN(void, EndConditionalRender)
DECLARE_GL_FUNCTION_HEAD(void, VertexAttribI1i, GLuint index, GLint x) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribI1i, index, x)
DECLARE_GL_FUNCTION_HEAD(void, VertexAttribI2i, GLuint index, GLint x, GLint y) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribI2i, index, x, y)
DECLARE_GL_FUNCTION_HEAD(void, VertexAttribI3i, GLuint index, GLint x, GLint y, GLint z) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribI3i, index, x, y, z)
@@ -982,7 +982,7 @@ DECLARE_GL_FUNCTION_HEAD(void, GetDoublei_v, GLenum target, GLuint index, GLdoub
DECLARE_GL_FUNCTION_HEAD(void, DrawArraysInstancedBaseInstance, GLenum mode, GLint first, GLsizei count, GLsizei instancecount, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawArraysInstancedBaseInstance, mode, first, count, instancecount, baseinstance)
DECLARE_GL_FUNCTION_HEAD(void, DrawElementsInstancedBaseInstance, GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawElementsInstancedBaseInstance, mode, count, type, indices, instancecount, baseinstance)
DECLARE_GL_FUNCTION_HEAD(void, DrawElementsInstancedBaseVertexBaseInstance, GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLint basevertex, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawElementsInstancedBaseVertexBaseInstance, mode, count, type, indices, instancecount, basevertex, baseinstance)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetActiveAtomicCounterBufferiv, GLuint program, GLuint bufferIndex, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetActiveAtomicCounterBufferiv, program, bufferIndex, pname, params)
DECLARE_GL_FUNCTION_HEAD(void, GetActiveAtomicCounterBufferiv, GLuint program, GLuint bufferIndex, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetActiveAtomicCounterBufferiv, program, bufferIndex, pname, params)
DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedbackInstanced, GLenum mode, GLuint id, GLsizei instancecount) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedbackInstanced, mode, id, instancecount)
DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedbackStreamInstanced, GLenum mode, GLuint id, GLuint stream, GLsizei instancecount) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedbackStreamInstanced, mode, id, stream, instancecount)
DECLARE_GL_FUNCTION_HEAD(void, ClearBufferData, GLenum target, GLenum internalformat, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearBufferData, target, internalformat, format, type, data)
@@ -2613,18 +2613,26 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void ClearBufferfi_Backend(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) {
// GL 4.6 core 10.9 makes ClearBuffer* conditional alongside the drawing commands.
if (MG_State::pGLContext->ConditionalRenderDiscardsCommands()) return;
MG_Backend::gBackendFunctionsTable.GL.ClearBufferfi(buffer, drawbuffer, depth, stencil);
}
void ClearBufferfv_Backend(GLenum buffer, GLint drawbuffer, const GLfloat* value) {
// GL 4.6 core 10.9 makes ClearBuffer* conditional alongside the drawing commands.
if (MG_State::pGLContext->ConditionalRenderDiscardsCommands()) return;
MG_Backend::gBackendFunctionsTable.GL.ClearBufferfv(buffer, drawbuffer, value);
}
void ClearBufferuiv_Backend(GLenum buffer, GLint drawbuffer, const GLuint* value) {
// GL 4.6 core 10.9 makes ClearBuffer* conditional alongside the drawing commands.
if (MG_State::pGLContext->ConditionalRenderDiscardsCommands()) return;
MG_Backend::gBackendFunctionsTable.GL.ClearBufferuiv(buffer, drawbuffer, value);
}
void ClearBufferiv_Backend(GLenum buffer, GLint drawbuffer, const GLint* value) {
// GL 4.6 core 10.9 makes ClearBuffer* conditional alongside the drawing commands.
if (MG_State::pGLContext->ConditionalRenderDiscardsCommands()) return;
MG_Backend::gBackendFunctionsTable.GL.ClearBufferiv(buffer, drawbuffer, value);
}
+99 -23
View File
@@ -25,6 +25,7 @@
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
#include <MG_Util/Texture/TextureFormatProcessor.h>
#include <MG_Util/Async/ShaderCompilePool.h>
#include <MG_Util/ShaderTranspiler/Types.h>
#include <MG_Backend/BackendObjects.h>
namespace MobileGL::MG_Impl::GLImpl {
@@ -46,13 +47,29 @@ namespace MobileGL::MG_Impl::GLImpl {
}
}
constexpr GLint kFrontendMaxComputeUniformComponents = 1024;
constexpr GLint kFrontendMaxComputeAtomicCounters = 8;
constexpr GLint kFrontendMaxComputeAtomicCounterBuffers = 8;
// Shared with the glslang resource table for the same reason as the atomic-counter
// limits below: gl_MaxComputeUniformComponents expands from BuildTBuiltInResource.
constexpr GLint kFrontendMaxComputeUniformComponents =
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_COMPUTE_UNIFORM_COMPONENTS);
// Every atomic-counter limit is shared with the glslang resource table
// (BuildTBuiltInResource) through MG_Util/ShaderTranspiler/Types.h: GL 4.6 requires
// glGetIntegerv and the gl_MaxAtomicCounter* built-in constants to agree, and the two
// used to be independent tables that disagreed on both the binding count and the buffer
// size. Never move one of these without the other.
constexpr GLint kFrontendMaxComputeAtomicCounters =
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTERS_PER_STAGE);
constexpr GLint kFrontendMaxComputeAtomicCounterBuffers =
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE);
constexpr GLint kFrontendMaxComputeSharedMemorySize = 32768;
constexpr GLint kFrontendMaxComputeWorkGroupInvocations = 1024;
constexpr GLint kFrontendMaxCombinedAtomicCounters = 8;
constexpr GLint kFrontendMaxFragmentAtomicCounters = 8;
constexpr GLint kFrontendMaxCombinedAtomicCounters =
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTERS_PER_STAGE);
constexpr GLint kFrontendMaxCombinedAtomicCounterBuffers =
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE);
constexpr GLint kFrontendMaxFragmentAtomicCounters =
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTERS_PER_STAGE);
constexpr GLint kFrontendMaxFragmentAtomicCounterBuffers =
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE);
constexpr GLint kFrontendMaxGeometryAtomicCounters = 0;
constexpr GLint kFrontendMaxTessControlAtomicCounters = 0;
constexpr GLint kFrontendMaxTessEvaluationAtomicCounters = 0;
@@ -66,10 +83,11 @@ namespace MobileGL::MG_Impl::GLImpl {
constexpr GLint kFrontendMaxTessControlAtomicCounterBuffers = 0;
constexpr GLint kFrontendMaxTessEvaluationAtomicCounterBuffers = 0;
constexpr GLint kFrontendMaxVertexAtomicCounterBuffers = 0;
// One atomic counter is a uint, and a buffer never has to hold more counters than the
// combined limit the frontend advertises. GL 4.6 table 23.63 floors this at 32 bytes.
// GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE: the byte offset ceiling a counter may be declared
// at. The matching binding count is applied in GetIndexedBufferQueryPointCount, so that
// the getter, the indexed queries and glBindBufferBase all share one ceiling.
constexpr GLint kFrontendMaxAtomicCounterBufferSize =
kFrontendMaxCombinedAtomicCounters * static_cast<GLint>(sizeof(GLuint));
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTER_BUFFER_SIZE);
// KHR_debug minima (GL 4.6 table 23.66); the debug entry points are stubs, but the
// limits they advertise still have to be legal.
constexpr GLint kFrontendMaxDebugGroupStackDepth = 64;
@@ -103,12 +121,16 @@ namespace MobileGL::MG_Impl::GLImpl {
constexpr GLint kFrontendSubpixelBits = 4;
constexpr GLint kFrontendMaxSamples = 4;
// The floors under GL_MAX_COMPUTE_WORK_GROUP_COUNT / _SIZE. Shared with the compile
// pipeline (CaptureCompileEnv floors the same driver answers at them, and
// BuildTBuiltInResource expands gl_MaxComputeWorkGroup* from the result), because a
// shader is allowed to compare the built-in constant against this query.
constexpr GLint GetMinComputeWorkGroupCount(GLuint index) {
return index < 3 ? 65535 : 0;
return index < 3 ? static_cast<GLint>(MG_Util::ShaderTranspiler::MIN_COMPUTE_WORK_GROUP_COUNT[index]) : 0;
}
constexpr GLint GetMinComputeWorkGroupSize(GLuint index) {
return index < 2 ? 1024 : (index == 2 ? 64 : 0);
return index < 3 ? static_cast<GLint>(MG_Util::ShaderTranspiler::MIN_COMPUTE_WORK_GROUP_SIZE[index]) : 0;
}
GLint GetMaxCombinedUniformComponents(GLint maxDefaultUniformComponents, GLint maxUniformBlocks,
@@ -186,6 +208,16 @@ namespace MobileGL::MG_Impl::GLImpl {
MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxShaderStorageBufferBindings;
return std::min(frontendCount, static_cast<SizeT>(std::max(backendCount, 0)));
}
if (bufferTarget == BufferTarget::AtomicCounter) {
// The counter family's binding count is NOT the state layer's array size: a
// counter buffer only reaches a shader as a lowered storage block, so what an
// implementation can serve is the reserved range, and that number is also what
// glslang compiles a layout(binding = N) atomic_uint against. Clamped here so
// GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS, the indexed getters' index check and
// glBindBufferBase's all report the same ceiling.
return std::min(frontendCount,
static_cast<SizeT>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTER_BUFFER_BINDINGS));
}
return frontendCount;
}
@@ -213,6 +245,23 @@ namespace MobileGL::MG_Impl::GLImpl {
return ClampBlockCountToBindingPoints(blockCount, BufferTarget::ShaderStorage);
}
// The per-stage GL_MAX_*_SHADER_STORAGE_BLOCKS answers. Backend-derived, and NOT a
// constant to be "restored" - these used to return a flat 16 for vertex, geometry and
// both tessellation stages, which is wrong on any host that does not serve storage
// blocks in those stages. Zero is a legal answer: GL 4.6 table 23.64 and ES 3.2 table
// 21.44 both set the minimum at 0 for every graphics stage except fragment, which is
// why the conformance suite gates each such test on the query instead of assuming it.
// ARM's GLES driver reports 0 for all four (a Mali-G925 does), and advertising 16 there
// bought nothing: the program still failed to link inside the backend, the frontend
// still reported LINK_STATUS as true, and every draw with it silently rendered nothing.
GLint StageStorageBlockCount(Int MG_Backend::DynamicBackendParameters::*stageLimit) {
static const MG_Backend::DynamicBackendParameters kBackendlessDefaults{};
const MG_Backend::DynamicBackendParameters& parameters =
MG_Backend::pActiveBackendObject ? MG_Backend::pActiveBackendObject->GetDynamicParameters()
: kBackendlessDefaults;
return ClampStorageBlockCount(static_cast<GLint>(parameters.*stageLimit));
}
bool TryDecodeDrawBufferQuery(GLenum pname, SizeT& drawBufferIndex) {
if (pname == GL_DRAW_BUFFER) {
drawBufferIndex = 0;
@@ -1532,6 +1581,9 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_MAX_COMBINED_ATOMIC_COUNTERS:
*params = kFrontendMaxCombinedAtomicCounters;
return;
case GL_MAX_COMBINED_ATOMIC_COUNTER_BUFFERS:
*params = kFrontendMaxCombinedAtomicCounterBuffers;
return;
case GL_MAX_COMBINED_UNIFORM_BLOCKS:
*params = ClampUniformBlockCount(kFrontendMaxCombinedUniformBlocks);
return;
@@ -1547,8 +1599,11 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_MAX_FRAGMENT_ATOMIC_COUNTERS:
*params = kFrontendMaxFragmentAtomicCounters;
return;
case GL_MAX_FRAGMENT_ATOMIC_COUNTER_BUFFERS:
*params = kFrontendMaxFragmentAtomicCounterBuffers;
return;
case GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS:
*params = ClampStorageBlockCount(16); // TODO
*params = StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxFragmentShaderStorageBlocks);
return;
case GL_MAX_FRAGMENT_INPUT_COMPONENTS:
*params = kFrontendMaxFragmentInputComponents;
@@ -1574,7 +1629,7 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = kFrontendMaxGeometryAtomicCounterBuffers;
return;
case GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS:
*params = ClampStorageBlockCount(16); // TODO
*params = StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxGeometryShaderStorageBlocks);
return;
case GL_MAX_GEOMETRY_INPUT_COMPONENTS:
*params = kFrontendMaxGeometryInputComponents;
@@ -1645,16 +1700,18 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = 0;
return;
case GL_MAX_TESS_CONTROL_SHADER_STORAGE_BLOCKS:
*params = ClampStorageBlockCount(16); // TODO
*params = StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxTessControlShaderStorageBlocks);
return;
case GL_MAX_TESS_EVALUATION_SHADER_STORAGE_BLOCKS:
*params = ClampStorageBlockCount(16); // TODO
*params =
StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxTessEvaluationShaderStorageBlocks);
return;
case GL_MAX_TEXTURE_LOD_BIAS:
*params = 15; // TODO
return;
case GL_MAX_UNIFORM_LOCATIONS:
*params = 1024 * 4; // TODO
// The same constant the link's location allocator enforces - see ProgramObject.
*params = MG_State::GLState::ProgramObject::MAX_UNIFORM_LOCATIONS;
return;
case GL_MAX_VARYING_COMPONENTS:
*params = kFrontendMaxVaryingComponents;
@@ -1674,7 +1731,7 @@ namespace MobileGL::MG_Impl::GLImpl {
: MG_Backend::DynamicBackendParameters{}.MaxVertexImageUniforms;
return;
case GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS:
*params = ClampStorageBlockCount(16); // TODO
*params = StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxVertexShaderStorageBlocks);
return;
case GL_MAX_VERTEX_UNIFORM_COMPONENTS:
*params = kFrontendMaxVertexUniformComponents;
@@ -1984,6 +2041,24 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_UNIFORM_BUFFER_START:
RecordIndexedOnlyGetterError(__func__, pname);
return;
// glBindBufferBase/Range set the GENERIC binding point too (GL 4.6 core 6.1.1), and this
// is the one indexed-buffer family whose non-indexed query was never answered - so it
// fell through to INVALID_ENUM and left the caller's variable holding whatever was in its
// stack slot. _START/_SIZE stay indexed-only, exactly like their uniform-buffer siblings.
case GL_ATOMIC_COUNTER_BUFFER_BINDING:
if (const auto& obj =
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::AtomicCounter).GetBoundObject()) {
*params = static_cast<GLint>(obj->GetExternalIndex());
} else {
*params = 0;
}
return;
case GL_ATOMIC_COUNTER_BUFFER_START:
RecordIndexedOnlyGetterError(__func__, pname);
return;
case GL_ATOMIC_COUNTER_BUFFER_SIZE:
RecordIndexedOnlyGetterError(__func__, pname);
return;
case GL_UNPACK_ALIGNMENT:
*params = MG_State::pGLContext->GetPixelStoreParam(PixelStoreParam::UnpackAlignment);
return;
@@ -2219,18 +2294,19 @@ namespace MobileGL::MG_Impl::GLImpl {
static_cast<Uint64>(INT32_MAX)));
break;
case GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS:
// NOT the frontend's binding-point array size: GetIndexedBufferQueryPointCount
// clamps this family to the range a lowered counter block can actually be served
// from, which is the same number glslang compiles a layout(binding = N) atomic_uint
// against and the same one glBindBufferBase validates an index against.
*params = static_cast<GLint>(GetIndexedBufferQueryPointCount(BufferTarget::AtomicCounter));
break;
case GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE:
// The conformance suite splits this evenly across every advertised binding point and
// binds all of them in one glBindBuffersRange
// (KHR-GL44.multi_bind.functional_bind_buffers_range), so the pair has to divide:
// 32 bytes over 36 binding points is a zero-sized range, which BindBufferRange
// rejects with INVALID_VALUE before it binds anything. Floor the advertised size at
// one counter per binding point.
*params = std::max<GLint>(
kFrontendMaxAtomicCounterBufferSize,
static_cast<GLint>(GetIndexedBufferQueryPointCount(BufferTarget::AtomicCounter) * sizeof(GLuint)));
// (KHR-GL44.multi_bind.functional_bind_buffers_range), so the pair has to divide -
// a zero-sized range is INVALID_VALUE before BindBufferRange binds anything. The
// shared constant is 16384 over 8 binding points, which divides.
*params = kFrontendMaxAtomicCounterBufferSize;
break;
case GL_MAX_TEXTURE_BUFFER_SIZE:
*params = dynamicParameters.MaxTextureBufferSize;
+74 -1
View File
@@ -645,7 +645,13 @@ namespace MobileGL::MG_Impl::GLImpl {
break;
}
case GL_ACTIVE_ATOMIC_COUNTER_BUFFERS:
*params = programObject->GetActiveAtomicCounterCount();
// Counter BUFFERS, not counters, and glslang's own getNumAtomicCounters() answers
// neither: the relaxed parse has already turned every atomic_uint into a plain uint
// member of a synthesized storage block by the time it builds its reflection, so it
// reports zero. The interface-query model recovers the buffers from those blocks and
// is what glGetProgramInterfaceiv(GL_ATOMIC_COUNTER_BUFFER, GL_ACTIVE_RESOURCES)
// already answers - the two queries are required to agree.
*params = ProgramInterface::GetActiveResourceCount(*programObject, GL_ATOMIC_COUNTER_BUFFER);
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
break;
case GL_ACTIVE_ATTRIBUTES:
@@ -2838,6 +2844,73 @@ namespace MobileGL::MG_Impl::GLImpl {
return ProgramInterface::GetResourceLocationIndex(*programObject, programInterface, name);
}
// GL 4.6 §7.7. Every property this reports is one the GL_ATOMIC_COUNTER_BUFFER interface
// already carries, so this is a rename of glGetProgramResourceiv's props onto the older
// entry point's - and the two are required to agree, which is only true while both read the
// same model. It was a silent stub: it wrote nothing, raised nothing, and left every probe
// reading its own uninitialised output.
static Bool TryMapActiveAtomicCounterBufferProp(GLenum pname, GLenum& outProp) {
switch (pname) {
case GL_ATOMIC_COUNTER_BUFFER_BINDING:
outProp = GL_BUFFER_BINDING;
return true;
case GL_ATOMIC_COUNTER_BUFFER_DATA_SIZE:
outProp = GL_BUFFER_DATA_SIZE;
return true;
case GL_ATOMIC_COUNTER_BUFFER_ACTIVE_ATOMIC_COUNTERS:
outProp = GL_NUM_ACTIVE_VARIABLES;
return true;
case GL_ATOMIC_COUNTER_BUFFER_ACTIVE_ATOMIC_COUNTER_INDICES:
outProp = GL_ACTIVE_VARIABLES;
return true;
case GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_VERTEX_SHADER:
outProp = GL_REFERENCED_BY_VERTEX_SHADER;
return true;
case GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_TESS_CONTROL_SHADER:
outProp = GL_REFERENCED_BY_TESS_CONTROL_SHADER;
return true;
case GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_TESS_EVALUATION_SHADER:
outProp = GL_REFERENCED_BY_TESS_EVALUATION_SHADER;
return true;
case GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_GEOMETRY_SHADER:
outProp = GL_REFERENCED_BY_GEOMETRY_SHADER;
return true;
case GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_FRAGMENT_SHADER:
outProp = GL_REFERENCED_BY_FRAGMENT_SHADER;
return true;
case GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_COMPUTE_SHADER:
outProp = GL_REFERENCED_BY_COMPUTE_SHADER;
return true;
default:
return false;
}
}
void GetActiveAtomicCounterBufferiv(GLuint program, GLuint bufferIndex, GLenum pname, GLint* params) {
auto& programObject = TryToGetProgramForInterfaceQuery(program, __func__);
if (!programObject) return;
GLenum prop = GL_NONE;
if (!TryMapActiveAtomicCounterBufferProp(pname, prop)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"pname is not an active atomic counter buffer property."));
return;
}
Vector<GLint> values;
if (!ProgramInterface::GetResourceProp(*programObject, GL_ATOMIC_COUNTER_BUFFER, bufferIndex, prop, values)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"bufferIndex is not an active atomic counter buffer index."));
return;
}
if (params == nullptr) return;
// GL_ATOMIC_COUNTER_BUFFER_ACTIVE_ATOMIC_COUNTER_INDICES is the only multi-value property
// here, and the caller sized its array from _ACTIVE_ATOMIC_COUNTERS.
for (SizeT i = 0; i < values.size(); ++i) params[i] = values[i];
}
// GL 4.6 §7.6.2: <storageBlockIndex> is an active shader storage block index of <program>
// - that is, exactly what glGetProgramResourceIndex(GL_SHADER_STORAGE_BLOCK) returned.
// Since wave 2 that index is the interface-query layer's, so this is where the one index
@@ -140,6 +140,7 @@ namespace MobileGL::MG_Impl::GLImpl {
const GLenum* props, GLsizei bufSize, GLsizei* length, GLint* params);
GLint GetProgramResourceLocation(GLuint program, GLenum programInterface, const GLchar* name);
GLint GetProgramResourceLocationIndex(GLuint program, GLenum programInterface, const GLchar* name);
void GetActiveAtomicCounterBufferiv(GLuint program, GLuint bufferIndex, GLenum pname, GLint* params);
void ShaderStorageBlockBinding(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding);
void Uniform1d(GLint location, GLdouble v0);
void Uniform1dv(GLint location, GLsizei count, const GLdouble* value);
@@ -19,7 +19,7 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
// "<getAtomicCounterBlockName()>_<binding>" (ParseContextBase.cpp), one per GL
// atomic-counter binding point. That block IS the GL_ATOMIC_COUNTER_BUFFER resource
// and its trailing number IS GL_BUFFER_BINDING; its members stay GL_UNIFORMs.
constexpr const char* kAtomicCounterBlockPrefix = "gl_AtomicCounterBlock";
constexpr const char* kAtomicCounterBlockPrefix = MG_Util::ShaderTranspiler::ATOMIC_COUNTER_BLOCK_PREFIX;
enum class BlockKind {
Uniform, // a real GL uniform block
@@ -565,6 +565,75 @@ namespace MobileGL::MG_Impl::GLImpl {
queryObject->ended = true;
}
void BeginConditionalRender(GLuint id, GLenum mode) {
// GL 4.6 core 10.9's eight modes. The _INVERTED half flips the sense of the predicate;
// the BY_REGION half only narrows WHERE an implementation is permitted to discard, so
// treating it as its whole-framebuffer sibling is what an implementation without region
// granularity does. The _NO_WAIT half is a permission to render rather than stall, not an
// obligation - see the resolve below.
Bool inverted = false;
switch (mode) {
case GL_QUERY_WAIT:
case GL_QUERY_NO_WAIT:
case GL_QUERY_BY_REGION_WAIT:
case GL_QUERY_BY_REGION_NO_WAIT:
inverted = false;
break;
case GL_QUERY_WAIT_INVERTED:
case GL_QUERY_NO_WAIT_INVERTED:
case GL_QUERY_BY_REGION_WAIT_INVERTED:
case GL_QUERY_BY_REGION_NO_WAIT_INVERTED:
inverted = true;
break;
default:
RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "mode is not a conditional render mode.");
return;
}
if (MG_State::pGLContext->IsConditionalRenderActive()) {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "Conditional rendering is already active.");
return;
}
{
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
const auto* queryObject = FindQueryObjectLocked(id);
// A generated NAME is not yet a query object; it becomes one at its first use with a
// target (the same rule glIsQuery answers by).
if (!queryObject || (!queryObject->created && queryObject->target == 0)) {
RecordQueryError(ErrorCode::InvalidValue, __FUNCTION__, "id is not the name of a query object.");
return;
}
if (queryObject->active) {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "The query object is still active.");
return;
}
if (queryObject->target != GL_SAMPLES_PASSED && queryObject->target != GL_ANY_SAMPLES_PASSED &&
queryObject->target != GL_ANY_SAMPLES_PASSED_CONSERVATIVE) {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__,
"Conditional rendering requires an occlusion query object.");
return;
}
}
// Resolved ONCE, here, and by WAITING even for the _NO_WAIT modes: the spec lets those
// render instead of stalling, so always waiting is conforming and is the only choice that
// gives the whole block one deterministic verdict. Reading it per command instead would
// let a result that lands mid-block change the answer half way through.
Uint64 samplesPassed = 0;
if (!GetQueryObjectValue(id, GL_QUERY_RESULT, __FUNCTION__, samplesPassed)) return;
const Bool passed = samplesPassed != 0;
MG_State::pGLContext->BeginConditionalRender(id, mode, inverted ? passed : !passed);
}
void EndConditionalRender() {
if (!MG_State::pGLContext->IsConditionalRenderActive()) {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "Conditional rendering is not active.");
return;
}
MG_State::pGLContext->EndConditionalRender();
}
void GetQueryiv(GLenum target, GLenum pname, GLint* params) {
if (!params) {
return;
+5
View File
@@ -29,6 +29,11 @@ namespace MobileGL::MG_Impl::GLImpl {
void GetQueryBufferObjecti64v(GLuint id, GLuint buffer, GLenum pname, GLintptr offset);
void GetQueryBufferObjectui64v(GLuint id, GLuint buffer, GLenum pname, GLintptr offset);
void QueryCounter(GLuint id, GLenum target);
// Conditional rendering (GL 4.6 core 10.9). Implemented here rather than beside the drawing
// entry points because the predicate is a QUERY OBJECT's result, and the object registry -
// with the lock that guards it - lives in this file.
void BeginConditionalRender(GLuint id, GLenum mode);
void EndConditionalRender();
// Destroys every still-registered query object exactly as DeleteQueries would.
// GL requires queries to die with their context; called only from full library
// teardown (DestroyImpl), where no context survives on any thread, so the
+395 -99
View File
@@ -661,21 +661,42 @@ namespace MobileGL::MG_Impl::GLImpl {
"Compressed texture formats are not supported."));
}
// glGetTexLevelParameter{i,f}v answers WIDTH/HEIGHT/DEPTH out of the mipmap chain. The only
// other storage type the state layer knows is GL_TEXTURE_BUFFER (TextureStorageType is
// {Mipmap, Buffer}), whose level geometry this stack does not track yet. Report that instead
// of throwing: THROW_UNIMPL_EXCEPTION unwinds a C++ exception through the C GL ABI and takes
// the process down, which is never an acceptable answer to a query - see the same reasoning
// above for the compressed-format path.
// GL_TEXTURE_WIDTH of a buffer texture: how many texels of the texture's internal format fit
// in the buffer range it addresses, CLAMPED to GL_MAX_TEXTURE_BUFFER_SIZE. Attaching a larger
// buffer is legal (GL 4.6 core 8.9) - the texture simply addresses the first
// MAX_TEXTURE_BUFFER_SIZE texels of it, and that clamped count is what WIDTH reports.
//
// GL_TEXTURE_BUFFER_SIZE is deliberately NOT clamped the same way: it reports the range in
// basic machine units exactly as glTexBuffer/glTexBufferRange were given it. Swapping the two
// fails KHR-GL43.texture_buffer.texture_buffer_max_size in the opposite direction.
GLint GetBufferTextureTexelWidth(const MG_State::GLState::ITextureObject* textureObject) {
const SizeT texelByteSize = MG_Util::GetSizedInternalFormatSizeInBytes(textureObject->GetFormat());
// A format with no known footprint has no texel count to report; answering 0 beats
// dividing by it.
if (texelByteSize == 0) return 0;
const auto* bufferTextureObject =
static_cast<const MG_State::GLState::TextureObjectBuffer*>(textureObject);
const SizeT texelCount = bufferTextureObject->GetBufferRangeSizeInBytes() / texelByteSize;
const SizeT maxTexelCount = static_cast<SizeT>(
std::max(0, MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxTextureBufferSize));
return static_cast<GLint>(std::min(texelCount, maxTexelCount));
}
// glGetTexLevelParameter{i,f}v answers WIDTH/HEIGHT/DEPTH out of the mipmap chain, and (since
// the buffer-texture arms above) out of the attached buffer range for GL_TEXTURE_BUFFER. This
// is what is left: a storage class with no level geometry at all. Report it instead of
// throwing - THROW_UNIMPL_EXCEPTION unwinds a C++ exception through the C GL ABI and takes the
// process down, which is never an acceptable answer to a query - see the same reasoning above
// for the compressed-format path.
void RecordUnsupportedLevelQueryStorage(const char* caller, GLenum pname) {
MGLOG_W_ONCE("%s: glGetTexLevelParameter(pname=%s) is not implemented for texture-buffer "
"storage; recording GL_INVALID_OPERATION instead of terminating",
MGLOG_W_ONCE("%s: glGetTexLevelParameter(pname=%s) is not implemented for this texture's "
"storage class; recording GL_INVALID_OPERATION instead of terminating",
caller, MG_Util::ConvertGLEnumToString(pname).c_str());
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", caller,
"Level queries are not supported for texture-buffer storage."));
"Level queries are not supported for this texture's storage class."));
}
} // namespace
@@ -691,6 +712,34 @@ namespace MobileGL::MG_Impl::GLImpl {
return textureObject;
}
// Whether a raw internalformat enum names a compressed format - the question GL asks whenever an
// entry point is forbidden on a compressed image: glTexStorage3D on TEXTURE_3D (no
// block-compressed format is defined for a three-dimensional image, so it is INVALID_OPERATION
// rather than the INVALID_ENUM an unknown sized format gets - GL 4.6 core 8.19 / Khronos bug
// 11239, KHR-GLxx.texture_storage.compressed_data) and the clear-texture pair (8.19 again).
// Written against the enum ranges rather than a name list because the families are contiguous
// and MobileGL's own internal-format enum drops the ones it cannot carry, which would make this
// check silently narrower than the API surface.
static Bool IsCompressedGLInternalFormat(GLenum internalformat) {
switch (internalformat) {
case 0x8225: // GL_COMPRESSED_RED
case 0x8226: // GL_COMPRESSED_RG
case 0x84ED: // GL_COMPRESSED_RGB
case 0x84EE: // GL_COMPRESSED_RGBA
case 0x8C48: // GL_COMPRESSED_SRGB
case 0x8C49: // GL_COMPRESSED_SRGB_ALPHA
return true;
default:
break;
}
return (internalformat >= 0x83F0 && internalformat <= 0x83F3) || // S3TC / DXT
(internalformat >= 0x8DBB && internalformat <= 0x8DBE) || // RGTC
(internalformat >= 0x8E8C && internalformat <= 0x8E8F) || // BPTC
(internalformat >= 0x9270 && internalformat <= 0x9279) || // ETC2 / EAC
(internalformat >= 0x93B0 && internalformat <= 0x93BD) || // ASTC LDR
(internalformat >= 0x93D0 && internalformat <= 0x93DD); // ASTC sRGB
}
namespace {
void RecordClearTextureError(const char* caller, ErrorCode code, const String& message) {
MG_State::pGLContext->RecordError(
@@ -726,6 +775,21 @@ namespace MobileGL::MG_Impl::GLImpl {
std::format("Texture level {} is not defined.", level));
return nullptr;
}
// GL 4.6 core 8.19: a compressed internal format is INVALID_OPERATION for both clear
// entry points. Two tags to ask, because they answer different questions: the stored
// one covers a level glCompressedTexImage* or a SPECIFIC compressed internalformat
// defined, the requested one covers the six generic GL_COMPRESSED_* enums that MobileGL
// deliberately backs with uncompressed storage (see MipmapStorage) and that would
// otherwise look like an ordinary RGBA8 image by the time the clear runs.
const auto& uploadTargets = mipmapTexture->GetUploadTargets();
if (!uploadTargets.empty() &&
(mipmapTexture->GetMipmapCompressedFormat(uploadTargets[0], static_cast<Uint>(level)) != GL_NONE ||
mipmapTexture->GetMipmapRequestedCompressedFormat(uploadTargets[0], static_cast<Uint>(level)) !=
GL_NONE)) {
RecordClearTextureError(caller, ErrorCode::InvalidOperation,
"Compressed textures cannot be cleared.");
return nullptr;
}
return mipmapTexture;
}
@@ -2197,6 +2261,26 @@ namespace MobileGL::MG_Impl::GLImpl {
} else {
DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level);
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, height, depth}, internalBytes});
// The same specific-compressed-format tag glTexImage2D records (see TexImage2D_State):
// GL 4.6 core 8.5 commits the level to that format, so GL_TEXTURE_COMPRESSED and
// GL_TEXTURE_INTERNAL_FORMAT must report it - and, less obviously, glCopyImageSubData
// sizes the level's texel BLOCK from it. Without the tag a GL_COMPRESSED_RG_RGTC2
// array level measured as the RG8 storage it resolved to, 2 bytes instead of 16, and
// the copy-compatibility rule refused a pairing 18.3.2 requires. AllocateStorage above
// clears the tag, so this has to follow it.
const auto compressedInfo = MG_Util::GetCompressedFormatInfo(static_cast<GLenum>(internalformat));
if (compressedInfo.blockWidth != 0) {
textureMipmapObject->SetMipmapCompressedImage(
textureUploadTarget, level, static_cast<GLenum>(internalformat), nullptr,
MG_Util::CalculateCompressedTextureImageSize(compressedInfo, {width, height, depth}));
}
// Also after AllocateStorage, which clears it. Records the generic GL_COMPRESSED_*
// enums too, which the tag above deliberately skips - glClearTexImage has to refuse
// them all (GL 4.6 core 8.19).
if (IsCompressedGLInternalFormat(static_cast<GLenum>(internalformat))) {
textureMipmapObject->SetMipmapRequestedCompressedFormat(textureUploadTarget, level,
static_cast<GLenum>(internalformat));
}
}
if (!originalPixels) {
@@ -2343,6 +2427,13 @@ namespace MobileGL::MG_Impl::GLImpl {
textureUploadTarget, level, static_cast<GLenum>(internalformat), nullptr,
MG_Util::CalculateCompressedTextureImageSize(compressedInfo, {width, height, 1}));
}
// Also after AllocateStorage, which clears it. Records the generic GL_COMPRESSED_*
// enums too, which the tag above deliberately skips - glClearTexImage has to refuse
// them all (GL 4.6 core 8.19).
if (IsCompressedGLInternalFormat(static_cast<GLenum>(internalformat))) {
textureMipmapObject->SetMipmapRequestedCompressedFormat(textureUploadTarget, level,
static_cast<GLenum>(internalformat));
}
}
if (!originalPixels) {
@@ -2431,6 +2522,13 @@ namespace MobileGL::MG_Impl::GLImpl {
if (!isProxy) {
DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level);
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, 1, 1}, internalBytes});
// After AllocateStorage, which clears the tag. No block-compressed format has a 1D
// layout, so only the specific-format tag the 2D/3D paths record is skipped here - the
// request itself still has to be remembered for glClearTexImage (GL 4.6 core 8.19).
if (IsCompressedGLInternalFormat(static_cast<GLenum>(internalFormat))) {
textureMipmapObject->SetMipmapRequestedCompressedFormat(textureUploadTarget, level,
static_cast<GLenum>(internalFormat));
}
}
if (!originalPixels) {
@@ -2982,6 +3080,15 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = textureObject->GetSamplerObject()->GetMaxAnisotropy();
}
break;
// GL 4.6 core 8.11 lists this among the parameters EVERY GetTexParameter form answers.
// It was handled by the iv/Iiv/Iuiv getters and missed by this one, so the float query
// raised GL_INVALID_ENUM and left the caller's float untouched - which is what
// KHR-GL4x.shader_image_load_store.basic-api-texParam reads back.
case GL_IMAGE_FORMAT_COMPATIBILITY_TYPE:
if (params) {
*params = static_cast<GLfloat>(GL_IMAGE_FORMAT_COMPATIBILITY_BY_SIZE);
}
break;
case GL_DEPTH_STENCIL_TEXTURE_MODE:
if (params) {
*params = static_cast<GLfloat>(textureObject->GetDepthStencilTextureMode());
@@ -3031,6 +3138,9 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).x();
break;
}
case TextureStorageType::Buffer:
*params = GetBufferTextureTexelWidth(textureObject.get());
break;
default:
RecordUnsupportedLevelQueryStorage("GetTexLevelParameteriv_State", pname);
break;
@@ -3046,6 +3156,9 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).y();
break;
}
case TextureStorageType::Buffer:
*params = 1; // a buffer texture is one-dimensional
break;
default:
RecordUnsupportedLevelQueryStorage("GetTexLevelParameteriv_State", pname);
break;
@@ -3061,6 +3174,9 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).z();
break;
}
case TextureStorageType::Buffer:
*params = 1; // a buffer texture is one-dimensional
break;
default:
RecordUnsupportedLevelQueryStorage("GetTexLevelParameteriv_State", pname);
break;
@@ -3130,6 +3246,31 @@ namespace MobileGL::MG_Impl::GLImpl {
}
break;
}
case GL_TEXTURE_BUFFER_SIZE:
case GL_TEXTURE_BUFFER_OFFSET: {
// GL 4.6 core 8.9: both describe the window of the attached buffer a GL_TEXTURE_BUFFER
// texture addresses, so there is nothing to report for any other storage - which is
// INVALID_OPERATION, the same shape GL_TEXTURE_COMPRESSED_IMAGE_SIZE guards itself with
// above.
if (textureObject->GetStorageType() != TextureStorageType::Buffer) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", "GetTexLevelParameteriv_State",
"GL_TEXTURE_BUFFER_SIZE / GL_TEXTURE_BUFFER_OFFSET need a buffer texture."));
return;
}
if (params) {
const auto* bufferTextureObject =
static_cast<MG_State::GLState::TextureObjectBuffer*>(textureObject.get());
// Basic machine units, and UNCLAMPED - see GetBufferTextureTexelWidth for why this
// half does not take the GL_MAX_TEXTURE_BUFFER_SIZE clamp that WIDTH does.
*params = static_cast<GLint>(pname == GL_TEXTURE_BUFFER_SIZE
? bufferTextureObject->GetBufferRangeSizeInBytes()
: bufferTextureObject->GetBufferRangeOffset());
}
break;
}
default:
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetTexLevelParameteriv_State",
@@ -3169,6 +3310,9 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = (GLfloat)textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).x();
break;
}
case TextureStorageType::Buffer:
*params = (GLfloat)GetBufferTextureTexelWidth(textureObject.get());
break;
default:
RecordUnsupportedLevelQueryStorage("GetTexLevelParameterfv_State", pname);
break;
@@ -3184,6 +3328,9 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = (GLfloat)textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).y();
break;
}
case TextureStorageType::Buffer:
*params = 1.0f; // a buffer texture is one-dimensional
break;
default:
RecordUnsupportedLevelQueryStorage("GetTexLevelParameterfv_State", pname);
break;
@@ -3199,6 +3346,9 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = (GLfloat)textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).z();
break;
}
case TextureStorageType::Buffer:
*params = 1.0f; // a buffer texture is one-dimensional
break;
default:
RecordUnsupportedLevelQueryStorage("GetTexLevelParameterfv_State", pname);
break;
@@ -3266,6 +3416,27 @@ namespace MobileGL::MG_Impl::GLImpl {
}
break;
}
case GL_TEXTURE_BUFFER_SIZE:
case GL_TEXTURE_BUFFER_OFFSET: {
// See GetTexLevelParameteriv_State: both describe the attached buffer range of a
// GL_TEXTURE_BUFFER texture, so any other storage makes the query INVALID_OPERATION.
if (textureObject->GetStorageType() != TextureStorageType::Buffer) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", "GetTexLevelParameterfv_State",
"GL_TEXTURE_BUFFER_SIZE / GL_TEXTURE_BUFFER_OFFSET need a buffer texture."));
return;
}
if (params) {
const auto* bufferTextureObject =
static_cast<MG_State::GLState::TextureObjectBuffer*>(textureObject.get());
*params = static_cast<GLfloat>(pname == GL_TEXTURE_BUFFER_SIZE
? bufferTextureObject->GetBufferRangeSizeInBytes()
: bufferTextureObject->GetBufferRangeOffset());
}
break;
}
default:
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetTexLevelParameterfv_State",
@@ -3412,9 +3583,9 @@ namespace MobileGL::MG_Impl::GLImpl {
MG_Backend::gBackendFunctionsTable.GL.CopyTexSubImage2D(target, level, xoffset, yoffset, x, y, width, height);
}
void CopyImageSubData_Backend(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
void CopyImageSubData_Backend(const MG_Backend::CopyImageEndpoint& src,
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
const MG_Backend::CopyImageEndpoint& dst,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
auto copyImageSubData = MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData;
@@ -3425,7 +3596,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"Backend does not support image-to-image copies."));
return;
}
copyImageSubData(srcTexture, srcTarget, srcLevel, srcX, srcY, srcZ, dstTexture, dstTarget, dstLevel, dstX,
copyImageSubData(src, srcTarget, srcLevel, srcX, srcY, srcZ, dst, dstTarget, dstLevel, dstX,
dstY, dstZ, srcWidth, srcHeight, srcDepth);
}
@@ -3472,9 +3643,9 @@ namespace MobileGL::MG_Impl::GLImpl {
// the ~30 entry points that reach it through a BOUND object (where the name was never
// in question and the fault is the binding), so this is a local rule rather than a
// change to the helper.
Bool ValidateCopyImageObjectExists(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
Bool ValidateCopyImageObjectExists(const MG_Backend::CopyImageEndpoint& endpoint,
const char* endpointName) {
if (textureObject) return true;
if (endpoint.Exists()) return true;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>(
@@ -3498,21 +3669,106 @@ namespace MobileGL::MG_Impl::GLImpl {
MG_Util::ConvertTextureTargetToString(textureObject->GetTarget()))));
return false;
}
} // namespace
Bool ValidateCopyImageSubData_State(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
if (!ValidateCopyImageObjectExists(srcTexture, "source") ||
!ValidateCopyImageObjectExists(dstTexture, "destination")) {
// ---- The questions ValidateCopyImageSubData_State asks of one endpoint. ---------------
// A renderbuffer answers all of them directly: it has exactly one image, no mip chain and
// no sampler state, and it carries its own internal format and extent.
Int GetCopyImageEndpointSamples(const MG_Backend::CopyImageEndpoint& endpoint) {
if (endpoint.IsRenderbuffer()) return endpoint.Renderbuffer->GetSamples();
return endpoint.Texture->GetSamples();
}
TextureInternalFormat GetCopyImageEndpointFormat(const MG_Backend::CopyImageEndpoint& endpoint) {
if (endpoint.IsRenderbuffer()) return endpoint.Renderbuffer->GetInternalFormat();
return endpoint.Texture->GetFormat();
}
// A renderbuffer has level 0 and nothing else, and the failure is the same INVALID_VALUE
// ValidateTextureLevelExists records for a level a texture does not have.
Bool ValidateCopyImageEndpointLevelExists(const MG_Backend::CopyImageEndpoint& endpoint, GLint level,
const char* caller) {
if (!endpoint.IsRenderbuffer()) {
return TextureImpl::ValidateTextureLevelExists(endpoint.Texture, level, caller);
}
if (level == 0) return true;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "A renderbuffer has only level 0."));
return false;
}
const auto srcTextureTarget = MG_Util::ConvertGLEnumToTextureTarget(srcTarget);
const auto dstTextureTarget = MG_Util::ConvertGLEnumToTextureTarget(dstTarget);
if (!TextureImpl::ValidateTextureTarget(srcTextureTarget) ||
!TextureImpl::ValidateTextureTarget(dstTextureTarget)) {
// Targets with no mip chain have q == level_base by definition (GL 4.6 core 8.17), so no
// minification filter can make them mipmap incomplete - while the shared predicate derives
// q from the base level's size alone and would call a 16x16 multisample image incomplete.
Bool CopyImageTargetHasMipmapChain(TextureTarget target) {
switch (target) {
case TextureTarget::TextureRectangle:
case TextureTarget::TextureBuffer:
case TextureTarget::Texture2DMultisample:
case TextureTarget::Texture2DMultisampleArray:
return false;
default:
return true;
}
}
Bool IsCopyImageEndpointComplete(const MG_Backend::CopyImageEndpoint& endpoint) {
// A renderbuffer is complete exactly when it has storage - there is nothing else it
// could be missing.
if (endpoint.IsRenderbuffer()) return endpoint.Renderbuffer->IsAllocated();
const auto* texture = endpoint.Texture.get();
if (!texture) return false;
// 18.3.2 asks for TEXTURE completeness, which GL 4.6 core 8.17 defines to include the
// MIP CHAIN whenever the minification filter samples it - and ITextureObject::
// IsComplete() only answers the storage half (an internal format, and no zero-size
// level in the middle of the chain). A texture with level 0 alone and the default
// NEAREST_MIPMAP_LINEAR filter is incomplete, which is exactly how
// KHR-GL43.copy_image.incomplete_tex builds its subject.
//
// The filter is the texture's OWN: copy-image never goes through a texture unit, so no
// sampler object is in play. An immutable texture is unaffected - glTexStorage clamps
// TEXTURE_MAX_LEVEL to levels-1, which is what makes a single-level immutable texture
// mipmap complete under any filter.
const auto& sampler = texture->GetSamplerObject();
const Bool mipmapped = CopyImageTargetHasMipmapChain(texture->GetTarget()) && sampler &&
sampler->GetMipmapMode() != SamplerMipmapMode::None;
return MG_State::GLState::IsMipmapCompleteForFilter(texture, mipmapped);
}
GLenum GetCopyImageEndpointCompressedFormat(const MG_Backend::CopyImageEndpoint& endpoint,
TextureUploadTarget uploadTarget, GLint level) {
if (endpoint.IsRenderbuffer()) return GL_NONE;
return GetCompressedLevelFormat(endpoint.Texture, uploadTarget, level);
}
IntVec3 GetCopyImageEndpointLevelSize(const MG_Backend::CopyImageEndpoint& endpoint,
TextureUploadTarget uploadTarget, GLint level) {
if (endpoint.IsRenderbuffer()) {
return {endpoint.Renderbuffer->GetWidth(), endpoint.Renderbuffer->GetHeight(), 1};
}
return GetCopyImageLevelSize(endpoint.Texture, uploadTarget, level);
}
} // namespace
Bool ValidateCopyImageSubData_State(const MG_Backend::CopyImageEndpoint& src,
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY,
const MG_Backend::CopyImageEndpoint& dst,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
if (!ValidateCopyImageObjectExists(src, "source") ||
!ValidateCopyImageObjectExists(dst, "destination")) {
return false;
}
// GL_RENDERBUFFER has no TextureTarget to convert to, and it needs none: it is its own
// whole-image target, and the endpoint that carries it was resolved from the renderbuffer
// namespace, so it matches its object by construction.
const auto srcTextureTarget =
src.IsRenderbuffer() ? TextureTarget::Unknown : MG_Util::ConvertGLEnumToTextureTarget(srcTarget);
const auto dstTextureTarget =
dst.IsRenderbuffer() ? TextureTarget::Unknown : MG_Util::ConvertGLEnumToTextureTarget(dstTarget);
if ((!src.IsRenderbuffer() && !TextureImpl::ValidateTextureTarget(srcTextureTarget)) ||
(!dst.IsRenderbuffer() && !TextureImpl::ValidateTextureTarget(dstTextureTarget))) {
return false;
}
// GL_TEXTURE_BUFFER and the cube FACE enums convert to a target this frontend knows, but
@@ -3520,8 +3776,8 @@ namespace MobileGL::MG_Impl::GLImpl {
if (!ValidateCopyImageTarget(srcTarget, "source") || !ValidateCopyImageTarget(dstTarget, "destination")) {
return false;
}
if (!ValidateCopyImageTargetMatchesObject(srcTexture, srcTextureTarget, "source") ||
!ValidateCopyImageTargetMatchesObject(dstTexture, dstTextureTarget, "destination")) {
if (!ValidateCopyImageTargetMatchesObject(src.Texture, srcTextureTarget, "source") ||
!ValidateCopyImageTargetMatchesObject(dst.Texture, dstTextureTarget, "destination")) {
return false;
}
if (!TextureImpl::ValidateTextureLevelNumber(srcLevel) ||
@@ -3535,8 +3791,8 @@ namespace MobileGL::MG_Impl::GLImpl {
// driver as an out-of-range mip index - on Adreno that is a SIGSEGV inside
// vkCmdCopyImage, which is what KHR-GL43.copy_image.non_existent_mipmap used to do to
// the whole glcts process. The answer the spec asks for is GL_INVALID_VALUE.
if (!TextureImpl::ValidateTextureLevelExists(srcTexture, srcLevel, __func__) ||
!TextureImpl::ValidateTextureLevelExists(dstTexture, dstLevel, __func__)) {
if (!ValidateCopyImageEndpointLevelExists(src, srcLevel, __func__) ||
!ValidateCopyImageEndpointLevelExists(dst, dstLevel, __func__)) {
return false;
}
if (srcWidth < 0 || srcHeight < 0 || srcDepth < 0) {
@@ -3552,37 +3808,41 @@ namespace MobileGL::MG_Impl::GLImpl {
// A multisample image can only be copied to one with the same sample count, and a
// single-sample image reports zero - so this one comparison is also what rejects
// copying between a multisample target and a non-multisample one.
if (srcTexture->GetSamples() != dstTexture->GetSamples()) {
const Int srcSamples = GetCopyImageEndpointSamples(src);
const Int dstSamples = GetCopyImageEndpointSamples(dst);
if (srcSamples != dstSamples) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", __func__,
std::format("The two images have different sample counts ({} vs. {}).",
srcTexture->GetSamples(), dstTexture->GetSamples())));
srcSamples, dstSamples)));
return false;
}
// 18.3.2: both images must be complete. An incomplete one has no defined texels to copy
// and no defined storage to copy into.
if (!srcTexture->IsComplete() || !dstTexture->IsComplete()) {
const Bool srcComplete = IsCopyImageEndpointComplete(src);
const Bool dstComplete = IsCopyImageEndpointComplete(dst);
if (!srcComplete || !dstComplete) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", __func__,
std::format("A copied image is incomplete (source complete: {}, destination complete: {}).",
srcTexture->IsComplete(), dstTexture->IsComplete())));
srcComplete, dstComplete)));
return false;
}
const auto srcUploadTarget = GetPrimaryUploadTarget(srcTexture);
const auto dstUploadTarget = GetPrimaryUploadTarget(dstTexture);
const auto srcUploadTarget = GetPrimaryUploadTarget(src.Texture);
const auto dstUploadTarget = GetPrimaryUploadTarget(dst.Texture);
const auto srcBlock = TextureImpl::ResolveCopyImageTexelBlock(
srcTexture->GetFormat(), GetCompressedLevelFormat(srcTexture, srcUploadTarget, srcLevel));
GetCopyImageEndpointFormat(src), GetCopyImageEndpointCompressedFormat(src, srcUploadTarget, srcLevel));
const auto dstBlock = TextureImpl::ResolveCopyImageTexelBlock(
dstTexture->GetFormat(), GetCompressedLevelFormat(dstTexture, dstUploadTarget, dstLevel));
GetCopyImageEndpointFormat(dst), GetCopyImageEndpointCompressedFormat(dst, dstUploadTarget, dstLevel));
if (!TextureImpl::ValidateCopyImageFormatCompatibility(srcBlock, dstBlock)) {
return false;
}
const IntVec3 srcLevelSize = GetCopyImageLevelSize(srcTexture, srcUploadTarget, srcLevel);
const IntVec3 dstLevelSize = GetCopyImageLevelSize(dstTexture, dstUploadTarget, dstLevel);
const IntVec3 srcLevelSize = GetCopyImageEndpointLevelSize(src, srcUploadTarget, srcLevel);
const IntVec3 dstLevelSize = GetCopyImageEndpointLevelSize(dst, dstUploadTarget, dstLevel);
if (!TextureImpl::ValidateCopyImageBlockAlignment(srcBlock, srcX, srcY, srcWidth, srcHeight,
srcLevelSize.x(), srcLevelSize.y(), "source") ||
!TextureImpl::ValidateCopyImageBlockAlignment(dstBlock, dstX, dstY, srcWidth, srcHeight,
@@ -4098,8 +4358,14 @@ namespace MobileGL::MG_Impl::GLImpl {
return false;
}
// For a cube map this is exactly cube completeness: IsComplete() wants all six faces.
if (!textureObject->IsComplete()) {
// GL 4.6 core 8.11.4 names cube completeness as the only completeness a readback requires,
// and for a cube map that is exactly what IsComplete() answers (all six faces defined at
// every level). It must not speak for any other target: on a mip chain it also rejects
// "level N defined, the levels below it not", which is a perfectly readable texture at
// level N - and the shape glClearTexImage's conformance cases build, since they define
// only the level they clear. The requested level's own existence is checked below.
if ((target == TextureTarget::TextureCubeMap || target == TextureTarget::TextureCubeMapArray) &&
!textureObject->IsComplete()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Texture is incomplete"));
@@ -4131,8 +4397,8 @@ namespace MobileGL::MG_Impl::GLImpl {
}
// Shared format/type/internal-format matrix (packed-type pairing, depth-vs-color mismatch,
// integer-ness). Also rejects STENCIL_INDEX readback, which needs GL_ARB_texture_stencil8
// (not advertised by MobileGL).
// integer-ness). Also rejects a STENCIL_INDEX readback of anything but stencil-only
// storage, which is the only pairing GL 4.4 / ARB_texture_stencil8 ever made legal.
if (!TextureImpl::ValidateTextureInternalFormatCompatibleWithInput(
textureInputFormat, textureObject->GetFormat(), texturePixelDataType)) {
return false;
@@ -4158,33 +4424,48 @@ namespace MobileGL::MG_Impl::GLImpl {
const auto* textureMipmapObject =
static_cast<const MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
const auto& uploadTargets = textureObject->GetUploadTargets();
if (!uploadTargets.empty() && static_cast<Uint>(level) < textureMipmapObject->GetMipmapLevelCount()) {
// Tightly packed, and summed over every face because a cube map query returns all
// six. Pack pixel-store state only ever grows this, so a request rejected here
// could not have fit under any packing.
const auto texelSize = textureMipmapObject->GetMipmapTexelSize(uploadTargets[0], level);
const SizeT required = MG_Util::CalculateInputTextureImageSize(textureInputFormat,
texturePixelDataType, texelSize) *
uploadTargets.size();
// The half of the completeness gate above that GL does keep: the REQUESTED level has
// to hold an image. A name that was never given one carries no levels at all (which is
// also what an Unknown internal format answers), and a chain grown to reach level N
// leaves every level below it at {0, 0, 0}.
if (uploadTargets.empty() || static_cast<Uint>(level) >= textureMipmapObject->GetMipmapLevelCount()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Texture level has no image to read back."));
return false;
}
const auto texelSize = textureMipmapObject->GetMipmapTexelSize(uploadTargets[0], level);
if (texelSize.x() <= 0 || texelSize.y() <= 0 || texelSize.z() <= 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Texture level has no image to read back."));
return false;
}
if (bufSize >= 0 && static_cast<SizeT>(bufSize) < required) {
// Tightly packed, and summed over every face because a cube map query returns all
// six. Pack pixel-store state only ever grows this, so a request rejected here
// could not have fit under any packing.
const SizeT required = MG_Util::CalculateInputTextureImageSize(textureInputFormat,
texturePixelDataType, texelSize) *
uploadTargets.size();
if (bufSize >= 0 && static_cast<SizeT>(bufSize) < required) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Destination buffer is too small."));
return false;
}
if (pixelPackBufferObject) {
const SizeT bufferSize = pixelPackBufferObject->GetSize();
const SizeT offset = reinterpret_cast<SizeT>(pixels);
if (offset > bufferSize || required > bufferSize - offset) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Destination buffer is too small."));
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"Packing would write past the end of the pixel pack buffer."));
return false;
}
if (pixelPackBufferObject) {
const SizeT bufferSize = pixelPackBufferObject->GetSize();
const SizeT offset = reinterpret_cast<SizeT>(pixels);
if (offset > bufferSize || required > 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 false;
}
}
}
}
@@ -4419,6 +4700,12 @@ namespace MobileGL::MG_Impl::GLImpl {
const SizeT byteSize = ComputeTextureStorageByteSize(textureInternalFormat, levelWidth, 1, 1);
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{levelWidth, 1, 1}, byteSize});
textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, false);
if (IsCompressedGLInternalFormat(internalformat)) {
// After AllocateStorage, which clears the tag. See TexImage1D_State: no compressed
// format has a 1D block layout, but glClearTexImage still has to refuse the request.
textureMipmapObject->SetMipmapRequestedCompressedFormat(textureUploadTarget,
static_cast<Uint>(level), internalformat);
}
}
// Immutable storage defines exactly `levels` levels; AllocateStorage only grows, so a
// longer pre-existing chain has to be dropped explicitly.
@@ -4487,6 +4774,12 @@ namespace MobileGL::MG_Impl::GLImpl {
MG_Util::CalculateCompressedTextureImageSize(compressedInfo,
{levelWidth, levelHeight, 1}));
}
if (IsCompressedGLInternalFormat(internalformat)) {
// Also after AllocateStorage. The generic enums land here and nowhere above,
// and glClearTexImage has to refuse them too (GL 4.6 core 8.19).
textureMipmapObject->SetMipmapRequestedCompressedFormat(uploadTarget,
static_cast<Uint>(level), internalformat);
}
}
// See TextureStorage1D.
textureMipmapObject->TruncateMipmapLevels(uploadTarget, static_cast<Uint>(levels));
@@ -4494,32 +4787,6 @@ namespace MobileGL::MG_Impl::GLImpl {
textureObject->SetImmutableLevels(static_cast<Uint>(levels));
}
// No block-compressed format is defined for a three-dimensional image, so glTexStorage3D on
// TEXTURE_3D must reject one - and with INVALID_OPERATION, not the INVALID_ENUM an unknown
// sized format gets (GL 4.6 core 8.19 / Khronos bug 11239, KHR-GLxx.texture_storage
// .compressed_data). Written against the enum ranges rather than a name list because the
// families are contiguous and MobileGL's own internal-format enum drops the ones it cannot
// carry, which would make this check silently narrower than the API surface.
static Bool IsCompressedGLInternalFormat(GLenum internalformat) {
switch (internalformat) {
case 0x8225: // GL_COMPRESSED_RED
case 0x8226: // GL_COMPRESSED_RG
case 0x84ED: // GL_COMPRESSED_RGB
case 0x84EE: // GL_COMPRESSED_RGBA
case 0x8C48: // GL_COMPRESSED_SRGB
case 0x8C49: // GL_COMPRESSED_SRGB_ALPHA
return true;
default:
break;
}
return (internalformat >= 0x83F0 && internalformat <= 0x83F3) || // S3TC / DXT
(internalformat >= 0x8DBB && internalformat <= 0x8DBE) || // RGTC
(internalformat >= 0x8E8C && internalformat <= 0x8E8F) || // BPTC
(internalformat >= 0x9270 && internalformat <= 0x9279) || // ETC2 / EAC
(internalformat >= 0x93B0 && internalformat <= 0x93BD) || // ASTC LDR
(internalformat >= 0x93D0 && internalformat <= 0x93DD); // ASTC sRGB
}
void TextureStorage3D(GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height,
GLsizei depth) {
auto textureObject = GetTextureObjectByName(texture, __func__);
@@ -4562,6 +4829,10 @@ namespace MobileGL::MG_Impl::GLImpl {
// Array targets keep their layer count constant across levels; only true 3D
// textures halve depth per level (GL 3.3 §3.9 glTexStorage3D).
const Bool depthMips = DepthParticipatesInMipmapping(textureObject->GetTarget());
// The same specific-compressed-format tag glTexStorage2D records, for the array targets a
// compressed glTexStorage3D is legal on (GL_TEXTURE_3D was refused above). Zero width means
// a generic format, which MobileGL answers with uncompressed storage, so it is not tagged.
const auto compressedInfo = MG_Util::GetCompressedFormatInfo(internalformat);
for (GLsizei level = 0; level < levels; ++level) {
const GLsizei levelWidth = std::max<GLsizei>(1, width >> level);
const GLsizei levelHeight = std::max<GLsizei>(1, height >> level);
@@ -4571,6 +4842,19 @@ namespace MobileGL::MG_Impl::GLImpl {
textureMipmapObject->AllocateStorage(textureUploadTarget, level,
{{levelWidth, levelHeight, levelDepth}, byteSize});
textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, false);
if (compressedInfo.blockWidth != 0) {
// After AllocateStorage, which clears the tag.
textureMipmapObject->SetMipmapCompressedImage(
textureUploadTarget, static_cast<Uint>(level), internalformat, nullptr,
MG_Util::CalculateCompressedTextureImageSize(compressedInfo,
{levelWidth, levelHeight, levelDepth}));
}
if (IsCompressedGLInternalFormat(internalformat)) {
// Also after AllocateStorage. The generic enums land here and nowhere above,
// and glClearTexImage has to refuse them too (GL 4.6 core 8.19).
textureMipmapObject->SetMipmapRequestedCompressedFormat(textureUploadTarget,
static_cast<Uint>(level), internalformat);
}
}
// See TextureStorage1D.
textureMipmapObject->TruncateMipmapLevels(textureUploadTarget, static_cast<Uint>(levels));
@@ -5780,17 +6064,29 @@ namespace MobileGL::MG_Impl::GLImpl {
GLuint dstName, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
// A missing name is INVALID_VALUE here, where GetTextureObjectByName's own diagnostic is
// INVALID_OPERATION - so resolve through the plain lookup, which answers a null
// INVALID_OPERATION - so resolve through the plain lookups, which answer a null
// SharedPtr, and let the validator record the error this entry point owes.
const SharedPtr<MG_State::GLState::ITextureObject> srcTexture =
MG_State::pGLContext->GetTextureObject(srcName);
const SharedPtr<MG_State::GLState::ITextureObject> dstTexture =
MG_State::pGLContext->GetTextureObject(dstName);
if (!ValidateCopyImageSubData_State(srcTexture, srcTarget, srcLevel, srcX, srcY, dstTexture, dstTarget,
//
// The TARGET picks the namespace: GL 4.6 core 18.3.2 accepts GL_RENDERBUFFER, and a
// renderbuffer name has nothing to do with a texture name. Resolving both through
// GetTextureObject made every renderbuffer endpoint INVALID_VALUE - or, when the number
// happened to collide with a live texture, INVALID_ENUM from the target check.
const auto resolveEndpoint = [](GLuint name, GLenum target) {
MG_Backend::CopyImageEndpoint endpoint{};
if (target == GL_RENDERBUFFER) {
endpoint.Renderbuffer = MG_State::pGLContext->GetRenderbufferObject(name);
} else {
endpoint.Texture = MG_State::pGLContext->GetTextureObject(name);
}
return endpoint;
};
const MG_Backend::CopyImageEndpoint src = resolveEndpoint(srcName, srcTarget);
const MG_Backend::CopyImageEndpoint dst = resolveEndpoint(dstName, dstTarget);
if (!ValidateCopyImageSubData_State(src, srcTarget, srcLevel, srcX, srcY, dst, dstTarget,
dstLevel, dstX, dstY, srcWidth, srcHeight, srcDepth)) {
return;
}
CopyImageSubData_Backend(srcTexture, srcTarget, srcLevel, srcX, srcY, srcZ, dstTexture, dstTarget, dstLevel,
CopyImageSubData_Backend(src, srcTarget, srcLevel, srcX, srcY, srcZ, dst, dstTarget, dstLevel,
dstX, dstY, dstZ, srcWidth, srcHeight, srcDepth);
}
@@ -313,9 +313,13 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
return false;
}
// TexImage in core 3.3 has no stencil-only upload path (that arrived with GL 4.4).
if (format == TextureInputFormat::StencilIndex) {
return recordInvalidOperation("STENCIL_INDEX is not a valid texture upload format");
// The stencil-only transfer path arrived with GL 4.4 / ARB_texture_stencil8, and only ever
// pairs with stencil-only storage: against a depth, depth-stencil or colour internal format
// STENCIL_INDEX keeps the pre-4.4 answer (GL CTS packed_pixels feeds exactly that pairing
// and expects INVALID_OPERATION).
if (format == TextureInputFormat::StencilIndex &&
internalFormat != TextureInternalFormat::StencilIndex8) {
return recordInvalidOperation("STENCIL_INDEX requires a stencil-only internal format");
}
if (IsDepthLikeInputFormat(format) != IsDepthLikeInternalFormat(internalFormat)) {
@@ -514,10 +514,15 @@ namespace MobileGL::MG_Impl::GLImpl {
// 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.
// Whether the backend can FEED it is detected, not assumed: DirectVulkan needs shaderFloat64,
// and DirectGLES can never have it at all. What that costs is the ARRAY, not the call: GL 4.6
// core 10.3.2 defines no error for a well-formed glVertexAttribLFormat, and a GL 4.3 context
// has 64-bit attributes in core, so declining the call would be non-conformant and would make
// the four pure state queries (VERTEX_ATTRIB_ARRAY_SIZE / _TYPE / _LONG / _RELATIVE_OFFSET)
// unanswerable (KHR-GL43.vertex_attrib_binding.basic-state1/3). The format is therefore
// RECORDED here and the enabled array is dropped at draw instead - loudly, once, naming the
// reason. The matching startup POST row is in MG_Util/SelfTest/DriverPost.cpp; the draw-side
// drop is DirectGLES/Managers.cpp and, on DirectVulkan, VertexInputStateFactory's Float64 case.
static void VertexAttribLFormatSeparate_State(const SharedPtr<MG_State::GLState::VertexArrayObject>& vao,
GLuint attribindex, GLint size, GLenum type,
GLuint relativeoffset) {
@@ -528,14 +533,11 @@ namespace MobileGL::MG_Impl::GLImpl {
if (!MG_Backend::pActiveBackendObject ||
!MG_Backend::pActiveBackendObject->GetDynamicParameters().SupportsFloat64VertexAttributes) {
MGLOG_W_ONCE("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",
"backend has no double-precision vertex attribute support - the format is recorded "
"and queryable, but the array will be DROPPED at draw and the attribute will read "
"its generic current value; 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),
@@ -80,6 +80,7 @@ add_executable(MobileGLIntegrationTest
Scenarios/ImageLoadStoreSsoScenario.cpp
Scenarios/ImageTargetKindScenario.cpp
Scenarios/ImageFormatQualifierScenario.cpp
Scenarios/ImageSizeAfterRespecScenario.cpp
Scenarios/SsboDeclarationFormScenario.cpp
Scenarios/Glsl420DeclarationScenario.cpp
Scenarios/FragmentOutputArrayIndexScenario.cpp
@@ -91,6 +92,8 @@ add_executable(MobileGLIntegrationTest
Scenarios/CopyImageLevelRangeScenario.cpp
Scenarios/CopyImageLayeredScenario.cpp
Scenarios/LayeredAttachmentBarrierScenario.cpp
Scenarios/LayeredTextureReadbackScenario.cpp
Scenarios/AtomicCounterScenario.cpp
)
target_include_directories(MobileGLIntegrationTest PRIVATE
@@ -247,6 +250,19 @@ endif()
set(MGL_ITEST_VULKAN_ENV ${MGL_ITEST_COMMON_ENV})
if (MOBILEGL_ITEST_VK_ICD)
list(APPEND MGL_ITEST_VULKAN_ENV "VK_ICD_FILENAMES=${MOBILEGL_ITEST_VK_ICD}")
# The three iterationRP repairs are tri-state quirks that default to device
# auto-detection, and lavapipe is not on any auto list - so on lavapipe the
# iterationRP scenarios run unrepaired and Program 203 misses its golden
# output. CI's integration-gpu job exports these three by hand; pinning them
# to the ICD instead means a local `ctest -L integration-gpu` measures the
# same thing the gate does, with no environment to remember.
if (MOBILEGL_ITEST_VK_ICD MATCHES "lvp_icd|lavapipe")
message(STATUS "Integration tests: lavapipe ICD - forcing the iterationRP repairs on")
list(APPEND MGL_ITEST_VULKAN_ENV
"MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH=1"
"MOBILEGL_DERIVE_NUM_SUBGROUPS=1"
"MOBILEGL_ITERATIONRP_FIX_BARRIER=1")
endif()
endif()
# The ENVIRONMENT test property is itself a `;`-list, and gtest_discover_tests
@@ -0,0 +1,239 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/AtomicCounterScenario.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
//
// Scenario - ATOMIC COUNTERS, END TO END.
//
// GL_ATOMIC_COUNTER_BUFFER does not exist in ES, and glslang does not hand one to a backend
// either: its Vulkan-relaxed parse rewrites every atomic_uint into a uint member of a
// synthesized gl_AtomicCounterBlock_<N> STORAGE block. Making counters work therefore means
// closing two open ends that used to be missing entirely -
//
// * the block's shader-storage binding, which the IO mapper picked at random and which had no
// relation to the GL binding point N the application bound its buffer to (and could alias an
// SSBO the application binds itself), is moved to a slot reserved at the top of the driver's
// range; and
// * the buffer bound at GL_ATOMIC_COUNTER_BUFFER point N, which nothing in the ES backend ever
// read, is re-issued as a shader-storage binding at that reserved slot.
//
// Neither end alone is observable: with only the first the shader increments a block nobody
// bound a buffer to, with only the second the buffer lands where the shader does not look. The
// only thing that proves both is the VALUE, so every assertion here reads the counter back.
//
// Compute rather than a draw on purpose: the invocation count is exactly what was dispatched,
// while a fragment stage's is a property of the rasterizer (helper invocations, early depth).
// Conformance cases behind this: KHR-GL42/GL43.shader_atomic_counters.basic-usage-cs,
// .advanced-usage-multi-stage and .advanced-usage-draw-update-draw.
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
// Two counters share binding 0 at DIFFERENT offsets and a third sits alone on binding 1.
// The offsets are what separates "the buffer arrived" from "the buffer arrived and the
// block is laid out the way GL says": a lowering that packed the members in declaration
// order without honouring `offset` would still pass a single-counter check.
constexpr const char* kCounterComputeSource = R"(#version 430 core
layout(local_size_x = 4) in;
layout(binding = 0, offset = 0) uniform atomic_uint g_first;
layout(binding = 0, offset = 4) uniform atomic_uint g_second;
layout(binding = 1, offset = 0) uniform atomic_uint g_other;
void main() {
atomicCounterIncrement(g_first);
atomicCounterIncrement(g_second);
atomicCounterIncrement(g_second);
atomicCounterIncrement(g_other);
}
)";
constexpr int kLocalSizeX = 4;
constexpr int kWorkGroups = 2;
constexpr unsigned int kInvocations = kLocalSizeX * kWorkGroups;
// Deliberately non-zero: the shader adds to whatever the application uploaded, so a seed
// that survives is also proof that the buffer's CPU-side contents reached the driver.
constexpr unsigned int kSeedFirst = 5;
constexpr unsigned int kSeedSecond = 100;
constexpr unsigned int kSeedOther = 7;
class AtomicCounterScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
GLint counters = 0;
glGetIntegerv(GL_MAX_COMPUTE_ATOMIC_COUNTERS, &counters);
GLint buffers = 0;
glGetIntegerv(GL_MAX_COMPUTE_ATOMIC_COUNTER_BUFFERS, &buffers);
if (counters < 3 || buffers < 2) {
GTEST_SKIP() << "GL_MAX_COMPUTE_ATOMIC_COUNTERS is " << counters
<< " and GL_MAX_COMPUTE_ATOMIC_COUNTER_BUFFERS is " << buffers
<< "; this needs 3 and 2";
}
if (!AtomicCountersAreWired()) {
GTEST_SKIP() << "atomic counter buffers are not wired up on " << Gl().BackendName()
<< " yet: glslang lowers them onto a storage block and that block's descriptor "
<< "is still resolved from the shader-storage binding points";
}
m_program = CompileComputeProgram(kCounterComputeSource);
ASSERT_NE(m_program, 0u) << m_buildLog;
}
void TearDown() override {
if (!Ready()) return;
glUseProgram(0);
if (!m_buffers.empty()) glDeleteBuffers(static_cast<GLsizei>(m_buffers.size()), m_buffers.data());
if (m_program != 0) glDeleteProgram(m_program);
m_buffers.clear();
m_program = 0;
}
// Magma binds the lowered block as an ordinary storage-buffer descriptor resolved
// from GL_SHADER_STORAGE_BUFFER point N, so the counter buffer never reaches it. The
// frontend half (limits, reflection queries, the link-time offset rules) is
// backend-agnostic and is covered by the unit suites; only the VALUE is scoped here.
bool AtomicCountersAreWired() const { return Gl().BackendName() != "DirectVulkan"; }
unsigned int CompileComputeProgram(const char* source) {
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
glShaderSource(shader, 1, &source, nullptr);
glCompileShader(shader);
GLint compiled = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (compiled == GL_FALSE) {
char log[2048] = {};
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
m_buildLog = std::string("compute shader did not compile: ") + log;
glDeleteShader(shader);
return 0;
}
const GLuint program = glCreateProgram();
glAttachShader(program, shader);
glLinkProgram(program);
glDeleteShader(shader);
GLint linked = 0;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
if (linked == GL_FALSE) {
char log[2048] = {};
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
m_buildLog = std::string("compute program did not link: ") + log;
glDeleteProgram(program);
return 0;
}
return program;
}
// A counter buffer of `count` uints, seeded and bound to atomic-counter point
// `binding`.
GLuint MakeCounterBuffer(GLuint binding, const std::vector<unsigned int>& seed) {
GLuint buffer = 0;
glGenBuffers(1, &buffer);
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, buffer);
glBufferData(GL_ATOMIC_COUNTER_BUFFER,
static_cast<GLsizeiptr>(seed.size() * sizeof(unsigned int)), seed.data(),
GL_DYNAMIC_DRAW);
glBindBufferBase(GL_ATOMIC_COUNTER_BUFFER, binding, buffer);
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, 0);
m_buffers.push_back(buffer);
return buffer;
}
std::vector<unsigned int> ReadCounters(GLuint buffer, int count) {
std::vector<unsigned int> values(static_cast<std::size_t>(count), 0xDEADBEEFu);
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, buffer);
glGetBufferSubData(GL_ATOMIC_COUNTER_BUFFER, 0,
static_cast<GLsizeiptr>(values.size() * sizeof(unsigned int)), values.data());
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, 0);
return values;
}
void Dispatch() {
glUseProgram(m_program);
glDispatchCompute(kWorkGroups, 1, 1);
glMemoryBarrier(GL_ATOMIC_COUNTER_BARRIER_BIT | GL_BUFFER_UPDATE_BARRIER_BIT);
}
unsigned int m_program = 0;
std::string m_buildLog;
std::vector<GLuint> m_buffers;
};
} // namespace
// The counter values a dispatch leaves behind, per binding point and per offset within one
// binding. Nothing in the ES backend used to touch BufferTarget::AtomicCounter at all, so
// before the wiring landed every one of these read back its seed unchanged.
TEST_F(AtomicCounterScenario, DispatchIncrementsTheBoundCounterBuffers) {
if (!Ready() || IsSkipped()) return;
const GLuint zero = MakeCounterBuffer(0, {kSeedFirst, kSeedSecond});
const GLuint one = MakeCounterBuffer(1, {kSeedOther});
ASSERT_EQ(FirstGLError(), 0u) << "binding the counter buffers raised a GL error";
Dispatch();
EXPECT_EQ(FirstGLError(), 0u) << "the dispatch raised a GL error";
const std::vector<unsigned int> zeroValues = ReadCounters(zero, 2);
const std::vector<unsigned int> oneValues = ReadCounters(one, 1);
EXPECT_EQ(FirstGLError(), 0u) << "reading the counters back raised a GL error";
EXPECT_EQ(zeroValues[0], kSeedFirst + kInvocations)
<< "binding 0 offset 0 read back " << zeroValues[0] << "; " << kSeedFirst
<< " means the shader's increments never reached the buffer the application bound";
EXPECT_EQ(zeroValues[1], kSeedSecond + 2 * kInvocations)
<< "binding 0 offset 4 read back " << zeroValues[1] << "; the seed means the counter at a NON-ZERO "
<< "offset was not carried through the lowering, even though offset 0 was";
EXPECT_EQ(oneValues[0], kSeedOther + kInvocations)
<< "binding 1 read back " << oneValues[0] << "; a counter buffer past the first binding point "
<< "resolves to a different reserved slot and is where an off-by-one shows up";
}
// A second dispatch continues from where the first left off, and a re-seed between them is
// visible to the shader. Both halves of the buffer's traffic have to work, in both
// directions: the increments are only observable through the readback path, and the re-seed
// is only observable if the upload reaches the driver AFTER the buffer has been GPU-written.
TEST_F(AtomicCounterScenario, CountersAccumulateAcrossDispatchesAndFollowAReseed) {
if (!Ready() || IsSkipped()) return;
const GLuint zero = MakeCounterBuffer(0, {0u, 0u});
MakeCounterBuffer(1, {0u});
ASSERT_EQ(FirstGLError(), 0u);
Dispatch();
Dispatch();
std::vector<unsigned int> values = ReadCounters(zero, 2);
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_EQ(values[0], 2 * kInvocations) << "two dispatches did not accumulate";
EXPECT_EQ(values[1], 4 * kInvocations) << "two dispatches did not accumulate at offset 4";
const unsigned int reseed[2] = {1000u, 2000u};
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, zero);
glBufferSubData(GL_ATOMIC_COUNTER_BUFFER, 0, sizeof(reseed), reseed);
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, 0);
ASSERT_EQ(FirstGLError(), 0u) << "re-seeding the counter buffer raised a GL error";
Dispatch();
values = ReadCounters(zero, 2);
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_EQ(values[0], reseed[0] + kInvocations) << "the re-seeded value did not reach the shader";
EXPECT_EQ(values[1], reseed[1] + 2 * kInvocations) << "the re-seeded value at offset 4 did not reach the shader";
}
} // namespace MGITest
@@ -299,4 +299,99 @@ void main() {
EXPECT_EQ(FirstGLError(), 0u);
}
// glGetTexLevelParameter used to refuse EVERY pname on a buffer texture: WIDTH/HEIGHT/DEPTH
// fell out of a mipmap-only switch as GL_INVALID_OPERATION, and GL_TEXTURE_BUFFER_SIZE /
// GL_TEXTURE_BUFFER_OFFSET were not in the switch at all, so they came back GL_INVALID_ENUM.
// KHR-GL43.texture_buffer wraps both queries in GLU_EXPECT_NO_ERROR, so the error alone fails
// the case before any value is compared.
//
// The two halves report DIFFERENT units and only one of them is clamped, which is the thing
// easiest to get backwards: WIDTH is a TEXEL count clamped to GL_MAX_TEXTURE_BUFFER_SIZE,
// BUFFER_SIZE is the range in basic machine units exactly as it was given.
TEST_F(BufferTextureScenario, LevelQueriesDescribeTheAttachedBufferRange) {
if (!Ready()) return;
FirstGLError();
GLint offsetAlignment = 1;
glGetIntegerv(GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT, &offsetAlignment);
if (offsetAlignment < 1) offsetAlignment = 1;
GLint maxTexels = 0;
glGetIntegerv(GL_MAX_TEXTURE_BUFFER_SIZE, &maxTexels);
ASSERT_EQ(FirstGLError(), 0u);
ASSERT_GT(maxTexels, 0) << "an OpenGL 4.x context may not advertise a zero buffer-texture limit";
constexpr GLint kTexelBytes = 4; // GL_RGBA8
const GLsizeiptr rangeOffset = static_cast<GLsizeiptr>(offsetAlignment);
const GLsizeiptr rangeBytes = 32 * kTexelBytes;
// Deliberately bigger than the range, so a getter that answered out of the BUFFER rather
// than out of the texture's window would be caught.
const GLsizeiptr bufferBytes = rangeOffset + rangeBytes + 16 * kTexelBytes;
const std::vector<GLubyte> zeros(static_cast<size_t>(bufferBytes), 0);
GLuint buffer = 0;
glGenBuffers(1, &buffer);
glBindBuffer(GL_TEXTURE_BUFFER, buffer);
glBufferData(GL_TEXTURE_BUFFER, bufferBytes, zeros.data(), GL_STATIC_DRAW);
GLuint texture = 0;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_BUFFER, texture);
glTexBufferRange(GL_TEXTURE_BUFFER, GL_RGBA8, buffer, rangeOffset, rangeBytes);
ASSERT_EQ(FirstGLError(), 0u) << "glTexBufferRange(GL_RGBA8) was refused";
const auto levelQuery = [](GLenum pname) {
GLint value = -1;
glGetTexLevelParameteriv(GL_TEXTURE_BUFFER, 0, pname, &value);
return value;
};
const auto levelQueryF = [](GLenum pname) {
GLfloat value = -1.0f;
glGetTexLevelParameterfv(GL_TEXTURE_BUFFER, 0, pname, &value);
return value;
};
EXPECT_EQ(levelQuery(GL_TEXTURE_WIDTH), static_cast<GLint>(rangeBytes / kTexelBytes))
<< "GL_TEXTURE_WIDTH is a texel count over the attached RANGE";
EXPECT_EQ(levelQuery(GL_TEXTURE_HEIGHT), 1);
EXPECT_EQ(levelQuery(GL_TEXTURE_DEPTH), 1);
EXPECT_EQ(levelQuery(GL_TEXTURE_BUFFER_SIZE), static_cast<GLint>(rangeBytes))
<< "GL_TEXTURE_BUFFER_SIZE reports basic machine units, not texels";
EXPECT_EQ(levelQuery(GL_TEXTURE_BUFFER_OFFSET), static_cast<GLint>(rangeOffset));
EXPECT_EQ(FirstGLError(), 0u) << "a buffer-texture level query raised an error";
EXPECT_LE(levelQuery(GL_TEXTURE_WIDTH), maxTexels)
<< "GL_TEXTURE_WIDTH must stay clamped to GL_MAX_TEXTURE_BUFFER_SIZE";
// The float getter is a separate switch and has drifted from the integer one before.
EXPECT_FLOAT_EQ(levelQueryF(GL_TEXTURE_WIDTH), static_cast<GLfloat>(rangeBytes / kTexelBytes));
EXPECT_FLOAT_EQ(levelQueryF(GL_TEXTURE_HEIGHT), 1.0f);
EXPECT_FLOAT_EQ(levelQueryF(GL_TEXTURE_BUFFER_SIZE), static_cast<GLfloat>(rangeBytes));
EXPECT_EQ(FirstGLError(), 0u) << "the float form of a buffer-texture level query raised an error";
// The whole-buffer form follows the buffer's current size instead of freezing a window.
glTexBuffer(GL_TEXTURE_BUFFER, GL_RGBA8, buffer);
EXPECT_EQ(levelQuery(GL_TEXTURE_BUFFER_OFFSET), 0);
EXPECT_EQ(levelQuery(GL_TEXTURE_BUFFER_SIZE), static_cast<GLint>(bufferBytes));
EXPECT_EQ(levelQuery(GL_TEXTURE_WIDTH), static_cast<GLint>(bufferBytes / kTexelBytes));
EXPECT_EQ(FirstGLError(), 0u);
// Both buffer pnames belong to buffer textures alone; anything else is INVALID_OPERATION,
// the same shape GL_TEXTURE_COMPRESSED_IMAGE_SIZE uses for an uncompressed image.
GLuint plainTexture = 0;
glGenTextures(1, &plainTexture);
glBindTexture(GL_TEXTURE_2D, plainTexture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
EXPECT_EQ(FirstGLError(), 0u);
GLint unused = -1;
glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_BUFFER_SIZE, &unused);
EXPECT_EQ(FirstGLError(), static_cast<unsigned int>(GL_INVALID_OPERATION));
glBindTexture(GL_TEXTURE_2D, 0);
glBindTexture(GL_TEXTURE_BUFFER, 0);
glBindBuffer(GL_TEXTURE_BUFFER, 0);
glDeleteTextures(1, &plainTexture);
glDeleteTextures(1, &texture);
glDeleteBuffers(1, &buffer);
EXPECT_EQ(FirstGLError(), 0u);
}
} // namespace MGITest
@@ -697,24 +697,127 @@ void main() {
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
}
TEST_F(DoublePrecisionScenario, A64BitVertexFormatIsDeclinedOnEveryBackend) {
TEST_F(DoublePrecisionScenario, A64BitVertexFormatIsRecordedAndItsArrayIsDroppedAtDraw) {
if (!Ready()) return;
// The demotion leaves no 64-bit shader input to feed, so there is nothing a 64-bit
// vertex FETCH could be fetched into - on either backend, and no longer only on the
// ones whose device lacks shaderFloat64. Declined loudly rather than accepted and
// drawn as garbage; the matching POST row says the same thing at startup.
// ones whose device lacks shaderFloat64.
//
// What that costs is the ARRAY, not the CALL. GL 4.6 core 10.3.2 defines no error for
// a well-formed glVertexAttribLFormat and 64-bit attributes are core in the GL 4.3
// context MobileGL advertises, so refusing the call would be non-conformant and would
// leave four pure state queries unanswerable
// (KHR-GL43.vertex_attrib_binding.basic-state1/3). The format is therefore recorded and
// queryable; the enabled array is what gets dropped, and the attribute then reads its
// generic current value. The matching POST row says exactly that at startup.
GLuint vao = 0;
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
while (glGetError() != GL_NO_ERROR) {}
glVertexAttribLFormat(0, 3, GL_DOUBLE, 0);
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_INVALID_OPERATION));
glVertexAttribLFormat(1, 3, GL_DOUBLE, 8);
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
<< "glVertexAttribLFormat is a legal call in a GL 4.3 context";
GLint attribSize = 0;
GLint attribType = 0;
GLint attribIsLong = 0;
GLint attribRelativeOffset = 0;
glGetVertexAttribiv(1, GL_VERTEX_ATTRIB_ARRAY_SIZE, &attribSize);
glGetVertexAttribiv(1, GL_VERTEX_ATTRIB_ARRAY_TYPE, &attribType);
glGetVertexAttribiv(1, GL_VERTEX_ATTRIB_ARRAY_LONG, &attribIsLong);
glGetVertexAttribiv(1, GL_VERTEX_ATTRIB_RELATIVE_OFFSET, &attribRelativeOffset);
EXPECT_EQ(attribSize, 3);
EXPECT_EQ(attribType, static_cast<GLint>(GL_DOUBLE));
EXPECT_EQ(attribIsLong, GL_TRUE) << "GL_VERTEX_ATTRIB_ARRAY_LONG is what makes this the "
"unconverted form; without it the state is a lie";
EXPECT_EQ(attribRelativeOffset, 8);
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
glBindVertexArray(0);
glDeleteVertexArrays(1, &vao);
while (glGetError() != GL_NO_ERROR) {}
}
// The consequence of recording the state rather than refusing the call: a 64-bit array can
// now be ENABLED in a VAO that a draw uses, which it never could before. That must not
// take the draw down. Leaving such an array enabled with no pointer behind it is exactly
// the documented Adreno null-deref (SIGSEGV inside the next glDraw*), so DirectGLES
// disables it before glVertexAttribPointer can ever see GL_DOUBLE, and DirectVulkan maps
// the format to VK_FORMAT_UNDEFINED so it never enters the pipeline's vertex input state.
//
// The shader deliberately does NOT read location 1: that keeps the two backends on the
// same path (DirectVulkan declines a draw whose SHADER reads an unsupported enabled array,
// by design and loudly, which is a different assertion from this one) and it is the shape
// the crash needed - an enabled array nothing set a pointer for.
TEST_F(DoublePrecisionScenario, AnEnabledLongArrayDoesNotBreakADrawThatIgnoresIt) {
if (!Ready()) return;
constexpr const char* kVs = R"(#version 430 core
layout(location = 0) in vec2 aPos;
void main() { gl_Position = vec4(aPos, 0.0, 1.0); }
)";
constexpr const char* kFs = R"(#version 430 core
out vec4 o_color;
void main() { o_color = vec4(0.0, 1.0, 0.0, 1.0); }
)";
std::string error;
const unsigned int program = CompileProgram(kVs, kFs, &error);
ASSERT_NE(program, 0u) << error;
ColorFbo target = MakeColorFbo(32, 32);
ASSERT_NE(target.fbo, 0u) << "could not create the render target";
BindFbo(target);
const float positions[8] = {-1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f};
const double doubles[4] = {1.0, 2.0, 3.0, 4.0};
GLuint vao = 0;
GLuint positionBuffer = 0;
GLuint doubleBuffer = 0;
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
glGenBuffers(1, &positionBuffer);
glBindBuffer(GL_ARRAY_BUFFER, positionBuffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(positions), positions, GL_STATIC_DRAW);
glGenBuffers(1, &doubleBuffer);
glBindBuffer(GL_ARRAY_BUFFER, doubleBuffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(doubles), doubles, GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glVertexAttribFormat(0, 2, GL_FLOAT, GL_FALSE, 0);
glVertexAttribBinding(0, 0);
glBindVertexBuffer(0, positionBuffer, 0, static_cast<GLsizei>(2 * sizeof(float)));
glEnableVertexAttribArray(0);
glVertexAttribLFormat(1, 1, GL_DOUBLE, 0);
glVertexAttribBinding(1, 1);
glBindVertexBuffer(1, doubleBuffer, 0, static_cast<GLsizei>(sizeof(double)));
glEnableVertexAttribArray(1);
EXPECT_EQ(FirstGLError(), 0u) << "setting up the 64-bit array was refused";
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
glUseProgram(program);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
EXPECT_EQ(FirstGLError(), 0u) << "a draw with an enabled 64-bit array must not raise an error";
const Image image = ReadPixels(target.width, target.height);
ASSERT_FALSE(image.Empty());
EXPECT_GT(image.At(target.width / 2, target.height / 2).g, 200)
<< "the draw did not happen; the enabled 64-bit array must be dropped, not fatal";
glDisableVertexAttribArray(0);
glDisableVertexAttribArray(1);
glBindVertexArray(0);
glDeleteVertexArrays(1, &vao);
glDeleteBuffers(1, &positionBuffer);
glDeleteBuffers(1, &doubleBuffer);
BindDefaultFramebuffer();
DestroyColorFbo(target);
glUseProgram(0);
glDeleteProgram(program);
EXPECT_EQ(FirstGLError(), 0u);
}
} // namespace
} // namespace MGITest
@@ -0,0 +1,234 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ImageSizeAfterRespecScenario.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
//
// Scenario - A DRAW READS imageSize() AFTER THE IMAGE TEXTURE IS RE-SPECIFIED.
//
// KHR-GL43.shader_image_size.advanced-changeSize reduced to its mechanism. The application binds
// a texture to an image unit ONCE, draws, then re-specifies that same texture with a new size
// through glTexImage2D and draws again - without touching the image unit. GL says the unit
// references the texture OBJECT, so the second draw must see the new dimensions.
//
// On Espryt it did not, and the reason is two facts meeting:
//
// 1. ES 3.1 only allows IMMUTABLE storage on an image unit, so the backend forces glTexStorage
// backing on any texture that reaches one (SyncTextureObjectToBackend's
// imageBindableStorageRequired). Immutable storage cannot be redefined, so a glTexImage2D
// that changes size or format has to MINT A NEW ES TEXTURE NAME.
// 2. The draw path never re-issued glBindImageTexture. Image units were established eagerly,
// once, when the application called glBindImageTexture, and PrepareForDraw only ever
// re-synced SAMPLED textures - so the unit kept pointing at the deleted name and
// imageSize() reported whatever that stale binding still meant.
//
// A dispatch was never affected: PrepareForCompute has always swept the image units. This is a
// draw-path scenario for exactly that reason - a compute-shaped case cannot see the defect.
//
// Both backends run it. Magma re-derives its image descriptors per draw and so was never wrong
// here, which makes it the control: the two backends have to agree on what the second draw sees.
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr int kTargetSize = 8;
constexpr const char* kVS = R"(#version 430 core
void main()
{
// A single triangle that covers the whole target, with no vertex buffer at all: the
// scenario is about the image unit, so nothing else may be able to make it fail.
switch (gl_VertexID)
{
case 0: gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); break;
case 1: gl_Position = vec4( 3.0, -1.0, 0.0, 1.0); break;
case 2: gl_Position = vec4(-1.0, 3.0, 0.0, 1.0); break;
}
}
)";
// Green when the image the unit currently holds has the size the application last gave
// it, red otherwise - the conformance case's own comparison, and its own colours.
constexpr const char* kFS = R"(#version 430 core
layout(rgba8) readonly uniform image2D g_image;
uniform ivec2 g_expected_size;
layout(location = 0) out vec4 o_color;
void main()
{
o_color = (imageSize(g_image) == g_expected_size) ? vec4(0.0, 1.0, 0.0, 1.0) : vec4(1.0, 0.0, 0.0, 1.0);
}
)";
class ImageSizeAfterRespecScenario : public ScenarioTest {
protected:
void TearDown() override {
if (!Ready()) return;
glUseProgram(0);
glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
if (m_program != 0) glDeleteProgram(m_program);
if (m_fbo != 0) glDeleteFramebuffers(1, &m_fbo);
if (m_color != 0) glDeleteTextures(1, &m_color);
if (m_image != 0) glDeleteTextures(1, &m_image);
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
m_program = m_fbo = m_color = m_image = m_vao = 0;
while (glGetError() != GL_NO_ERROR) {
}
}
// imageSize() needs a fragment-stage image uniform; a driver that serves none should
// skip rather than fail.
bool FragmentImagesAreUsable() const {
GLint maxImageUnits = 0;
GLint maxFragmentImageUniforms = 0;
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
glGetIntegerv(GL_MAX_FRAGMENT_IMAGE_UNIFORMS, &maxFragmentImageUniforms);
while (glGetError() != GL_NO_ERROR) {
}
return maxImageUnits >= 1 && maxFragmentImageUniforms >= 1;
}
GLuint MakeProgram() {
const GLuint vs = glCreateShader(GL_VERTEX_SHADER);
const GLuint fs = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(vs, 1, &kVS, nullptr);
glShaderSource(fs, 1, &kFS, nullptr);
glCompileShader(vs);
glCompileShader(fs);
for (const GLuint shader : {vs, fs}) {
GLint compiled = GL_FALSE;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (compiled == GL_FALSE) {
char log[4096] = {};
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
ADD_FAILURE() << "a shader did not compile: " << log;
glDeleteShader(vs);
glDeleteShader(fs);
return 0;
}
}
const GLuint program = glCreateProgram();
glAttachShader(program, vs);
glAttachShader(program, fs);
glLinkProgram(program);
glDeleteShader(vs);
glDeleteShader(fs);
GLint linked = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
if (linked == GL_FALSE) {
char log[4096] = {};
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
ADD_FAILURE() << "the program did not link: " << log;
glDeleteProgram(program);
return 0;
}
return program;
}
void MakeRenderTarget() {
glGenTextures(1, &m_color);
glBindTexture(GL_TEXTURE_2D, m_color);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kTargetSize, kTargetSize, 0, GL_RGBA, GL_UNSIGNED_BYTE,
nullptr);
glGenFramebuffers(1, &m_fbo);
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_color, 0);
}
// Draw once with `expected` pushed to the shader and report the centre pixel.
void DrawAndReadCentre(int expectedWidth, int expectedHeight, unsigned char (&centre)[4]) {
const GLint location = glGetUniformLocation(m_program, "g_expected_size");
ASSERT_NE(location, -1) << "the program has no g_expected_size uniform";
glUseProgram(m_program);
glUniform2i(location, expectedWidth, expectedHeight);
glViewport(0, 0, kTargetSize, kTargetSize);
glDisable(GL_SCISSOR_TEST);
glDisable(GL_DEPTH_TEST);
glClearColor(0.0f, 0.0f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glDrawArrays(GL_TRIANGLES, 0, 3);
ASSERT_EQ(FirstGLError(), 0u) << "the draw left a GL error";
std::vector<unsigned char> pixels(static_cast<std::size_t>(kTargetSize) * kTargetSize * 4, 0);
glReadPixels(0, 0, kTargetSize, kTargetSize, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
ASSERT_EQ(FirstGLError(), 0u) << "reading the target back errored";
const std::size_t offset =
(static_cast<std::size_t>(kTargetSize / 2) * kTargetSize + kTargetSize / 2) * 4;
for (int i = 0; i < 4; ++i) {
centre[i] = pixels[offset + static_cast<std::size_t>(i)];
}
}
GLuint m_program = 0;
GLuint m_fbo = 0;
GLuint m_color = 0;
GLuint m_image = 0;
GLuint m_vao = 0;
};
} // namespace
// The whole conformance shape: bind once, draw, re-specify the SAME texture smaller, draw
// again. The first draw is the control - it proves the binding and the shader work at all -
// and the second is the regression pin. Blue would mean the draw never ran; red means the
// image unit answered with the size the texture had BEFORE the re-spec.
TEST_F(ImageSizeAfterRespecScenario, ADrawSeesTheNewSizeOfARespecifiedImageTexture) {
if (!Ready()) return;
if (!FragmentImagesAreUsable()) GTEST_SKIP() << "no fragment-stage image uniform available";
m_program = MakeProgram();
if (m_program == 0) return;
glGenVertexArrays(1, &m_vao);
glBindVertexArray(m_vao);
MakeRenderTarget();
ASSERT_EQ(FirstGLError(), 0u) << "setting the render target up errored";
glGenTextures(1, &m_image);
glBindTexture(GL_TEXTURE_2D, m_image);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 32, 32, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
glBindImageTexture(0, m_image, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
ASSERT_EQ(FirstGLError(), 0u) << "binding the image texture errored";
unsigned char centre[4] = {0, 0, 0, 0};
DrawAndReadCentre(32, 32, centre);
EXPECT_EQ(static_cast<int>(centre[0]), 0) << "the FIRST draw already disagrees about imageSize(): got ("
<< static_cast<int>(centre[0]) << ", "
<< static_cast<int>(centre[1]) << ", "
<< static_cast<int>(centre[2]) << ")";
EXPECT_EQ(static_cast<int>(centre[1]), 255);
// The re-spec. The image unit is deliberately NOT re-bound: GL 4.6 core 8.26 says the
// unit references the texture object, so this alone has to be visible to the next draw.
glBindTexture(GL_TEXTURE_2D, m_image);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
ASSERT_EQ(FirstGLError(), 0u) << "re-specifying the image texture errored";
DrawAndReadCentre(16, 16, centre);
EXPECT_EQ(static_cast<int>(centre[0]), 0)
<< "after the re-spec the draw still sees the OLD image size; centre pixel was ("
<< static_cast<int>(centre[0]) << ", " << static_cast<int>(centre[1]) << ", "
<< static_cast<int>(centre[2]) << ")";
EXPECT_EQ(static_cast<int>(centre[1]), 255);
}
} // namespace MGITest
@@ -0,0 +1,286 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/LayeredTextureReadbackScenario.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
//
// Scenario - READING EVERY LAYER OF A 1D-ARRAY / CUBE-MAP-ARRAY LEVEL BACK.
//
// glGetTexImage has no ES equivalent, so Espryt serves it by attaching the level to a scratch
// READ framebuffer and reading it with glReadPixels. Two of the targets it has to answer for do
// not fit that shape the way the others do, and both came back as zeroes in
// KHR-GL4x.shader_image_load_store.basic-allTargets-* and .non-layered_binding:
//
// * GL_TEXTURE_1D_ARRAY carries its LAYERS in the state-side height - that is what
// glTexImage2D(GL_TEXTURE_1D_ARRAY, w, layers) means - while the ES texture behind it is a 2D
// array of height 1 with the layers in depth. The readback used the state-side shape, so it
// asked layer 0 for a `layers`-row rectangle that layer does not have: row 0 was the only one
// that could be right, and everything past it was whatever reading outside an attachment
// produces.
// * GL_TEXTURE_CUBE_MAP_ARRAY has no glFramebufferTexture2D target token at all, so the 2D
// attach it used to take errored, the scratch FBO stayed incomplete, and every read fell
// through to the CPU shadow - which holds what was UPLOADED, i.e. the seed, not what the
// shader stored.
//
// Both cases store from a compute dispatch (so the only copy of the data is the GPU one and a
// stale shadow cannot pass) and then read the whole level back in one glGetTexImage, checking
// every layer separately so a failure names which one. r32ui throughout: it is a core GLSL ES
// image format, so nothing here can be confused with the missing-format story that
// ImageFormatQualifierScenario covers.
//
// Magma reads these back through its own path and is unaffected by the ES attachment rules, so
// both cases run on both backends and must agree.
#include <cstddef>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr int kExtent = 4;
constexpr int kArrayLayers = 3; // enough that "layer 0 only" is visibly wrong
constexpr int kCubeLayerFaces = 12; // two cubes, which is what the conformance case uses
// A value no store writes, so "the store never landed" and "the store wrote the wrong
// thing" cannot be confused - and so a readback served from the stale CPU shadow is
// recognisable on sight.
constexpr GLuint kSeed = 0xFEEDBEEFu;
// Deliberately not 0: the unit has to travel through glUniform1i and be baked into the
// generated ESSL, so a defect there cannot hide behind the default.
constexpr GLint kImageUnit = 1;
GLuint Expected1DArrayTexel(int x, int layer) {
return 1000u + static_cast<GLuint>(layer) * 100u + static_cast<GLuint>(x);
}
GLuint ExpectedCubeArrayTexel(int x, int y, int layerFace) {
return 1000u + static_cast<GLuint>(layerFace) * 100u + static_cast<GLuint>(y) * 10u +
static_cast<GLuint>(x);
}
// One invocation per texel, and the value it writes is a function of its coordinate - so
// a layer read from the wrong slice does not merely differ, it says which slice it came
// from.
const char* k1DArrayStoreSource = R"(#version 430 core
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
layout (r32ui) writeonly uniform uimage1DArray uni_image;
void main()
{
uint x = gl_GlobalInvocationID.x;
uint layer = gl_GlobalInvocationID.z;
imageStore(uni_image, ivec2(int(x), int(layer)), uvec4(1000u + layer * 100u + x, 0u, 0u, 0u));
}
)";
const char* kCubeArrayStoreSource = R"(#version 430 core
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
layout (r32ui) writeonly uniform uimageCubeArray uni_image;
void main()
{
uint x = gl_GlobalInvocationID.x;
uint y = gl_GlobalInvocationID.y;
uint layerFace = gl_GlobalInvocationID.z;
imageStore(uni_image, ivec3(int(x), int(y), int(layerFace)),
uvec4(1000u + layerFace * 100u + y * 10u + x, 0u, 0u, 0u));
}
)";
class LayeredTextureReadbackScenario : public ScenarioTest {
protected:
void TearDown() override {
if (!Ready()) return;
glUseProgram(0);
for (GLuint p : m_programs) glDeleteProgram(p);
for (GLuint t : m_textures) glDeleteTextures(1, &t);
m_programs.clear();
m_textures.clear();
GLint maxImageUnits = 0;
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
for (GLint unit = 0; unit < maxImageUnits; ++unit) {
glBindImageTexture(static_cast<GLuint>(unit), 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_R32UI);
}
while (glGetError() != GL_NO_ERROR) {
}
}
bool ImagesAreUsable() const {
GLint maxImageUnits = 0;
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
GLint maxComputeImageUniforms = 0;
glGetIntegerv(GL_MAX_COMPUTE_IMAGE_UNIFORMS, &maxComputeImageUniforms);
while (glGetError() != GL_NO_ERROR) {
}
return maxImageUnits > kImageUnit && maxComputeImageUniforms >= 1;
}
GLuint MakeComputeProgram(const char* source) {
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
glShaderSource(shader, 1, &source, nullptr);
glCompileShader(shader);
GLint compiled = GL_FALSE;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (compiled == GL_FALSE) {
char log[4096] = {};
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
ADD_FAILURE() << "the compute shader did not compile: " << log;
glDeleteShader(shader);
return 0;
}
const GLuint program = glCreateProgram();
m_programs.push_back(program);
glAttachShader(program, shader);
glLinkProgram(program);
glDeleteShader(shader);
GLint linked = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
if (linked == GL_FALSE) {
char log[4096] = {};
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
ADD_FAILURE() << "the compute program did not link: " << log;
return 0;
}
return program;
}
GLuint TrackTexture() {
GLuint texture = 0;
glGenTextures(1, &texture);
m_textures.push_back(texture);
return texture;
}
// layered = GL_TRUE, i.e. the whole level: that is what makes every layer reachable
// from one dispatch, and it is what glBindImageTextures is specified to pass.
bool DispatchStore(GLuint program, GLuint texture, GLsizei groupsX, GLsizei groupsY, GLsizei groupsZ) {
glBindImageTexture(static_cast<GLuint>(kImageUnit), texture, 0, GL_TRUE, 0, GL_WRITE_ONLY, GL_R32UI);
if (const GLenum error = FirstGLError()) {
ADD_FAILURE() << "glBindImageTexture errored with " << GLErrorName(error);
return false;
}
glUseProgram(program);
const GLint location = glGetUniformLocation(program, "uni_image");
if (location < 0) {
ADD_FAILURE() << "the image uniform was not reflected";
return false;
}
glUniform1i(location, kImageUnit);
if (const GLenum error = FirstGLError()) {
ADD_FAILURE() << "assigning the image unit errored with " << GLErrorName(error);
return false;
}
glDispatchCompute(groupsX, groupsY, groupsZ);
glMemoryBarrier(GL_ALL_BARRIER_BITS);
glUseProgram(0);
if (const GLenum error = FirstGLError()) {
ADD_FAILURE() << "the dispatch errored with " << GLErrorName(error);
return false;
}
return true;
}
std::vector<GLuint> m_programs;
std::vector<GLuint> m_textures;
};
// The 1D-array half. A layer past the first is the whole test: layer 0 lines up with the
// ES image's only row whichever way the axes are read, so a readback that never swapped
// them still got it right and only the deeper layers came back wrong.
TEST_F(LayeredTextureReadbackScenario, GetTexImageReturnsEveryLayerOfA1DArray) {
if (!Ready()) return;
if (!ImagesAreUsable()) GTEST_SKIP() << "no compute image uniforms";
const GLuint program = MakeComputeProgram(k1DArrayStoreSource);
if (program == 0) return;
const GLuint texture = TrackTexture();
glBindTexture(GL_TEXTURE_1D_ARRAY, texture);
glTexParameteri(GL_TEXTURE_1D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_1D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
const std::vector<GLuint> seed(static_cast<std::size_t>(kExtent) * kArrayLayers, kSeed);
glTexImage2D(GL_TEXTURE_1D_ARRAY, 0, GL_R32UI, kExtent, kArrayLayers, 0, GL_RED_INTEGER, GL_UNSIGNED_INT,
seed.data());
ASSERT_EQ(FirstGLError(), 0u) << "creating the R32UI 1D-array texture errored";
if (!DispatchStore(program, texture, kExtent, 1, kArrayLayers)) return;
std::vector<GLuint> texels(seed.size(), 0u);
glBindTexture(GL_TEXTURE_1D_ARRAY, texture);
glGetTexImage(GL_TEXTURE_1D_ARRAY, 0, GL_RED_INTEGER, GL_UNSIGNED_INT, texels.data());
ASSERT_EQ(FirstGLError(), 0u) << "reading the 1D-array level back errored";
// GL hands a 1D array back as a plain two-dimensional image whose ROWS are the
// layers, so the destination index is layer * width + x.
for (int layer = 0; layer < kArrayLayers; ++layer) {
for (int x = 0; x < kExtent; ++x) {
const std::size_t index = static_cast<std::size_t>(layer) * kExtent + x;
EXPECT_EQ(texels[index], Expected1DArrayTexel(x, layer))
<< "layer " << layer << " texel " << x << " read back "
<< (texels[index] == kSeed ? "the seed (the store never reached it, or the readback came "
"from the stale CPU shadow)"
: "an unexpected value");
}
}
}
// The cube-map-array half. glFramebufferTexture2D has no token for the target, so the
// scratch FBO used to stay incomplete and every read - including layer 0 - was answered
// from the CPU shadow; the seed is what makes that visible rather than merely wrong.
TEST_F(LayeredTextureReadbackScenario, GetTexImageReturnsEveryLayerFaceOfACubeMapArray) {
if (!Ready()) return;
if (!ImagesAreUsable()) GTEST_SKIP() << "no compute image uniforms";
const GLuint program = MakeComputeProgram(kCubeArrayStoreSource);
if (program == 0) return;
const GLuint texture = TrackTexture();
glBindTexture(GL_TEXTURE_CUBE_MAP_ARRAY, texture);
glTexParameteri(GL_TEXTURE_CUBE_MAP_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_CUBE_MAP_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
const std::vector<GLuint> seed(static_cast<std::size_t>(kExtent) * kExtent * kCubeLayerFaces, kSeed);
glTexImage3D(GL_TEXTURE_CUBE_MAP_ARRAY, 0, GL_R32UI, kExtent, kExtent, kCubeLayerFaces, 0, GL_RED_INTEGER,
GL_UNSIGNED_INT, seed.data());
ASSERT_EQ(FirstGLError(), 0u) << "creating the R32UI cube-map-array texture errored";
if (!DispatchStore(program, texture, kExtent, kExtent, kCubeLayerFaces)) return;
std::vector<GLuint> texels(seed.size(), 0u);
glBindTexture(GL_TEXTURE_CUBE_MAP_ARRAY, texture);
glGetTexImage(GL_TEXTURE_CUBE_MAP_ARRAY, 0, GL_RED_INTEGER, GL_UNSIGNED_INT, texels.data());
ASSERT_EQ(FirstGLError(), 0u) << "reading the cube-map-array level back errored";
for (int layerFace = 0; layerFace < kCubeLayerFaces; ++layerFace) {
for (int y = 0; y < kExtent; ++y) {
for (int x = 0; x < kExtent; ++x) {
const std::size_t index =
(static_cast<std::size_t>(layerFace) * kExtent + y) * kExtent + x;
EXPECT_EQ(texels[index], ExpectedCubeArrayTexel(x, y, layerFace))
<< "layer-face " << layerFace << " texel (" << x << ", " << y << ") read back "
<< (texels[index] == kSeed ? "the seed (the store never reached it, or the readback "
"came from the stale CPU shadow)"
: "an unexpected value");
}
}
}
}
} // namespace
} // namespace MGITest
+32
View File
@@ -368,6 +368,31 @@ namespace MobileGL {
return m_transformFeedbackGeometryCaptureDraws;
}
// Conditional rendering (GL 4.6 core 10.9). `discard` is the verdict already
// resolved from the query object at glBeginConditionalRender - the predicate is
// read ONCE there, not per command, because GL specifies the block against the
// result available at Begin and re-reading it would let a query that is still
// being written change the answer mid-block.
void BeginConditionalRender(GLuint queryId, GLenum mode, Bool discard) {
m_conditionalRenderActive = true;
m_conditionalRenderQuery = queryId;
m_conditionalRenderMode = mode;
m_conditionalRenderDiscards = discard;
}
void EndConditionalRender() {
m_conditionalRenderActive = false;
m_conditionalRenderQuery = 0;
m_conditionalRenderMode = GL_NONE;
m_conditionalRenderDiscards = false;
}
Bool IsConditionalRenderActive() const { return m_conditionalRenderActive; }
GLuint GetConditionalRenderQuery() const { return m_conditionalRenderQuery; }
// Whether the commands GL 4.6 core 10.9 makes conditional are being discarded
// right now. False whenever no block is open, so a caller needs no second test.
Bool ConditionalRenderDiscardsCommands() const {
return m_conditionalRenderActive && m_conditionalRenderDiscards;
}
// Transform feedback objects (ARB_transform_feedback2 / GL 4.0 core).
// The capture state above and the indexed GL_TRANSFORM_FEEDBACK_BUFFER
// binding points are object state, but the context keeps exactly one live
@@ -466,6 +491,13 @@ namespace MobileGL {
Uint64 m_transformFeedbackAccountedCaptureDraws = 0;
Uint64 m_transformFeedbackGeometryCaptureDraws = 0;
// Conditional rendering. Context state, not object state: GL 4.6 core 10.9 allows
// exactly one block open at a time and no object owns it.
Bool m_conditionalRenderActive = false;
Bool m_conditionalRenderDiscards = false;
GLuint m_conditionalRenderQuery = 0;
GLenum m_conditionalRenderMode = GL_NONE;
// Everything a transform feedback object owns while it is NOT the bound one.
struct TransformFeedbackObjectState {
struct SavedBufferBinding {
@@ -129,6 +129,180 @@ namespace {
return element;
}
// GL 4.6 core 7.7 / ARB_shader_atomic_counters: within one binding no two atomic counters
// may occupy the same bytes, every offset is a multiple of 4, and no counter may reach past
// GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE. glslang enforces all three in fixOffset(), which the
// Vulkan-relaxed parse never reaches - vkRelaxedRemapUniformVariable folds the atomic_uint
// into a synthesized storage block and returns from declareVariable() before fixOffset()
// runs, clearing explicitOffset on the way ("xxTODO: use logic from fixOffset()"). Two
// counters declared at the same binding AND the same offset therefore linked cleanly.
//
// The offsets themselves survive that lowering (reflection and the SPIR-V generator both
// honour layoutOffset), so the check belongs here, over the same model the GL queries answer
// from. Returns the info-log line for an illegal layout, empty for a legal one.
static MobileGL::String ValidateAtomicCounterLayout(glslang::TProgram& reflection) {
using MobileGL::Bool;
using MobileGL::Int;
using MobileGL::SizeT;
using MobileGL::String;
using MobileGL::Vector;
namespace Transpiler = MobileGL::MG_Util::ShaderTranspiler;
const Int blockCount = reflection.getNumUniformBlocks();
if (blockCount <= 0) return {};
const SizeT prefixLength = std::strlen(Transpiler::ATOMIC_COUNTER_BLOCK_PREFIX);
Vector<Bool> isCounterBlock(static_cast<SizeT>(blockCount), false);
Bool anyCounterBlock = false;
for (Int i = 0; i < blockCount; ++i) {
const auto& block = reflection.getUniformBlock(i);
isCounterBlock[static_cast<SizeT>(i)] =
block.name.compare(0, prefixLength, Transpiler::ATOMIC_COUNTER_BLOCK_PREFIX) == 0;
anyCounterBlock = anyCounterBlock || isCounterBlock[static_cast<SizeT>(i)];
}
if (!anyCounterBlock) return {}; // every program that declares no atomic counter
struct CounterSpan {
Int offset = 0;
Int size = 0;
String name;
};
Vector<Vector<CounterSpan>> spansByBlock(static_cast<SizeT>(blockCount));
const Int uniformCount = reflection.getNumUniformVariables();
for (Int i = 0; i < uniformCount; ++i) {
const auto& uniform = reflection.getUniform(i);
const Int owner = uniform.index;
if (owner < 0 || owner >= blockCount || !isCounterBlock[static_cast<SizeT>(owner)]) continue;
const Int offset = uniform.offset;
if (offset < 0) continue; // no offset recorded; nothing to compare
Int elements = uniform.size > 1 ? uniform.size : 1;
if (const glslang::TType* type = uniform.getType(); type != nullptr && type->isArray()) {
elements = type->isSizedArray() ? type->getCumulativeArraySize() : 1;
}
const Int size = elements * static_cast<Int>(sizeof(MobileGL::Uint32));
if (offset % 4 != 0) {
return std::format("Atomic counter '{}' is declared at offset {}, which is not a multiple of 4.",
uniform.name, offset);
}
if (offset > Transpiler::MAX_ATOMIC_COUNTER_BUFFER_SIZE - size) {
return std::format("Atomic counter '{}' ends at byte {}, past the {}-byte "
"GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE.",
uniform.name, offset + size, Transpiler::MAX_ATOMIC_COUNTER_BUFFER_SIZE);
}
auto& spans = spansByBlock[static_cast<SizeT>(owner)];
for (const CounterSpan& existing : spans) {
if (offset < existing.offset + existing.size && existing.offset < offset + size) {
return std::format("Atomic counters '{}' and '{}' share a binding and overlap at byte offset {}.",
existing.name, uniform.name, std::max(offset, existing.offset));
}
}
spans.push_back({offset, size, uniform.name});
}
return {};
}
// GL 4.6 core 7.6: LinkProgram FAILS when a stage's count of active image uniforms exceeds
// GL_MAX_{VERTEX,TESS_CONTROL,TESS_EVALUATION,GEOMETRY,FRAGMENT,COMPUTE}_IMAGE_UNIFORMS, or
// when their sum exceeds GL_MAX_COMBINED_IMAGE_UNIFORMS. Nothing enforced it: glslang carries
// those numbers in TBuiltInResource only so gl_Max*ImageUniforms can expand from them, and
// its linker never counts uniforms against them - so a program declaring one image uniform
// more than the limit linked cleanly and then rendered nothing.
//
// The limits are the ones glGetIntegerv answers (MG_Impl/GLImpl/Getter/GL_Getter.cpp), the
// hardcoded tessellation zeros included: a program may not exceed a limit the implementation
// advertises, whatever the driver underneath would have taken.
//
// Counts the APPLICATION's image uniforms. The DirectGLES read/write split emits a second
// declaration for an image a stage both reads and writes (MG_Backend/DirectGLES/Utils.h), but
// that happens in the backend after this link, and counting the expanded set here would
// reject programs that are legal by the numbers GL advertises. Returns the info-log line for
// a program over a limit, empty for one within them.
static MobileGL::String ValidateImageUniformLimits(
glslang::TProgram& reflection, const MobileGL::MG_Util::ShaderTranspiler::CompileEnv& env) {
using MobileGL::Array;
using MobileGL::Int;
using MobileGL::SizeT;
using MobileGL::UnorderedMap;
static constexpr EShLanguage kStages[] = {EShLangVertex, EShLangTessControl, EShLangTessEvaluation,
EShLangGeometry, EShLangFragment, EShLangCompute};
static constexpr const char* kLimitNames[] = {
"GL_MAX_VERTEX_IMAGE_UNIFORMS", "GL_MAX_TESS_CONTROL_IMAGE_UNIFORMS",
"GL_MAX_TESS_EVALUATION_IMAGE_UNIFORMS", "GL_MAX_GEOMETRY_IMAGE_UNIFORMS",
"GL_MAX_FRAGMENT_IMAGE_UNIFORMS", "GL_MAX_COMPUTE_IMAGE_UNIFORMS"};
constexpr SizeT kStageCount = sizeof(kStages) / sizeof(kStages[0]);
const Int limits[kStageCount] = {env.params.MaxVertexImageUniforms,
0,
0,
env.params.MaxGeometryImageUniforms,
env.params.MaxFragmentImageUniforms,
env.params.MaxComputeImageUniforms};
// Reflection spells an image ARRAY one of two ways, and which one it picks depends on how
// the shader indexed it: a variable index makes glslang expand the array into one entry
// per element ("u_image[0]".."u_image[8]", each carrying the ELEMENT type), while an
// array never dereferenced at all stays a single entry carrying the array type. One
// program can even produce both spellings for the same array. So neither counting entries
// nor trusting the declared size is right on its own - they are reconciled per declared
// name with a max, which is exact for either spelling and cannot double-count the mixture.
struct ImageUse {
Int entries = 0; // reflection entries seen for this name in this stage
Int declared = 0; // largest element count any of them declared
};
UnorderedMap<MobileGL::String, Array<ImageUse, kStageCount>> useByName;
const Int uniformCount = reflection.getNumUniformVariables();
for (Int i = 0; i < uniformCount; ++i) {
const auto& uniform = reflection.getUniform(i);
const glslang::TType* type = uniform.getType();
if (type == nullptr || !type->isImage()) continue;
// An array occupies one image unit per element; an unsized one (never indexed, so
// never more than the single element glslang kept) counts as one.
Int elements = uniform.size > 1 ? uniform.size : 1;
if (type->isArray()) {
elements = type->isSizedArray() ? type->getCumulativeArraySize() : 1;
}
// `stages` is the set of stages that REFERENCE the uniform, which is exactly what GL
// counts: an image declared in two stages costs a unit in each, and one no stage
// reads is not active at all and costs nothing.
Array<ImageUse, kStageCount>* use = nullptr;
for (SizeT stage = 0; stage < kStageCount; ++stage) {
if ((static_cast<unsigned>(uniform.stages) & (1u << static_cast<unsigned>(kStages[stage]))) == 0) {
continue;
}
// The one insert this uniform performs, so the reference survives the rest of the
// stage loop - a flat hash map relocates on insert, never on read.
if (use == nullptr) {
use = &useByName[StripArrayElementSuffix(uniform.name)];
}
++(*use)[stage].entries;
(*use)[stage].declared = std::max((*use)[stage].declared, elements);
}
}
Int counts[kStageCount] = {};
for (const auto& entry : useByName) {
for (SizeT stage = 0; stage < kStageCount; ++stage) {
counts[stage] += std::max(entry.second[stage].entries, entry.second[stage].declared);
}
}
Int combined = 0;
for (SizeT stage = 0; stage < kStageCount; ++stage) {
combined += counts[stage];
if (counts[stage] > limits[stage]) {
return std::format("This program uses {} active image uniforms in one stage, more than the {} "
"{} allows.",
counts[stage], limits[stage], kLimitNames[stage]);
}
}
if (combined > env.params.MaxCombinedImageUniforms) {
return std::format("This program uses {} active image uniforms across its stages, more than the {} "
"GL_MAX_COMBINED_IMAGE_UNIFORMS allows.",
combined, env.params.MaxCombinedImageUniforms);
}
return {};
}
static bool IsBuiltInPipelineOutput(const glslang::TObjectReflection& output) {
const auto* type = output.getType();
return type && type->getQualifier().builtIn != glslang::EbvNone;
@@ -795,6 +969,22 @@ namespace MobileGL::MG_State::GLState {
return false;
}
if (String atomicCounterError = ValidateAtomicCounterLayout(*artifacts.program);
!atomicCounterError.empty()) {
artifacts.infoLog = Move(atomicCounterError);
DeferLog(std::format("ProgramObject {}: Link failed - {}", in.externalIndex, artifacts.infoLog));
ProgramObject::ResetLinkArtifacts(artifacts);
return false;
}
if (String imageUniformError = ValidateImageUniformLimits(*artifacts.program, env);
!imageUniformError.empty()) {
artifacts.infoLog = Move(imageUniformError);
DeferLog(std::format("ProgramObject {}: Link failed - {}", in.externalIndex, artifacts.infoLog));
ProgramObject::ResetLinkArtifacts(artifacts);
return false;
}
// ---------- GL-facing index spaces (relaxed-parse cleanup) ----------
// Blocks first: global-UBO membership drives the uniform filter below. The
// synthesized MGL_GLOBAL_UBO is a transpiler artifact - its members are GL
@@ -847,7 +1037,16 @@ namespace MobileGL::MG_State::GLState {
// MGL_GLOBAL_UBO, so reflection cannot provide them ("source-explicit");
// - glslang's layoutLocation() for opaque uniforms, where the qualifier
// survives the relaxed parse (and mapIO auto-assigns the rest).
constexpr Uint kNoLocation = glslang::TQualifier::layoutLocationEnd;
//
// "no effective location yet". Deliberately OUTSIDE the location space rather than
// glslang::TQualifier::layoutLocationEnd, which is the first location past the pool and
// therefore only one off a legal one - a sentinel that sits at the boundary it guards has
// to be re-proved safe every time the ceiling moves, and glslang uses that same value for
// "this opaque uniform has no location" as well.
constexpr Uint kNoLocation = ~static_cast<Uint>(0);
// The ceiling glGetIntegerv(GL_MAX_UNIFORM_LOCATIONS) advertises, which is what the
// allocator below has to honour: locations 0..kMaxUniformLocations-1 and no others.
constexpr Uint kMaxUniformLocations = static_cast<Uint>(ProgramObject::MAX_UNIFORM_LOCATIONS);
Vector<Uint> effectiveLocation(tProgramUniformCount, kNoLocation);
Vector<Bool> locationIsSourceExplicit(tProgramUniformCount, false);
UnorderedMap<String, Uint> structExplicitCursor; // declared root -> next member location
@@ -884,13 +1083,19 @@ namespace MobileGL::MG_State::GLState {
cursor->second += static_cast<Uint>(GetUniformLocationSpan(uniform));
}
}
if (effectiveLocation[i] == kNoLocation && type != nullptr && type->isOpaque()) {
// glslang parks "no location" at layoutLocationEnd, which is a real location in this
// table's numbering - test for it explicitly rather than letting it through as one.
if (effectiveLocation[i] == kNoLocation && type != nullptr && type->isOpaque() &&
uniform.layoutLocation() != glslang::TQualifier::layoutLocationEnd) {
effectiveLocation[i] = uniform.layoutLocation();
}
if (locationIsSourceExplicit[i] &&
effectiveLocation[i] + static_cast<Uint>(GetUniformLocationSpan(uniform)) > kNoLocation) {
effectiveLocation[i] + static_cast<Uint>(GetUniformLocationSpan(uniform)) > kMaxUniformLocations) {
// Config A rejected out-of-range explicit locations at parse; keep them
// from growing the location table unboundedly.
// from growing the location table unboundedly. Stated against the advertised
// GL_MAX_UNIFORM_LOCATIONS, because that is the rule being enforced (GL 4.6 core
// 7.6.1): an array whose LAST element passes the ceiling is a link error even
// though its base compiled fine.
artifacts.infoLog = std::format("Uniform '{}' explicit location {} is out of range.", uniform.name,
effectiveLocation[i]);
ProgramObject::ResetLinkArtifacts(artifacts);
@@ -898,12 +1103,55 @@ namespace MobileGL::MG_State::GLState {
}
}
Int requiredUniformLocations = 0;
// ARB_explicit_uniform_location / GL 4.6 core 7.6.1: an explicit location is RESERVED
// whether or not the uniform turned out to be active. The dead default-block uniforms
// filtered out of glUniformIndexToTProgram above are invisible to every GL query - which
// is correct - but their locations must still be kept out of the implicit allocator's
// reach, or an implicit uniform is handed a location the source already claimed.
//
// Deliberately NOT written into artifacts.uniformLocations or uniformIndexInTProgram:
// glGetUniformLocation must keep answering -1 for a dead uniform, and a location no
// application can legally obtain must not become writable through glUniform*. The
// occupancy therefore lives in its own bitset, built once the table has been sized.
Vector<Pair<Uint, Int>> deadExplicitReservations;
Int deadReservedLocationCount = 0;
for (Int i = 0; i < tProgramUniformCount; i++) {
if (artifacts.tProgramUniformIndexToGl[i] >= 0) continue; // GL-visible: handled above
const auto& uniform = artifacts.program->getUniform(i);
if (!isGlobalUboMember(uniform) || uniform.stages != 0) continue;
const Int* explicitLocation = findExplicitLocation(uniform.name);
if (explicitLocation == nullptr) continue;
const Uint location = static_cast<Uint>(*explicitLocation);
const Int locationSpan = GetUniformLocationSpan(uniform);
if (location + static_cast<Uint>(locationSpan) > kMaxUniformLocations) {
artifacts.infoLog = std::format("Uniform '{}' explicit location {} is out of range.", uniform.name,
location);
ProgramObject::ResetLinkArtifacts(artifacts);
return false;
}
deadExplicitReservations.emplace_back(location, locationSpan);
deadReservedLocationCount += locationSpan;
artifacts.maxUniformLocation = std::max(artifacts.maxUniformLocation, location + locationSpan - 1);
MGLOG_D("ProgramObject %u: Reflection - inactive uniform '%s' reserves locations %u..%u without "
"becoming GL-visible",
in.externalIndex, uniform.name.c_str(), location, location + locationSpan - 1);
}
Int requiredUniformLocations = deadReservedLocationCount;
// The same count restricted to DEFAULT-BLOCK uniforms, which is the only thing
// GL_MAX_UNIFORM_LOCATIONS bounds. requiredUniformLocations cannot serve: it also carries
// named-block members, which take a slot in this allocator's table (an implementation
// detail) but consume no GL uniform location at all, so a big UBO array would otherwise
// fail a link the spec allows.
Int defaultBlockLocationDemand = deadReservedLocationCount;
for (const Int i : artifacts.glUniformIndexToTProgram) {
auto& uniform = artifacts.program->getUniform(i);
const Uint location = effectiveLocation[i];
const Int locationSpan = GetUniformLocationSpan(uniform);
requiredUniformLocations += locationSpan;
const Bool inNamedBlock = uniform.index >= 0 && !isGlobalUboMember(uniform);
if (!inNamedBlock) defaultBlockLocationDemand += locationSpan;
if (location != kNoLocation) {
artifacts.maxUniformLocation = std::max(artifacts.maxUniformLocation, location + locationSpan - 1);
}
@@ -916,6 +1164,22 @@ namespace MobileGL::MG_State::GLState {
MGLOG_D("ProgramObject %u: Reflection - computed maxUniformLocation=%u uniformNameMaxLength=%d",
in.externalIndex, artifacts.maxUniformLocation, artifacts.uniformNameMaxLength);
// GL 4.6 core 7.6.1: explicit, implicit and reserved-but-inactive default-block uniforms
// all draw from the one GL_MAX_UNIFORM_LOCATIONS pool, and a program asking for more than
// the implementation advertises FAILS TO LINK
// (KHR-GL43.explicit_uniform_location.uniform-loc-negative-link-max-num-of-locations).
// A single uniform whose own span passes the ceiling was already rejected above; this is
// the aggregate half of the same rule.
if (defaultBlockLocationDemand > static_cast<Int>(kMaxUniformLocations)) {
artifacts.infoLog =
std::format("Uniform locations exhausted: the default-block uniforms need {} locations but "
"GL_MAX_UNIFORM_LOCATIONS is {}.",
defaultBlockLocationDemand, kMaxUniformLocations);
DeferLog(std::format("ProgramObject {}: Link failed - {}", in.externalIndex, artifacts.infoLog));
ProgramObject::ResetLinkArtifacts(artifacts);
return false;
}
if (artifacts.maxUniformLocation + 1 < requiredUniformLocations) {
MGLOG_D("ProgramObject %u: Reflection - maxUniformLocation+1 (%u) < requiredUniformLocations (%d), "
"adjusting",
@@ -930,6 +1194,27 @@ namespace MobileGL::MG_State::GLState {
glslang::TQualifier::layoutLocationEnd);
artifacts.uniformSamplerOrImageUnitIndex.resize(artifacts.maxUniformLocation + 1, -1);
// Occupancy for the inactive explicit uniforms collected above: a set bit means "the
// source claimed this location", which is enough to keep the two implicit passes off it
// without making the location reachable through any GL entry point. A location the
// fallback grow path mints later is past this bitset by construction (every reservation
// was folded into maxUniformLocation before the table was sized), so the lookup treats
// out-of-range as free rather than resizing in lockstep.
// Left empty - and unallocated - when nothing reserved anything, which is every program in
// the shader-pack corpus; the lookup below reads an empty bitset as "nothing is reserved".
Vector<Bool> reservedLocation;
if (!deadExplicitReservations.empty()) {
reservedLocation.assign(artifacts.maxUniformLocation + 1, false);
for (const auto& [reservedBase, reservedSpan] : deadExplicitReservations) {
for (Int element = 0; element < reservedSpan; ++element) {
reservedLocation[reservedBase + element] = true;
}
}
}
const auto locationIsReserved = [&reservedLocation](SizeT location) {
return location < reservedLocation.size() && reservedLocation[location];
};
Vector<int> unallocatedUniformIndex;
// Pass 1: source-explicit locations. These are API contract
@@ -974,7 +1259,8 @@ namespace MobileGL::MG_State::GLState {
Bool spanIsFree = location + locationSpan - 1 <= artifacts.maxUniformLocation;
for (Int element = 0; spanIsFree && element < locationSpan; ++element) {
spanIsFree =
artifacts.uniformIndexInTProgram[location + element] == glslang::TQualifier::layoutLocationEnd;
artifacts.uniformIndexInTProgram[location + element] == glslang::TQualifier::layoutLocationEnd &&
!locationIsReserved(location + element);
}
if (!spanIsFree) {
artifacts.uniformLocations[uniform.name] = kNoLocation;
@@ -1006,7 +1292,8 @@ namespace MobileGL::MG_State::GLState {
bool hasRoom = locNeedle + locationSpan - 1 <= artifacts.maxUniformLocation;
for (Int element = 0; hasRoom && element < locationSpan; ++element) {
hasRoom = artifacts.uniformIndexInTProgram[locNeedle + element] ==
glslang::TQualifier::layoutLocationEnd;
glslang::TQualifier::layoutLocationEnd &&
!locationIsReserved(locNeedle + element);
}
if (!hasRoom) continue;
// Found a vacant location at locNeedle
@@ -1239,7 +1526,6 @@ namespace MobileGL::MG_State::GLState {
}
artifacts.lastStageIsFragment = program.getIntermediate(EShLangFragment) != nullptr;
artifacts.atomicCounterCount = program.getNumAtomicCounters();
for (Uint dim = 0; dim < 3u; ++dim) {
artifacts.computeLocalSize[dim] = program.getLocalSize(static_cast<Int>(dim));
}
@@ -615,15 +615,22 @@ namespace MobileGL::MG_State::GLState {
Int ProgramObject::GetFragmentDataLocation(const char* name) {
if (!Artifacts().program || !name) return -1;
// Answered from the OWNED pipe-output snapshot, not from Artifacts().program. The live
// TProgram is null on a translation-cache L1 hit - that is the entire point of the memo
// - and it is also null for any program that never linked. The old `if
// (!Artifacts().program) return -1` guard silently produced the never-linked answer for
// a perfectly good cached program, so glGetFragDataLocation returned -1 for every
// fragment output of it. The empty snapshot gives the never-linked case the same -1
// without needing the guard at all.
if (!name) return -1;
const auto explicitLocation = Artifacts().linkedFragDataLocation.find(name);
const Int outputCount = Artifacts().program->getNumPipeOutputs();
for (Int index = 0; index < outputCount; ++index) {
const auto& output = Artifacts().program->getPipeOutput(index);
for (const PipeOutputReflection& output : Artifacts().pipeOutputReflection) {
if (output.name != name) continue;
if (explicitLocation != Artifacts().linkedFragDataLocation.end()) return static_cast<Int>(explicitLocation->second);
return static_cast<Int>(output.layoutLocation());
if (explicitLocation != Artifacts().linkedFragDataLocation.end()) {
return static_cast<Int>(explicitLocation->second);
}
return output.location;
}
return -1;
}
@@ -24,6 +24,20 @@ namespace MobileGL::MG_State::GLState {
class ProgramObject {
public:
// GL_MAX_UNIFORM_LOCATIONS: locations 0 .. MAX_UNIFORM_LOCATIONS-1 are the whole legal
// range (GL 4.6 core 7.6.1 / ARB_explicit_uniform_location). Shared with GL_Getter rather
// than spelled twice, because the link and the query must agree exactly - the CTS declares
// a uniform at the advertised value minus one and expects it to link
// (KHR-GL43.explicit_uniform_location.uniform-loc-max).
//
// Tied to glslang's own ceiling and NOT raisable past it: ParseHelper rejects
// `layout(location = N)` for N >= TQualifier::layoutLocationEnd at COMPILE time, so
// layoutLocationEnd - 1 is the largest location any shader in this stack can declare -
// which makes exactly layoutLocationEnd locations, 0 .. layoutLocationEnd - 1, the pool.
// Advertising more would promise a location no shader could name. Comfortably above the
// 1024 GL 4.3 requires.
static constexpr Int MAX_UNIFORM_LOCATIONS = static_cast<Int>(glslang::TQualifier::layoutLocationEnd);
// Everything the query surface ever asked a glslang::TType, flattened. Twenty
// predicates, no recursion: nothing post-link ever walks a struct, a type name or the
// AST, so a POD covers the whole surface exactly.
@@ -761,9 +775,6 @@ namespace MobileGL::MG_State::GLState {
// SIGSEGV inside glslang::TProgram::getNumPipeInputs - KHR-GL30.api.coverage does exactly
// this after a failed glGetAttribLocation, and reached it as soon as the CopyTexImage2D
// throw ahead of it stopped killing the run first.
Int GetActiveAtomicCounterCount() const {
return Artifacts().atomicCounterCount;
}
Int GetActiveAttributesCount() const {
return static_cast<Int>(Artifacts().pipeInputReflection.size());
}
@@ -1002,7 +1013,6 @@ namespace MobileGL::MG_State::GLState {
// outputs are varyings and must report -1 (KHR-GL43.program_interface_query.
// separate-programs-tess-control).
Bool lastStageIsFragment = false;
Int atomicCounterCount = 0;
Array<GLuint, 3> computeLocalSize{};
// Replaces program->getUniformIndex(name). Maps the reflected name to its
// TProgram uniform index.
@@ -8,6 +8,7 @@
#include "ShaderCompileTask.h"
#include <MG_State/GLState/BufferState/BufferState.h>
#include <MG_Util/Converters/MGToGL/ProgramEnumConverter.h>
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <MG_Util/ShaderTranspiler/ShaderSourceProcessor.h>
@@ -15,6 +16,7 @@
#include <glslang/Include/PoolAlloc.h>
#include <algorithm>
#include <charconv>
namespace {
@@ -137,8 +139,21 @@ namespace {
return std::nullopt;
}
// What glGetIntegerv(GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS) answers, recomputed rather than
// queried: the compile runs on a worker with no context, and the pname is not a plain backend
// parameter - the getter caps the backend's count by the state layer's fixed binding-point
// array (GL_Getter's GetIndexedBufferQueryPointCount). A shader must be judged against the
// number the application was told, not against either half of it.
static MobileGL::Int MaxShaderStorageBufferBindings(
const MobileGL::MG_Util::ShaderTranspiler::CompileEnv& env) {
const MobileGL::Int frontendPoints =
static_cast<MobileGL::Int>(MobileGL::MG_State::GLState::BufferBindingPointCount);
if (!env.HasBackend()) return frontendPoints;
return std::min<MobileGL::Int>(frontendPoints, std::max<MobileGL::Int>(env.params.MaxShaderStorageBufferBindings, 0));
}
// The half of a compile that depends on nothing but the source text, the stage and the
// environment snapshot: preprocessing, the two lexical rejections, and the two lexical
// environment snapshot: preprocessing, the three lexical rejections, and the two lexical
// side-channel extractions. Split out so P0b layer 2 can memoize exactly this and
// nothing else - the glslang parse stays per-object because its TShader is consume-once.
// Deliberately free of any per-object state so the memo is sound.
@@ -172,6 +187,13 @@ namespace {
return result;
}
if (const std::optional<String> bindingError = FindShaderStorageBindingViolation(
result.preprocessedSource, MaxShaderStorageBufferBindings(env))) {
result.outcome = ShaderPreprocessOutcome::ResourceBindingRejected;
result.infoLog = *bindingError;
return result;
}
// The parse this feeds runs in the link-compatible configuration (Vulkan-client
// env with relaxed rules): the TShader it produces is what glLinkProgram links and
// what the backends' SPIR-V is generated from - there is no second, GL-client
@@ -26,6 +26,9 @@ namespace MobileGL::MG_State::GLState {
ComputeLocalSizeRejected,
// FindReservedIdentifierViolation rejected it.
ReservedIdentifierRejected,
// FindShaderStorageBindingViolation rejected it: a storage block declared a binding at or
// past GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS.
ResourceBindingRejected,
// The source-only half was clean but glslang rejected the preprocessed source.
// Memoizing this saves the parse itself on every later object with that source.
ParseFailed,
@@ -48,6 +48,7 @@ namespace MobileGL {
m_dirtyRects.resize(requiredLevelCount);
m_compressedData.resize(requiredLevelCount);
m_compressedFormats.resize(requiredLevelCount, GL_NONE);
m_requestedCompressedFormats.resize(requiredLevelCount, GL_NONE);
}
m_texelSizes[level] = input.texelSize;
@@ -79,6 +80,9 @@ namespace MobileGL {
m_compressedFormats[level] = GL_NONE;
m_compressedData[level].clear();
m_compressedData[level].shrink_to_fit();
// Same story for the requested-format tag: a respecified level is whatever this
// call asked for, and the compressed entry points re-arm it right afterwards.
m_requestedCompressedFormats[level] = GL_NONE;
}
void MipmapStorage::SetCompressedImage(Uint level, GLenum internalFormat, const void* data, SizeT size) {
@@ -110,6 +114,16 @@ namespace MobileGL {
return m_compressedData[level].data();
}
void MipmapStorage::SetRequestedCompressedFormat(Uint level, GLenum internalFormat) {
if (level >= m_requestedCompressedFormats.size()) return;
m_requestedCompressedFormats[level] = internalFormat;
}
GLenum MipmapStorage::GetRequestedCompressedFormat(Uint level) const {
if (level >= m_requestedCompressedFormats.size()) return GL_NONE;
return m_requestedCompressedFormats[level];
}
void MipmapStorage::TruncateToLevelCount(SizeT levelCount) {
if (levelCount >= m_data.size()) return;
@@ -120,6 +134,7 @@ namespace MobileGL {
m_dirtyRects.resize(levelCount);
m_compressedData.resize(levelCount);
m_compressedFormats.resize(levelCount);
m_requestedCompressedFormats.resize(levelCount);
}
void MipmapStorage::UpdateSubData(Uint level, DataPtr input) {
@@ -96,6 +96,18 @@ namespace MobileGL {
SizeT GetCompressedByteSize(Uint level) const;
const void* MapCompressedData(Uint level) const;
// The compressed internalformat the application ASKED for, which is not the same
// question as the one above: the six generic GL_COMPRESSED_* enums let the
// implementation choose, MobileGL chooses uncompressed storage, and the level is
// deliberately left untagged so GL_TEXTURE_COMPRESSED keeps answering false and
// glGetCompressedTexImage is not handed a blob nothing ever compressed. The entry
// points that must refuse a compressed image outright (glClearTexImage /
// glClearTexSubImage, GL 4.6 core 8.19) still need to know, so the request is
// recorded separately. Set right after AllocateLevel, which clears it.
void SetRequestedCompressedFormat(Uint level, GLenum internalFormat);
// GL_NONE when the level was not requested with a compressed internalformat.
GLenum GetRequestedCompressedFormat(Uint level) const;
protected:
// Insert one clamped, non-empty write box, keeping the list disjoint
// and bounded (see kMaxDirtyRects).
@@ -115,6 +127,7 @@ namespace MobileGL {
Vector<Vector<MipmapDirtyRegion>> m_dirtyRects;
Vector<Vector<Uint8>> m_compressedData;
Vector<GLenum> m_compressedFormats;
Vector<GLenum> m_requestedCompressedFormats;
};
} // namespace GLState
} // namespace MG_State
@@ -111,6 +111,16 @@ namespace MobileGL {
return m_storage[targetIndex].MapCompressedData(level);
}
void SetRequestedCompressedFormat(Uint targetIndex, Uint level, GLenum internalFormat) {
MOBILEGL_ASSERT(targetIndex < TargetCount, "SetRequestedCompressedFormat: target invalid");
m_storage[targetIndex].SetRequestedCompressedFormat(level, internalFormat);
}
GLenum GetRequestedCompressedFormat(Uint targetIndex, Uint level) const {
MOBILEGL_ASSERT(targetIndex < TargetCount, "GetRequestedCompressedFormat: target invalid");
return m_storage[targetIndex].GetRequestedCompressedFormat(level);
}
protected:
Array<MipmapStorage, TargetCount> m_storage;
};
@@ -250,6 +250,10 @@ namespace MobileGL {
return m_contentVersion;
}
Uint64 TextureObjectBase::GetShapeVersion() const {
return m_shapeVersion;
}
Bool TextureObjectBase::IsMipmapCompleteForFilterCached(Bool mipmapped) const {
const int slot = mipmapped ? 1 : 0;
if (m_completeMemoShapeVersion[slot] == m_shapeVersion) {
@@ -373,6 +377,18 @@ namespace MobileGL {
return m_textureStorage.MapCompressedData(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
}
void TextureObjectWithOneMipmap::SetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget,
Uint mipmapLevel, GLenum internalFormat) {
m_textureStorage.SetRequestedCompressedFormat(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel,
internalFormat);
}
GLenum TextureObjectWithOneMipmap::GetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget,
Uint mipmapLevel) const {
return m_textureStorage.GetRequestedCompressedFormat(GetIndexOfTextureUploadTarget(uploadTarget),
mipmapLevel);
}
IntVec3 TextureObjectWithOneMipmap::GetBaseSize() const {
if (m_textureStorage.GetLevelCount() == 0) {
return {0, 0, 0};
@@ -55,6 +55,12 @@ namespace MobileGL::MG_State::GLState {
// Backends compare it against a per-resource snapshot to skip re-syncing unchanged
// textures across draws (e.g. the block atlas bound across a whole terrain batch).
virtual Uint64 GetContentVersion() const = 0;
// Monotonic counter bumped on every SHAPE mutation - level sizes, the stored level
// set, the internal format, the level range (see BumpShapeVersion). Disjoint from the
// content version on purpose: glTexImage2D(..., nullptr) re-specifies a level's size
// without dirtying a single texel, so a backend that keys its "nothing changed since
// the last sync" skip on content alone keeps a resource of the OLD size alive.
virtual Uint64 GetShapeVersion() const = 0;
// Answers IsMipmapCompleteForFilter() from a memo. Sampling completeness is a
// property of the texture's SHAPE - level sizes, level count, level range,
// internal format - and never of its texel content, but every draw asks about
@@ -106,6 +112,7 @@ namespace MobileGL::MG_State::GLState {
void SetImmutableLevels(Uint levels) override;
Uint16 GetTextureParamsVersion() const override;
Uint64 GetContentVersion() const override;
Uint64 GetShapeVersion() const override;
Bool IsMipmapCompleteForFilterCached(Bool mipmapped) const override;
// Bumps the content version without touching per-level storage-dirty flags. Used when the
// set of defined mip levels grows via GPU-side mip generation (glGenerateMipmap): the level
@@ -220,6 +227,15 @@ namespace MobileGL::MG_State::GLState {
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;
// The compressed internalformat the level was REQUESTED with, recorded even when MobileGL
// answered it with uncompressed storage (the six generic GL_COMPRESSED_* enums) - see
// MipmapStorage. Only the entry points GL forbids on a compressed image read it.
virtual void SetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel,
GLenum internalFormat) = 0;
// GL_NONE when the level was not requested with a compressed internalformat.
virtual GLenum GetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget,
Uint mipmapLevel) const = 0;
};
// Cheap replacement for dynamic_cast on the hot path: TextureObjectMipmap is the
@@ -286,6 +302,9 @@ namespace MobileGL::MG_State::GLState {
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;
void SetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel,
GLenum internalFormat) override;
GLenum GetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
IntVec3 GetBaseSize() const override;
Bool IsComplete() const override;
@@ -96,6 +96,18 @@ namespace MobileGL {
return m_textureStorage.MapCompressedData(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
}
void TextureObject2DCube::SetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget,
Uint mipmapLevel, GLenum internalFormat) {
m_textureStorage.SetRequestedCompressedFormat(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel,
internalFormat);
}
GLenum TextureObject2DCube::GetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget,
Uint mipmapLevel) const {
return m_textureStorage.GetRequestedCompressedFormat(GetIndexOfTextureUploadTarget(uploadTarget),
mipmapLevel);
}
Uint TextureObject2DCube::GetIndexOfTextureUploadTarget(TextureUploadTarget target) const {
MOBILEGL_ASSERT(TextureUploadTarget::CubeMapPositiveX <= target &&
target <= TextureUploadTarget::CubeMapNegativeZ,
@@ -39,6 +39,10 @@ namespace MobileGL {
SizeT GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
const void* MapMipmapCompressedImage(TextureUploadTarget uploadTarget,
Uint mipmapLevel) const override;
void SetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel,
GLenum internalFormat) override;
GLenum GetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget,
Uint mipmapLevel) const override;
IntVec3 GetBaseSize() const override;
Bool IsComplete() const override;
@@ -0,0 +1,167 @@
// MobileGL - MobileGL/MG_Test/Backend/DirectGLES/BaseInstanceInjectionTest.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
//
// The gate on the gl_BaseInstance indirect lowering in
// MG_Backend/DirectGLES/Managers.cpp. That lowering declares a std430 storage block in the
// VERTEX stage, and a vertex-stage storage block is optional in both APIs: the minimum for
// GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS is 0 (GL 4.6 table 23.64, ES 3.2 table 21.44), and ARM's
// GLES driver takes that allowance - a Mali-G925-Immortalis reports 0 for it and for all three
// other graphics stages.
//
// Emitting the block on such a driver does not make it work. The driver refuses the program at
// link time ("The number of vertex shader storage blocks (1) is greater than the maximum number
// allowed (0)"), and because MobileGL's frontend GL_LINK_STATUS is glslang's rather than the
// driver's, the application is told the program linked and then every draw with it renders
// nothing. Dropping the indirect half instead keeps ordinary draws working and costs only the
// per-command baseInstance of an indirect draw.
//
// No GL context and no driver: the lowering is a pure String -> String pass over one capability.
#include <gtest/gtest.h>
#include <MG_Backend/DirectGLES/DirectGLES.h>
#include <MG_Backend/DirectGLES/Managers.h>
using MobileGL::Bool;
using MobileGL::String;
using MobileGL::MG_Backend::DirectGLES::g_GLESCapabilities;
using MobileGL::MG_Backend::DirectGLES::PromoteDrawParameterGlobalsToUniforms;
using MobileGL::MG_Backend::DirectGLES::VertexStageStorageBlockUsable;
namespace {
// The capability block is a process-global the backend fills in at init; restore whatever
// was there so ordering between this suite and any other that touches it cannot matter.
struct ScopedGLESCapabilitiesOverride {
ScopedGLESCapabilitiesOverride(): saved(g_GLESCapabilities) {}
~ScopedGLESCapabilitiesOverride() { g_GLESCapabilities = saved; }
ScopedGLESCapabilitiesOverride(const ScopedGLESCapabilitiesOverride&) = delete;
ScopedGLESCapabilitiesOverride& operator=(const ScopedGLESCapabilitiesOverride&) = delete;
MobileGL::MG_External::GLESCapabilities saved;
};
Bool Contains(const String& haystack, const String& needle) {
return haystack.find(needle) != String::npos;
}
// What SPIRV-Cross hands the backend after LowerDrawParametersPass has demoted
// gl_BaseInstance to a Private global.
constexpr const char* kLoweredBaseInstanceVertexShader = R"(#version 310 es
highp int mg_BaseInstanceLowered;
void main() {
int instance = gl_InstanceID + mg_BaseInstanceLowered;
gl_Position = vec4(float(instance));
}
)";
} // namespace
// One block is all the indirect view needs, so the predicate is a >= 1 test.
TEST(VertexStageStorageBlockUsableTest, RequiresAtLeastOneBlock) {
EXPECT_FALSE(VertexStageStorageBlockUsable(0));
EXPECT_TRUE(VertexStageStorageBlockUsable(1));
EXPECT_TRUE(VertexStageStorageBlockUsable(16));
}
// A driver that leaves the out-param untouched tells us nothing, and guessing "yes" is exactly
// what produces the unlinkable program. Unusable, not clamped up to one.
TEST(VertexStageStorageBlockUsableTest, ANegativeCountIsUnusableRatherThanClamped) {
EXPECT_FALSE(VertexStageStorageBlockUsable(-1));
EXPECT_FALSE(VertexStageStorageBlockUsable(-2147483647 - 1));
}
TEST(BaseInstanceInjectionGate, DriverWithVertexStorageBlocksGetsTheIndirectView) {
const ScopedGLESCapabilitiesOverride capsGuard;
g_GLESCapabilities.IndirectDrawInstanceIdIncludesBaseInstance = false;
g_GLESCapabilities.MaxShaderStorageBufferBindings = 13;
g_GLESCapabilities.MaxVertexShaderStorageBlocks = 1;
const String rewritten =
PromoteDrawParameterGlobalsToUniforms(kLoweredBaseInstanceVertexShader, GL_VERTEX_SHADER);
EXPECT_TRUE(Contains(rewritten, "layout(std430, binding = 12) readonly buffer mg_IndirectParams"));
EXPECT_TRUE(Contains(rewritten, "uniform highp int mg_BaseInstanceWordIndex;"));
EXPECT_TRUE(Contains(rewritten, "#define mg_BaseInstanceLowered ((mg_BaseInstanceWordIndex > 0) ? "
"int(mg_indirectWords[uint(mg_BaseInstanceWordIndex - 1)]) : mg_BaseInstance)"))
<< rewritten;
}
// The bug this gate exists for. The block must not appear at all - not at a different binding,
// not behind a preprocessor guard: a declaration the driver counts is a declaration that makes
// the whole program unlinkable, and the frontend never surfaces that failure.
TEST(BaseInstanceInjectionGate, DriverWithoutVertexStorageBlocksDeclaresNoBlockAtAll) {
const ScopedGLESCapabilitiesOverride capsGuard;
g_GLESCapabilities.IndirectDrawInstanceIdIncludesBaseInstance = false;
g_GLESCapabilities.MaxShaderStorageBufferBindings = 13;
g_GLESCapabilities.MaxVertexShaderStorageBlocks = 0;
const String rewritten =
PromoteDrawParameterGlobalsToUniforms(kLoweredBaseInstanceVertexShader, GL_VERTEX_SHADER);
EXPECT_FALSE(Contains(rewritten, "mg_IndirectParams")) << rewritten;
EXPECT_FALSE(Contains(rewritten, "buffer"));
EXPECT_FALSE(Contains(rewritten, "mg_indirectWords"));
// Nothing reads the word index any more, so nothing may declare it either - its presence is
// what BackendProgramObjectImpl uses to decide whether to bind an indirect params buffer.
EXPECT_FALSE(Contains(rewritten, "mg_BaseInstanceWordIndex"));
}
// Degraded, but still correct for every non-indirect draw: the plain mg_BaseInstance uniform is
// what the non-indirect draw entry points already write.
TEST(BaseInstanceInjectionGate, WithoutTheBlockBaseInstanceFallsBackToThePlainUniform) {
const ScopedGLESCapabilitiesOverride capsGuard;
g_GLESCapabilities.IndirectDrawInstanceIdIncludesBaseInstance = false;
g_GLESCapabilities.MaxShaderStorageBufferBindings = 13;
g_GLESCapabilities.MaxVertexShaderStorageBlocks = 0;
const String rewritten =
PromoteDrawParameterGlobalsToUniforms(kLoweredBaseInstanceVertexShader, GL_VERTEX_SHADER);
EXPECT_TRUE(Contains(rewritten, "uniform highp int mg_BaseInstance;")) << rewritten;
EXPECT_TRUE(Contains(rewritten, "#define mg_BaseInstanceLowered (mg_BaseInstance)")) << rewritten;
// The global declaration must be gone; leaving it would shadow the define.
EXPECT_FALSE(Contains(rewritten, "highp int mg_BaseInstanceLowered;\n"));
}
// On a driver that both leaks baseInstance into gl_InstanceID and has no vertex storage block,
// the rebase has nothing to subtract. Subtracting the uniform instead would remove the base
// twice from every non-indirect draw, which is worse than not rebasing at all.
TEST(BaseInstanceInjectionGate, WithoutTheBlockInstanceIdRebaseCollapsesToIdentity) {
const ScopedGLESCapabilitiesOverride capsGuard;
g_GLESCapabilities.IndirectDrawInstanceIdIncludesBaseInstance = true;
g_GLESCapabilities.MaxShaderStorageBufferBindings = 13;
g_GLESCapabilities.MaxVertexShaderStorageBlocks = 0;
const String rewritten =
PromoteDrawParameterGlobalsToUniforms(kLoweredBaseInstanceVertexShader, GL_VERTEX_SHADER);
EXPECT_TRUE(Contains(rewritten, "#define mg_ZeroBasedInstanceID gl_InstanceID")) << rewritten;
EXPECT_FALSE(Contains(rewritten, "gl_InstanceID - ("));
EXPECT_FALSE(Contains(rewritten, "mg_indirectWords"));
}
// The gate is scoped to the block, not to the whole pass: mg_DrawID and mg_BaseVertex are plain
// uniforms with no storage block behind them and must still be promoted on such a driver.
TEST(BaseInstanceInjectionGate, DrawIdAndBaseVertexArePromotedRegardless) {
const ScopedGLESCapabilitiesOverride capsGuard;
g_GLESCapabilities.IndirectDrawInstanceIdIncludesBaseInstance = false;
g_GLESCapabilities.MaxShaderStorageBufferBindings = 13;
g_GLESCapabilities.MaxVertexShaderStorageBlocks = 0;
const String source = R"(#version 310 es
highp int mg_DrawID;
highp int mg_BaseVertex;
void main() {
gl_Position = vec4(float(mg_DrawID + mg_BaseVertex));
}
)";
const String rewritten = PromoteDrawParameterGlobalsToUniforms(source, GL_VERTEX_SHADER);
EXPECT_TRUE(Contains(rewritten, "uniform highp int mg_DrawID;")) << rewritten;
EXPECT_TRUE(Contains(rewritten, "uniform highp int mg_BaseVertex;")) << rewritten;
}
@@ -16,5 +16,22 @@ target_link_libraries(
${LINK_LIBRARIES}
)
add_executable(
BaseInstanceInjectionTest
BaseInstanceInjectionTest.cpp
)
target_include_directories(BaseInstanceInjectionTest PRIVATE
${MGL_ROOT}/include
${MGL_ROOT}/MobileGL
)
target_link_libraries(
BaseInstanceInjectionTest PRIVATE
GTest::gtest_main
${LINK_LIBRARIES}
)
include(GoogleTest)
gtest_discover_tests(EsslShaderPassTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
gtest_discover_tests(BaseInstanceInjectionTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
@@ -59,9 +59,11 @@ void main()
const String out = SplitReadWriteImageUniforms(source);
// Both halves: same binding, same format, same type - which is what makes two image
// variables on one image unit legal.
EXPECT_TRUE(Contains(out, "layout(binding = 2, rgba8) uniform readonly highp image2D goku;"));
EXPECT_TRUE(Contains(out, "layout(binding = 2, rgba8) uniform writeonly highp image2D " + WriteAlias("goku") + ";"));
// variables on one image unit legal - and both `coherent`, which is what makes the store
// through one of them visible to the load through the other.
EXPECT_TRUE(Contains(out, "layout(binding = 2, rgba8) uniform coherent readonly highp image2D goku;"));
EXPECT_TRUE(Contains(
out, "layout(binding = 2, rgba8) uniform coherent writeonly highp image2D " + WriteAlias("goku") + ";"));
// The load keeps the original name, the store moves to the writeonly half.
EXPECT_TRUE(Contains(out, "imageLoad(goku,"));
@@ -152,9 +154,9 @@ void main()
}
)";
const String out = SplitReadWriteImageUniforms(source);
EXPECT_TRUE(Contains(out, "layout(binding = 6, rgba8) uniform readonly highp image2D gohan[3];"));
EXPECT_TRUE(Contains(out,
"layout(binding = 6, rgba8) uniform writeonly highp image2D " + WriteAlias("gohan") + "[3];"));
EXPECT_TRUE(Contains(out, "layout(binding = 6, rgba8) uniform coherent readonly highp image2D gohan[3];"));
EXPECT_TRUE(Contains(
out, "layout(binding = 6, rgba8) uniform coherent writeonly highp image2D " + WriteAlias("gohan") + "[3];"));
EXPECT_TRUE(Contains(out, "imageStore(" + WriteAlias("gohan") + "[1],"));
EXPECT_TRUE(Contains(out, "imageLoad(gohan[2],"));
}
@@ -174,9 +176,11 @@ void main()
)";
const String out = SplitReadWriteImageUniforms(source);
// goku is read+write -> split; goku_hd is write-only -> qualified in place, not split.
EXPECT_TRUE(Contains(out, "layout(binding = 1, rgba8) uniform readonly highp image2D goku;"));
EXPECT_TRUE(Contains(out, "layout(binding = 1, rgba8) uniform writeonly highp image2D " + WriteAlias("goku") + ";"));
// goku is read+write -> split (and coherent with it); goku_hd is write-only -> qualified in
// place, not split, and left non-coherent because nothing aliases it.
EXPECT_TRUE(Contains(out, "layout(binding = 1, rgba8) uniform coherent readonly highp image2D goku;"));
EXPECT_TRUE(Contains(
out, "layout(binding = 1, rgba8) uniform coherent writeonly highp image2D " + WriteAlias("goku") + ";"));
EXPECT_TRUE(Contains(out, "layout(binding = 2, rgba8) uniform writeonly highp image2D goku_hd;"));
EXPECT_TRUE(Contains(out, "imageStore(goku_hd,"));
EXPECT_FALSE(Contains(out, WriteAlias("goku") + "_hd"));
@@ -197,6 +201,36 @@ void main()
EXPECT_TRUE(Contains(out, "uniform readonly coherent restrict highp image2D goku;"));
EXPECT_TRUE(
Contains(out, "uniform writeonly coherent restrict highp image2D " + WriteAlias("goku") + ";"));
// ...and the coherent the split adds is not a SECOND one: a repeated memory qualifier is a
// compile error in ESSL, so the source's own has to be recognized.
EXPECT_EQ(CountOf(out, "coherent"), 2u);
}
// The visibility half of the split, and the reason it is not cosmetic: GLSL orders a
// same-variable read-after-write within one invocation by construction, but once the store goes
// through `mg_imageWrite_goku` and the load through `goku` the two are DIFFERENT variables, and
// the ordering only holds if both are coherent. Desktop sources almost never say so - they had
// no reason to - which is how KHR-GL4x.shader_image_load_store.advanced-memory-order's
// store/load/compare loop started reading back the value it had not stored yet.
TEST(SplitReadWriteImageUniformsTest, SplitPairIsMadeCoherentEvenWhenTheSourceIsNot) {
const String source = R"(#version 320 es
layout(binding = 2, rgba8) uniform highp image2D goku;
layout(binding = 3, rgba8) uniform highp image2D storeOnly;
layout(location = 0) out highp vec4 mg_FragColor;
void main()
{
imageStore(goku, ivec2(0), vec4(1.0));
mg_FragColor = imageLoad(goku, ivec2(0));
imageStore(storeOnly, ivec2(0), vec4(2.0));
}
)";
const String out = SplitReadWriteImageUniforms(source);
EXPECT_TRUE(Contains(out, "uniform coherent readonly highp image2D goku;")) << out;
EXPECT_TRUE(Contains(out, "uniform coherent writeonly highp image2D " + WriteAlias("goku") + ";")) << out;
// Exactly the two halves of the pair, and nothing else: the store-only image is repaired in
// place, has no alias to stay visible to, and must not pay for uncached access.
EXPECT_EQ(CountOf(out, "coherent"), 2u);
EXPECT_TRUE(Contains(out, "uniform writeonly highp image2D storeOnly;")) << out;
}
// imageSize reads no texels and writes none, so it decides nothing; readonly is what keeps
@@ -34,6 +34,17 @@ namespace {
GLint maxFragmentImageUniforms = 4;
GLint maxComputeImageUniforms = 5;
bool maxGeometryImageUniformsQueried = false;
// Per-stage GL_MAX_*_SHADER_STORAGE_BLOCKS. The vertex and fragment pnames are ES 3.1,
// but the tessellation and geometry ones only exist from ES 3.2 on, so asking for them
// on an older context raises GL_INVALID_ENUM - the same shape as the buffer-texture and
// anisotropy probes. The "queried" flags are what pin that gating; the "raises error"
// knob is what pins the drain.
GLint maxTessControlSsboBlocks = 6;
GLint maxTessEvaluationSsboBlocks = 7;
GLint maxGeometrySsboBlocks = 8;
GLint maxFragmentSsboBlocks = 9;
bool tessAndGeometrySsboBlocksQueried = false;
bool perStageSsboBlockQueryRaisesError = false;
GLfloat minFragmentInterpolationOffset = -0.75f;
GLfloat maxFragmentInterpolationOffset = 0.625f;
GLint fragmentInterpolationOffsetBits = 6;
@@ -111,7 +122,30 @@ namespace {
funcs.glGetIntegerv = [](GLenum pname, GLint* data) {
switch (pname) {
case GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS:
*data = g_fake.maxVertexSsboBlocks;
if (g_fake.perStageSsboBlockQueryRaisesError) {
g_fake.pendingError = GL_INVALID_ENUM;
} else {
*data = g_fake.maxVertexSsboBlocks;
}
break;
case GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS:
if (g_fake.perStageSsboBlockQueryRaisesError) {
g_fake.pendingError = GL_INVALID_ENUM;
} else {
*data = g_fake.maxFragmentSsboBlocks;
}
break;
case GL_MAX_TESS_CONTROL_SHADER_STORAGE_BLOCKS:
g_fake.tessAndGeometrySsboBlocksQueried = true;
*data = g_fake.maxTessControlSsboBlocks;
break;
case GL_MAX_TESS_EVALUATION_SHADER_STORAGE_BLOCKS:
g_fake.tessAndGeometrySsboBlocksQueried = true;
*data = g_fake.maxTessEvaluationSsboBlocks;
break;
case GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS:
g_fake.tessAndGeometrySsboBlocksQueried = true;
*data = g_fake.maxGeometrySsboBlocks;
break;
case GL_MAX_VERTEX_IMAGE_UNIFORMS:
*data = g_fake.maxVertexImageUniforms;
@@ -400,6 +434,10 @@ namespace {
MobileGL::MG_External::GLESCapabilities MakeEs31Capabilities() {
MobileGL::MG_External::GLESCapabilities caps;
caps.GLESVersion = {3, 1, 0};
// The probe reads its vertex storage-block gate from caps rather than re-querying the
// driver (FillInGLESCapabilities resolves the per-stage limits before calling it), so a
// caps struct handed to the probe directly has to carry what the fake reports.
caps.MaxVertexShaderStorageBlocks = g_fake.maxVertexSsboBlocks;
return caps;
}
@@ -527,6 +565,83 @@ TEST(ImageUniformCapabilities, QueriesRealPerStageLimitsAndConservativelyGatesGe
EXPECT_TRUE(g_fake.maxGeometryImageUniformsQueried);
}
// The per-stage GL_MAX_*_SHADER_STORAGE_BLOCKS probes. These decide whether an application is
// told it may declare a storage block in a graphics stage, and on a driver that cannot serve one
// a wrong answer is not a cosmetic mis-report: the program is built, the driver refuses it at
// link time, the frontend reports LINK_STATUS true anyway, and every draw with it renders
// nothing. A Mali-G925-Immortalis reports 0 for vertex, both tessellation stages and geometry.
TEST(PerStageStorageBlockCapabilities, TakesTheDriverValuesAndGatesTessAndGeometryOnEs32) {
const auto funcs = MakeFakeGLESFunctions();
// ES 3.1: the tessellation and geometry pnames do not exist, so they must not be asked for
// and the stages must report the spec minimum of 0 rather than a hopeful driver number.
ResetFakeDriver();
g_fake.maxVertexSsboBlocks = 3;
MobileGL::MG_External::GLESCapabilities es31Caps;
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(es31Caps, funcs));
EXPECT_EQ(es31Caps.MaxVertexShaderStorageBlocks, 3);
EXPECT_EQ(es31Caps.MaxFragmentShaderStorageBlocks, g_fake.maxFragmentSsboBlocks);
EXPECT_EQ(es31Caps.MaxTessControlShaderStorageBlocks, 0);
EXPECT_EQ(es31Caps.MaxTessEvaluationShaderStorageBlocks, 0);
EXPECT_EQ(es31Caps.MaxGeometryShaderStorageBlocks, 0);
EXPECT_FALSE(g_fake.tessAndGeometrySsboBlocksQueried);
// ES 3.2: all five are real pnames and all five driver values must come through verbatim.
ResetFakeDriver();
g_fake.maxVertexSsboBlocks = 3;
g_fake.glesMinorVersion = 2;
MobileGL::MG_External::GLESCapabilities es32Caps;
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(es32Caps, funcs));
EXPECT_EQ(es32Caps.MaxVertexShaderStorageBlocks, 3);
EXPECT_EQ(es32Caps.MaxTessControlShaderStorageBlocks, g_fake.maxTessControlSsboBlocks);
EXPECT_EQ(es32Caps.MaxTessEvaluationShaderStorageBlocks, g_fake.maxTessEvaluationSsboBlocks);
EXPECT_EQ(es32Caps.MaxGeometryShaderStorageBlocks, g_fake.maxGeometrySsboBlocks);
EXPECT_EQ(es32Caps.MaxFragmentShaderStorageBlocks, g_fake.maxFragmentSsboBlocks);
EXPECT_TRUE(g_fake.tessAndGeometrySsboBlocksQueried);
}
// Zero has to survive the round trip intact. It is the answer that matters most - it is what
// ARM's driver actually reports - so a probe that silently substituted a floor would put the
// bug straight back.
TEST(PerStageStorageBlockCapabilities, AZeroFromTheDriverIsReportedAsZero) {
const auto funcs = MakeFakeGLESFunctions();
ResetFakeDriver();
g_fake.glesMinorVersion = 2;
g_fake.maxVertexSsboBlocks = 0;
g_fake.maxTessControlSsboBlocks = 0;
g_fake.maxTessEvaluationSsboBlocks = 0;
g_fake.maxGeometrySsboBlocks = 0;
g_fake.maxFragmentSsboBlocks = 16;
MobileGL::MG_External::GLESCapabilities maliLikeCaps;
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(maliLikeCaps, funcs));
EXPECT_EQ(maliLikeCaps.MaxVertexShaderStorageBlocks, 0);
EXPECT_EQ(maliLikeCaps.MaxTessControlShaderStorageBlocks, 0);
EXPECT_EQ(maliLikeCaps.MaxTessEvaluationShaderStorageBlocks, 0);
EXPECT_EQ(maliLikeCaps.MaxGeometryShaderStorageBlocks, 0);
EXPECT_EQ(maliLikeCaps.MaxFragmentShaderStorageBlocks, 16);
}
// A rejected query must leave no error behind for the application's first glGetError to find,
// and must fall back to the spec minimums rather than to whatever the untouched out-param held.
TEST(PerStageStorageBlockCapabilities, ARejectedQueryIsDrainedAndFallsBackToTheSpecMinimums) {
const auto funcs = MakeFakeGLESFunctions();
ResetFakeDriver();
g_fake.perStageSsboBlockQueryRaisesError = true;
g_fake.maxVertexSsboBlocks = 12;
g_fake.maxFragmentSsboBlocks = 12;
MobileGL::MG_External::GLESCapabilities caps;
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(caps, funcs));
EXPECT_EQ(caps.MaxVertexShaderStorageBlocks, 0);
EXPECT_EQ(caps.MaxFragmentShaderStorageBlocks, 4);
EXPECT_EQ(g_fake.pendingError, static_cast<GLenum>(GL_NO_ERROR));
}
TEST(FragmentInterpolationCapabilities, QueriesOnlyWhenSupportedAndPreservesDriverLimits) {
const auto funcs = MakeFakeGLESFunctions();
@@ -716,6 +716,177 @@ void main() {
EXPECT_EQ(TakeError(), GL_INVALID_ENUM);
}
// Two counters that share a binding AND an offset must fail to link. glslang's own check
// lives in fixOffset(), which the Vulkan-relaxed parse never reaches - it folds the
// atomic_uint into a storage block and returns from declareVariable() first - so the pair
// used to link cleanly and then increment the same four bytes.
TEST_F(ProgramInterfaceTest, OverlappingAtomicCounterOffsetsFailToLink) {
const char* fs = R"(#version 430
out vec4 color;
layout (binding = 0, offset = 0) uniform atomic_uint a;
layout (binding = 0, offset = 0) uniform atomic_uint b;
void main() { color = vec4(float(atomicCounterIncrement(a) + atomicCounterIncrement(b))); }
)";
const GLuint p = MakeProgram(kSimpleVs, fs);
LinkProgram(p);
GLint status = -1;
GetProgramiv(p, GL_LINK_STATUS, &status);
EXPECT_EQ(status, GL_FALSE);
char log[4096] = "";
GetProgramInfoLog(p, sizeof(log), nullptr, log);
EXPECT_NE(std::string(log).find("overlap"), std::string::npos) << "info log was: " << log;
ClearErrors();
// Distinct offsets at one binding, and the same offset at two different bindings, are
// both legal and must still link - a check keyed any wider would reject them.
const char* legalFs = R"(#version 430
out vec4 color;
layout (binding = 0, offset = 0) uniform atomic_uint a;
layout (binding = 0, offset = 4) uniform atomic_uint b;
layout (binding = 1, offset = 0) uniform atomic_uint c;
void main() {
color = vec4(float(atomicCounterIncrement(a) + atomicCounterIncrement(b) + atomicCounterIncrement(c)));
}
)";
const GLuint legal = MakeProgram(kSimpleVs, legalFs);
LinkProgram(legal);
ExpectLinked(legal);
ClearErrors();
}
// GL 4.6 core 7.6 fails the link when a stage's active image uniforms exceed
// GL_MAX_*_IMAGE_UNIFORMS, or when their sum exceeds GL_MAX_COMBINED_IMAGE_UNIFORMS. Nothing
// counted them - glslang keeps those numbers only so gl_Max*ImageUniforms can expand from
// them - so every deliberately-oversized program in
// KHR-GL4x.shader_image_load_store.uniform-limits linked cleanly and then rendered nothing.
//
// Sized off the ADVERTISED limits rather than a constant, because the numbers come from the
// active backend and the whole point of the check is that the two agree.
TEST_F(ProgramInterfaceTest, ImageUniformsOverAStageLimitFailToLink) {
GLint maxFragmentImages = 0;
GLint maxCombinedImages = 0;
GetIntegerv(GL_MAX_FRAGMENT_IMAGE_UNIFORMS, &maxFragmentImages);
GetIntegerv(GL_MAX_COMBINED_IMAGE_UNIFORMS, &maxCombinedImages);
ClearErrors();
ASSERT_GT(maxFragmentImages, 0);
// The fragment stage is compiled explicitly so a COMPILE failure can never be mistaken
// for the link failure under test.
const auto linkWithFragmentImages = [](GLint count) {
const std::string n = std::to_string(count);
const std::string source = std::string(R"(#version 430
out vec4 color;
layout(r32i) uniform iimage2D u_image[)") + n + R"(];
void main() {
int value = 1;
for (int i = 0; i < )" + n + R"(; ++i) {
value = imageAtomicAdd(u_image[i], ivec2(0), value);
}
color = vec4(float(value));
}
)";
const char* sourcePtr = source.c_str();
const GLuint fs = CreateShader(GL_FRAGMENT_SHADER);
ShaderSource(fs, 1, &sourcePtr, nullptr);
CompileShader(fs);
GLint compiled = 0;
GetShaderiv(fs, GL_COMPILE_STATUS, &compiled);
EXPECT_EQ(compiled, GL_TRUE) << "the fragment stage with " << count << " image uniforms must compile";
const GLuint vs = CreateShader(GL_VERTEX_SHADER);
ShaderSource(vs, 1, &kSimpleVs, nullptr);
CompileShader(vs);
const GLuint program = CreateProgram();
AttachShader(program, vs);
AttachShader(program, fs);
LinkProgram(program);
return program;
};
const GLuint over = linkWithFragmentImages(maxFragmentImages + 1);
GLint status = -1;
GetProgramiv(over, GL_LINK_STATUS, &status);
EXPECT_EQ(status, GL_FALSE);
char log[4096] = "";
GetProgramInfoLog(over, sizeof(log), nullptr, log);
EXPECT_NE(std::string(log).find("GL_MAX_FRAGMENT_IMAGE_UNIFORMS"), std::string::npos)
<< "info log was: " << log;
ClearErrors();
// Exactly AT the limit is legal and must still link: the comparison is strictly
// greater-than, and the conformance suite's combined-stage subcase builds a program that
// fills every stage to its own limit and expects it to link whenever the combined limit
// can hold them.
if (maxFragmentImages <= maxCombinedImages) {
const GLuint atLimit = linkWithFragmentImages(maxFragmentImages);
ExpectLinked(atLimit);
ClearErrors();
}
}
// glGetProgramiv(GL_ACTIVE_ATOMIC_COUNTER_BUFFERS) and glGetActiveAtomicCounterBufferiv are
// the pre-4.3 spelling of the interface above, and the spec requires the two to agree.
// Neither did: the first counted glslang's atomic counter UNIFORMS - zero, because the
// relaxed parse folds every atomic_uint into a storage block before reflection runs - and
// the second was a stub that wrote nothing and raised nothing.
TEST_F(ProgramInterfaceTest, ActiveAtomicCounterBufferQueriesMatchTheInterface) {
const char* fs = R"(#version 430
out vec4 color;
layout (binding = 1, offset = 0) uniform atomic_uint a;
layout (binding = 2, offset = 0) uniform atomic_uint b;
layout (binding = 2, offset = 4) uniform atomic_uint c;
void main() {
color = vec4(float(atomicCounterIncrement(a) + atomicCounterIncrement(b) + atomicCounterIncrement(c)));
}
)";
const GLuint p = MakeProgram(kSimpleVs, fs);
LinkProgram(p);
ExpectLinked(p);
ClearErrors();
GLint bufferCount = -12345;
GetProgramiv(p, GL_ACTIVE_ATOMIC_COUNTER_BUFFERS, &bufferCount);
EXPECT_EQ(bufferCount, Interfaceiv(p, GL_ATOMIC_COUNTER_BUFFER, GL_ACTIVE_RESOURCES));
ASSERT_EQ(bufferCount, 2);
const auto activeBufferiv = [p](GLuint index, GLenum pname) {
GLint value = -12345;
GetActiveAtomicCounterBufferiv(p, index, pname, &value);
return value;
};
for (GLuint index = 0; index < static_cast<GLuint>(bufferCount); ++index) {
const std::vector<GLint> viaInterface =
Props(p, GL_ATOMIC_COUNTER_BUFFER, index,
{GL_BUFFER_BINDING, GL_BUFFER_DATA_SIZE, GL_NUM_ACTIVE_VARIABLES,
GL_REFERENCED_BY_VERTEX_SHADER, GL_REFERENCED_BY_FRAGMENT_SHADER});
ASSERT_EQ(viaInterface.size(), 5u);
EXPECT_EQ(activeBufferiv(index, GL_ATOMIC_COUNTER_BUFFER_BINDING), viaInterface[0]);
EXPECT_EQ(activeBufferiv(index, GL_ATOMIC_COUNTER_BUFFER_DATA_SIZE), viaInterface[1]);
EXPECT_EQ(activeBufferiv(index, GL_ATOMIC_COUNTER_BUFFER_ACTIVE_ATOMIC_COUNTERS), viaInterface[2]);
EXPECT_EQ(activeBufferiv(index, GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_VERTEX_SHADER), viaInterface[3]);
EXPECT_EQ(activeBufferiv(index, GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_FRAGMENT_SHADER), viaInterface[4]);
// The counter indices are the GL_UNIFORM indices, in the same order.
const std::vector<GLint> expectedIndices = Props(p, GL_ATOMIC_COUNTER_BUFFER, index, {GL_ACTIVE_VARIABLES});
ASSERT_FALSE(expectedIndices.empty());
std::vector<GLint> indices(expectedIndices.size(), -12345);
GetActiveAtomicCounterBufferiv(p, index, GL_ATOMIC_COUNTER_BUFFER_ACTIVE_ATOMIC_COUNTER_INDICES,
indices.data());
EXPECT_EQ(indices, expectedIndices);
}
EXPECT_EQ(TakeError(), GL_NO_ERROR);
GLint sink = -12345;
GetActiveAtomicCounterBufferiv(p, static_cast<GLuint>(bufferCount), GL_ATOMIC_COUNTER_BUFFER_BINDING, &sink);
EXPECT_EQ(TakeError(), GL_INVALID_VALUE);
EXPECT_EQ(sink, -12345) << "a rejected query must not write the caller's output";
// The interface-query spelling of the same property is NOT accepted here.
GetActiveAtomicCounterBufferiv(p, 0, GL_BUFFER_BINDING, &sink);
EXPECT_EQ(TakeError(), GL_INVALID_ENUM);
EXPECT_EQ(sink, -12345);
}
// --------------------------------------------------------- transform-feedback ------
TEST_F(ProgramInterfaceTest, TransformFeedbackVaryingTypes) {
const char* vs = R"(#version 430
+73
View File
@@ -3239,3 +3239,76 @@ TEST_F(ProgramTest, CreateShaderAndCreateShaderProgramvReportTheRightErrorClasse
EXPECT_NE(program, 0u);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// ARB_explicit_uniform_location / GL 4.6 core 7.6.1: a `layout(location = N)` uniform reserves N
// EVEN WHEN IT IS INACTIVE. Dead default-block uniforms are correctly filtered off the GL surface
// (glGetUniformLocation must answer -1 for them), but the implicit allocator used to walk straight
// over the location they claimed and hand it to a uniform that never asked for it
// (KHR-GL43.explicit_uniform_location.uniform-loc-mix-with-implicit3).
TEST_F(ProgramTest, InactiveExplicitUniformLocationIsStillReserved) {
const char* vsSource = R"(#version 430 core
layout(location = 2) uniform vec4 uDeadAtTwo;
uniform vec4 uA;
uniform vec4 uB;
uniform vec4 uC;
uniform vec4 uD;
void main() { gl_Position = uA + uB + uC + uD; }
)";
const char* fsSource = R"(#version 430 core
out vec4 fragColor;
void main() { fragColor = vec4(1.0); }
)";
const GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, vsSource);
const GLuint fs = CompileShaderChecked(GL_FRAGMENT_SHADER, fsSource);
const GLuint program = LinkVsFs(vs, fs, GL_TRUE);
// Reserving a location must not resurrect the uniform: it is still inactive to GL.
EXPECT_EQ(GetUniformLocation(program, "uDeadAtTwo"), -1);
for (const char* name : {"uA", "uB", "uC", "uD"}) {
const GLint location = GetUniformLocation(program, name);
EXPECT_GE(location, 0) << name << " lost its implicit location";
EXPECT_NE(location, 2) << name << " was handed the location uDeadAtTwo reserved";
}
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// The GL_MAX_UNIFORM_LOCATIONS boundary, from both sides. MAX_UNIFORM_LOCATIONS - 1 is the LAST
// LEGAL location: it has to link and read back verbatim
// (KHR-GL43.explicit_uniform_location.uniform-loc-max), which is only true while the advertised
// value and what the link accepts are the SAME number - the getter used to advertise one more
// location than any shader could name.
//
// The over-the-ceiling half is asserted through an ARRAY, because that is the only spelling the
// link gets to judge: a bare `layout(location = MAX)` is already a compile error inside glslang
// ("location is too large"), while an array's base compiles fine and only its last element passes
// the ceiling (...uniform-loc-negative-link-max-num-of-locations).
TEST_F(ProgramTest, ExplicitUniformLocationsHonourMaxUniformLocations) {
GLint maxLocations = 0;
GetIntegerv(GL_MAX_UNIFORM_LOCATIONS, &maxLocations);
ASSERT_GE(maxLocations, 1024) << "GL 4.3 requires at least 1024 uniform locations";
const char* fsSource = R"(#version 430 core
out vec4 fragColor;
void main() { fragColor = vec4(1.0); }
)";
const GLuint fs = CompileShaderChecked(GL_FRAGMENT_SHADER, fsSource);
{
const String source = String("#version 430 core\nlayout(location = ") +
std::to_string(maxLocations - 1) +
") uniform vec4 uAtLimit;\nvoid main() { gl_Position = uAtLimit; }\n";
const GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, source.c_str());
const GLuint program = LinkVsFs(vs, fs, GL_TRUE);
EXPECT_EQ(GetUniformLocation(program, "uAtLimit"), maxLocations - 1)
<< "the last location in the pool is legal and must come back verbatim";
}
{
const String source = String("#version 430 core\nlayout(location = ") +
std::to_string(maxLocations - 4) +
") uniform vec4 uSpill[8];\nvoid main() { gl_Position = uSpill[0]; }\n";
const GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, source.c_str());
(void)LinkVsFs(vs, fs, GL_FALSE);
}
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
@@ -3840,3 +3840,228 @@ TEST_F(ProgramUtilTest, EsslCoreImageFormatSetIsTheThirteenTheSpecLists) {
EXPECT_FALSE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(0x8051 /*GL_RGB8*/));
EXPECT_FALSE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(0 /*GL_NONE*/));
}
// KHR-GL43.shader_storage_buffer_object.basic-syntax iteration 6. glslang assigns a block's member
// offsets at DECLARATION time, where a member array that is still unsized contributes zero bytes -
// so `vec4 position01[]; vec4 position2;` put both members at offset 0 and the shader read
// position01[0] where it asked for position2. The preprocessor sizes the non-final member from the
// largest constant index the source uses, which is what the language says it means.
TEST_F(ProgramUtilTest, ANonFinalUnsizedBufferBlockMemberIsSizedFromItsLargestConstantIndex) {
using namespace MG_Util::ShaderTranspiler;
String source = R"(#version 430 core
layout(packed) coherent buffer Buffer {
vec4 position01[];
vec4 position2;
} g_buffer;
void main() {
if (gl_VertexID == 0) gl_Position = g_buffer.position01[0];
else if (gl_VertexID == 1) gl_Position = g_buffer.position01[1];
else if (gl_VertexID == 2) gl_Position = g_buffer.position2;
}
)";
PreprocessShaderSource(ShaderStage::Vertex, source);
EXPECT_NE(source.find("vec4 position01[2];"), String::npos) << source;
EXPECT_EQ(source.find("position01[];"), String::npos) << source;
// The LAST member of a storage block is a run-time sized array, which is legal and already
// laid out correctly - sizing it would be a wire-format change, not a repair.
String lastMember = R"(#version 430 core
buffer Buffer {
vec4 head;
vec4 tail[];
} g_buffer;
void main() {
gl_Position = g_buffer.tail[0] + g_buffer.tail[3];
}
)";
PreprocessShaderSource(ShaderStage::Vertex, lastMember);
EXPECT_NE(lastMember.find("vec4 tail[];"), String::npos) << lastMember;
// A member the shader subscripts with anything but a literal cannot be sized from the source,
// so it is left exactly as it was.
String dynamicIndex = R"(#version 430 core
buffer Buffer {
vec4 head[];
vec4 tail;
} g_buffer;
uniform int g_index;
void main() {
gl_Position = g_buffer.head[g_index] + g_buffer.tail;
}
)";
PreprocessShaderSource(ShaderStage::Vertex, dynamicIndex);
EXPECT_NE(dynamicIndex.find("vec4 head[];"), String::npos) << dynamicIndex;
// `buffer` is also a member memory qualifier; a declaration that uses it must not be mistaken
// for a block header.
String memberQualifier = R"(#version 430 core
coherent buffer Buffer {
buffer vec4 position0;
vec4 position1[];
vec4 position2;
} g_buffer;
void main() {
gl_Position = g_buffer.position0 + g_buffer.position1[2] + g_buffer.position2;
}
)";
PreprocessShaderSource(ShaderStage::Vertex, memberQualifier);
EXPECT_NE(memberQualifier.find("vec4 position1[3];"), String::npos) << memberQualifier;
}
// KHR-GL43.shader_storage_buffer_object.negative-glsl-compileTime: a storage block declared at
// GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS must fail to compile, and so must an arrayed one whose
// LAST element passes the ceiling. The relaxed Vulkan-rules parse enforces neither.
TEST_F(ProgramUtilTest, StorageBlockBindingCeilingIsCheckedAtItsExactBoundary) {
using namespace MG_Util::ShaderTranspiler;
constexpr Int kMaxBindings = 36;
const auto violation = [](const String& body) {
return FindShaderStorageBindingViolation("#version 430 core\n" + body + "void main() {}\n", kMaxBindings);
};
// The boundary itself: max - 1 is the last legal point, max is one past it.
EXPECT_FALSE(violation("layout(binding = 35) buffer Buffer { int x; };\n").has_value());
EXPECT_TRUE(violation("layout(binding = 36) buffer Buffer { int x; };\n").has_value());
// An instance array takes CONSECUTIVE points, so what has to fit is base + count - 1.
EXPECT_FALSE(violation("layout(binding = 32) buffer Buffer { int x; } g_array[4];\n").has_value());
EXPECT_TRUE(violation("layout(binding = 34) buffer Buffer { int x; } g_array[4];\n").has_value());
// Qualifiers and a second layout list may sit between the binding and the keyword.
EXPECT_TRUE(violation("layout(std430) layout(binding = 36) coherent restrict buffer B { int x; };\n")
.has_value());
// Things the scanner must NOT judge: a uniform block (a different ceiling), a storage block
// with no explicit binding, the bare default-qualifier form, and an instance array whose size
// is not a literal.
EXPECT_FALSE(violation("layout(binding = 40) uniform Block { int x; };\n"
"layout(binding = 0) buffer Buffer { int y; };\n")
.has_value());
EXPECT_FALSE(violation("buffer Buffer { int x; };\nconst int binding = 40;\n").has_value());
EXPECT_FALSE(violation("layout(binding = 1) buffer;\nbuffer Buffer { int x; };\n").has_value());
EXPECT_FALSE(violation("const int kCount = 4;\nlayout(binding = 34) buffer B { int x; } g[kCount];\n")
.has_value());
// A backend that advertises no binding points has no ceiling to enforce.
EXPECT_FALSE(FindShaderStorageBindingViolation("layout(binding = 36) buffer B { int x; };\n", 0).has_value());
}
// KHR-GL43.explicit_uniform_location.uniform-loc-nondecimal: GLSL integer literals are C-style, so
// layout(location = 0xA) is 10 and layout(location = 010) is OCTAL 8. The extractor used to accept
// a base-10 digit run and nothing else: the hex spelling failed the test entirely and the
// declaration silently lost its explicit location, while the octal one was read as decimal 10.
// The identical defect sat on every array dimension and on layout(binding = N).
TEST_F(ProgramUtilTest, ExtractExplicitUniformLocationsReadsNonDecimalIntegerLiterals) {
using namespace MG_Util::ShaderTranspiler;
const String source = R"(#version 430 core
layout(location = 0xA) uniform vec4 hexLower;
layout(location = 0X1f) uniform vec4 hexUpper;
layout(location = 010) uniform vec4 octal;
layout(location = 3u) uniform vec4 unsignedSuffix;
layout(location = 0x2) uniform float hexArray[0x3];
layout(location = 1.0) uniform vec4 notAnInteger;
layout(location = 7f) uniform vec4 unknownSuffix;
void main() {}
)";
const UnorderedMap<String, Int> locations = ExtractExplicitUniformLocations(source);
ASSERT_EQ(locations.count("hexLower"), 1u);
EXPECT_EQ(locations.at("hexLower"), 10);
ASSERT_EQ(locations.count("hexUpper"), 1u);
EXPECT_EQ(locations.at("hexUpper"), 31);
ASSERT_EQ(locations.count("octal"), 1u);
EXPECT_EQ(locations.at("octal"), 8) << "a leading zero is octal in GLSL, not decimal";
ASSERT_EQ(locations.count("unsignedSuffix"), 1u);
EXPECT_EQ(locations.at("unsignedSuffix"), 3);
ASSERT_EQ(locations.count("hexArray"), 1u);
EXPECT_EQ(locations.at("hexArray"), 2);
// Still never guessed at: a float and an unknown suffix are skipped, not rounded.
EXPECT_EQ(locations.count("notAnInteger"), 0u);
EXPECT_EQ(locations.count("unknownSuffix"), 0u);
}
// A hexadecimal array dimension has to size the declarator's span too, or the declarator after it
// in the same statement starts at the wrong location.
TEST_F(ProgramUtilTest, ExtractExplicitUniformLocationsSpansANonDecimalArrayDimension) {
using namespace MG_Util::ShaderTranspiler;
const UnorderedMap<String, Int> locations = ExtractExplicitUniformLocations(
"#version 430 core\nlayout(location = 50) uniform float first[0x3], second;\nvoid main() {}\n");
ASSERT_EQ(locations.count("first"), 1u);
EXPECT_EQ(locations.at("first"), 50);
ASSERT_EQ(locations.count("second"), 1u);
EXPECT_EQ(locations.at("second"), 53) << "0x3 is three elements, not zero and not three hundred";
}
// KHR-GL43.explicit_uniform_location.uniform-loc-array-of-arrays: glslang reflects
// `float u[2][3]` as "u[0][0]" and "u[1][0]", and the linker resolves such a name by stripping the
// single trailing "[0]" - so the map has to answer "u[1]", not just "u". Without the pre-flattened
// keys both records missed the map entirely and were first-fitted from location 0.
TEST_F(ProgramUtilTest, ExtractExplicitUniformLocationsExpandsArrayOfArraysElements) {
using namespace MG_Util::ShaderTranspiler;
const String source = R"(#version 430 core
layout(location = 2) uniform float two_d[2][3];
layout(location = 20) uniform float three_d[2][2][4];
layout(location = 40) uniform float one_d[3];
void main() {}
)";
const UnorderedMap<String, Int> locations = ExtractExplicitUniformLocations(source);
// The root entry is unchanged - the synthesized keys are additional, never a replacement.
ASSERT_EQ(locations.count("two_d"), 1u);
EXPECT_EQ(locations.at("two_d"), 2);
// One key per outer index, each starting a run of the innermost dimension (3 here).
ASSERT_EQ(locations.count("two_d[0]"), 1u);
EXPECT_EQ(locations.at("two_d[0]"), 2);
ASSERT_EQ(locations.count("two_d[1]"), 1u);
EXPECT_EQ(locations.at("two_d[1]"), 5);
// Three dimensions: glslang expands all but the innermost, so both outer indices are spelled.
ASSERT_EQ(locations.count("three_d"), 1u);
EXPECT_EQ(locations.at("three_d"), 20);
ASSERT_EQ(locations.count("three_d[0][0]"), 1u);
EXPECT_EQ(locations.at("three_d[0][0]"), 20);
ASSERT_EQ(locations.count("three_d[0][1]"), 1u);
EXPECT_EQ(locations.at("three_d[0][1]"), 24);
ASSERT_EQ(locations.count("three_d[1][0]"), 1u);
EXPECT_EQ(locations.at("three_d[1][0]"), 28);
ASSERT_EQ(locations.count("three_d[1][1]"), 1u);
EXPECT_EQ(locations.at("three_d[1][1]"), 32);
// A 1-D array needs no expansion: stripping "[0]" already reaches the root.
ASSERT_EQ(locations.count("one_d"), 1u);
EXPECT_EQ(locations.at("one_d"), 40);
EXPECT_EQ(locations.count("one_d[0]"), 0u);
// The declarator after an array-of-arrays still advances by the WHOLE element count.
const UnorderedMap<String, Int> pair = ExtractExplicitUniformLocations(
"#version 430 core\nlayout(location = 0) uniform float a[2][3], b;\nvoid main() {}\n");
ASSERT_EQ(pair.count("b"), 1u);
EXPECT_EQ(pair.at("b"), 6);
}
// KHR-GL43.explicit_uniform_location: layout(binding = 0x2) on a sampler is the same literal defect
// as the location one, and losing it costs the sampler its initial texture unit.
TEST_F(ProgramUtilTest, ExtractExplicitOpaqueBindingsReadsNonDecimalIntegerLiterals) {
using namespace MG_Util::ShaderTranspiler;
const String source = R"(#version 430 core
layout(binding = 0x2) uniform sampler2D hexUnit;
layout(binding = 012) uniform sampler2D octalUnit;
layout(binding = 1u) uniform sampler2D suffixedUnit;
void main() {}
)";
const UnorderedMap<String, Uint> bindings = ExtractExplicitOpaqueBindings(source);
ASSERT_EQ(bindings.count("hexUnit"), 1u);
EXPECT_EQ(bindings.at("hexUnit"), 2u);
ASSERT_EQ(bindings.count("octalUnit"), 1u);
EXPECT_EQ(bindings.at("octalUnit"), 10u) << "012 is octal ten, not twelve";
ASSERT_EQ(bindings.count("suffixedUnit"), 1u);
EXPECT_EQ(bindings.at("suffixedUnit"), 1u);
}
+125
View File
@@ -140,6 +140,29 @@ namespace {
void StubEndXfbPrimitivesQuery(MG_Backend::BackendQueryHandle) { ++g_stubXfbEndCount; }
// Stub backend occlusion queries. The host has no ES context, and BeginQuery refuses the
// occlusion targets outright when the backend advertises no hook - so a conditional-render
// test cannot get a legal predicate object without these. g_stubResultNs is the sample count
// the "driver" reports, which is the whole input to the predicate.
MG_Backend::BackendQueryHandle StubBeginOcclusionQuery() {
return reinterpret_cast<MG_Backend::BackendQueryHandle>(static_cast<uintptr_t>(0x54));
}
void StubEndOcclusionQuery(MG_Backend::BackendQueryHandle) {}
void InstallStubBackendOcclusionQueries() {
auto& backendGL = MG_Backend::gBackendFunctionsTable.GL;
backendGL.BeginOcclusionQuery = StubBeginOcclusionQuery;
backendGL.EndOcclusionQuery = StubEndOcclusionQuery;
backendGL.IsQueryResultAvailable = StubIsQueryResultAvailable;
backendGL.GetQueryResult64 = StubGetQueryResult64;
backendGL.DeleteBackendQuery = StubDeleteBackendQuery;
g_stubDeleteCount = 0;
g_stubResultAvailable = true;
g_stubResultObtainable = true;
g_stubResultNs = 0;
}
void InstallStubBackendXfbQueries() {
auto& backendGL = MG_Backend::gBackendFunctionsTable.GL;
backendGL.BeginXfbPrimitivesQuery = StubBeginXfbPrimitivesQuery;
@@ -677,6 +700,108 @@ TEST_F(QueryTest, PrimitivesGeneratedKeepsTheBackendResultUnderTheCpuPreference)
// unified truthy rule (set, non-empty, not "0", case-insensitive not "false").
// Running the binary under MOBILEGL_DISABLE_TIMERQUERY=1 therefore exercises
// the real end-to-end path rather than the struct field alone.
// KHR-GL43.compute_shader.conditional-dispatching and the conditional_render family.
// glBeginConditionalRender/glEndConditionalRender were bare stubs: every command inside a
// conditional block executed whatever the query said, so the block that should have been
// discarded ran and doubled the atomic counter the case reads back.
TEST_F(QueryTest, ConditionalRenderResolvesItsPredicateFromTheOcclusionQuery) {
ScopedBackendFunctionsOverride backendGuard;
InstallStubBackendOcclusionQueries();
GLuint ids[2] = {0, 0};
MG_Impl::GLImpl::GenQueries(2, ids);
ASSERT_NE(ids[0], 0u);
ASSERT_NE(ids[1], 0u);
// One span that saw samples and one that saw none, which is exactly the pair the
// conformance case builds out of a passing and a failing depth test.
g_stubResultNs = 1;
MG_Impl::GLImpl::BeginQuery(GL_ANY_SAMPLES_PASSED, ids[0]);
MG_Impl::GLImpl::EndQuery(GL_ANY_SAMPLES_PASSED);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
GLuint passedResult = 0xFFFFFFFFu;
MG_Impl::GLImpl::GetQueryObjectuiv(ids[0], GL_QUERY_RESULT, &passedResult);
ASSERT_EQ(passedResult, 1u);
g_stubResultNs = 0;
MG_Impl::GLImpl::BeginQuery(GL_ANY_SAMPLES_PASSED, ids[1]);
MG_Impl::GLImpl::EndQuery(GL_ANY_SAMPLES_PASSED);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// A block on the query that passed executes.
MG_Impl::GLImpl::BeginConditionalRender(ids[0], GL_QUERY_WAIT);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
EXPECT_TRUE(MG_State::pGLContext->IsConditionalRenderActive());
EXPECT_FALSE(MG_State::pGLContext->ConditionalRenderDiscardsCommands());
MG_Impl::GLImpl::EndConditionalRender();
EXPECT_FALSE(MG_State::pGLContext->IsConditionalRenderActive());
EXPECT_FALSE(MG_State::pGLContext->ConditionalRenderDiscardsCommands());
// A block on the query that did not passes nothing through.
MG_Impl::GLImpl::BeginConditionalRender(ids[1], GL_QUERY_WAIT);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
EXPECT_TRUE(MG_State::pGLContext->ConditionalRenderDiscardsCommands());
MG_Impl::GLImpl::EndConditionalRender();
// ...and the _INVERTED modes swap both verdicts.
MG_Impl::GLImpl::BeginConditionalRender(ids[0], GL_QUERY_WAIT_INVERTED);
EXPECT_TRUE(MG_State::pGLContext->ConditionalRenderDiscardsCommands());
MG_Impl::GLImpl::EndConditionalRender();
MG_Impl::GLImpl::BeginConditionalRender(ids[1], GL_QUERY_BY_REGION_NO_WAIT_INVERTED);
EXPECT_FALSE(MG_State::pGLContext->ConditionalRenderDiscardsCommands());
MG_Impl::GLImpl::EndConditionalRender();
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::DeleteQueries(2, ids);
}
TEST_F(QueryTest, ConditionalRenderRejectsTheErrorsTheSpecNames) {
ScopedBackendFunctionsOverride backendGuard;
InstallStubBackendOcclusionQueries();
GLuint ids[2] = {0, 0};
MG_Impl::GLImpl::GenQueries(2, ids);
g_stubResultNs = 1;
MG_Impl::GLImpl::BeginQuery(GL_ANY_SAMPLES_PASSED, ids[0]);
MG_Impl::GLImpl::EndQuery(GL_ANY_SAMPLES_PASSED);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// GL 4.6 core 10.9, one rule at a time.
MG_Impl::GLImpl::BeginConditionalRender(ids[0], GL_TIME_ELAPSED);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_ENUM);
EXPECT_FALSE(MG_State::pGLContext->IsConditionalRenderActive());
// A generated NAME is not yet a query object.
MG_Impl::GLImpl::BeginConditionalRender(ids[1], GL_QUERY_WAIT);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_VALUE);
MG_Impl::GLImpl::BeginConditionalRender(0, GL_QUERY_WAIT);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_VALUE);
// A query that is not an occlusion query cannot drive one.
GLuint timerId = 0;
MG_Impl::GLImpl::GenQueries(1, &timerId);
MG_Impl::GLImpl::BeginQuery(GL_TIME_ELAPSED, timerId);
MG_Impl::GLImpl::EndQuery(GL_TIME_ELAPSED);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::BeginConditionalRender(timerId, GL_QUERY_WAIT);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
// End without a block, and a nested Begin.
MG_Impl::GLImpl::EndConditionalRender();
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
MG_Impl::GLImpl::BeginConditionalRender(ids[0], GL_QUERY_WAIT);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::BeginConditionalRender(ids[0], GL_QUERY_WAIT);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
// The rejected nested Begin must not have disturbed the open block.
EXPECT_EQ(MG_State::pGLContext->GetConditionalRenderQuery(), ids[0]);
MG_Impl::GLImpl::EndConditionalRender();
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::DeleteQueries(2, ids);
MG_Impl::GLImpl::DeleteQueries(1, &timerId);
}
TEST_F(QueryTest, DisableTimerQueryFeatureMatchesEnvironment) {
const char* raw = std::getenv("MOBILEGL_DISABLE_TIMERQUERY");
Bool expected = false;
+172
View File
@@ -17,6 +17,7 @@
#include <MG_Backend/DirectGLES/Managers.h>
#include <MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h>
#include <MG_Backend/BackendObjects.h>
#include <MG_Impl/GLImpl/Buffer/GL_Buffer.h>
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
#include <MG_Impl/GLImpl/RenderState/GL_RenderState.h>
#include <MG_Impl/GLImpl/Texture/GL_Texture.h>
@@ -365,6 +366,13 @@ TEST(DirectGLESSanity, RebasesInstanceIdWhenIndirectDrawsLeakBaseInstance) {
// MaxShaderStorageBufferBindings - 1 = 12, so a regression that stops reading the
// probed cap and falls back to the struct default would surface as "binding = 7".
caps.MaxShaderStorageBufferBindings = 13;
// The indirect lowering reads its baseInstance through a storage block declared in the
// VERTEX stage, which is optional in both APIs and which the GLESCapabilities default
// (0, the spec minimum) therefore denies. This suite is pinning the shape of that
// lowering, so it has to describe a driver that can actually have it - see
// VertexStageStorageBlockUsable and the BaseInstanceInjectionGate suite for the
// zero case.
caps.MaxVertexShaderStorageBlocks = 1;
const MobileGL::String source = R"(#version 310 es
highp int mg_BaseInstanceLowered;
@@ -403,6 +411,9 @@ TEST(DirectGLESSanity, TheIndirectWordIndexIsOneBasedSoItsUnwrittenValueMeansNot
auto& caps = MobileGL::MG_Backend::DirectGLES::g_GLESCapabilities;
caps.IndirectDrawInstanceIdIncludesBaseInstance = false;
caps.MaxShaderStorageBufferBindings = 13;
// See RebasesInstanceIdWhenIndirectDrawsLeakBaseInstance: without a vertex-stage
// storage block there is no word index to be one-based about.
caps.MaxVertexShaderStorageBlocks = 1;
const MobileGL::String source = R"(#version 310 es
highp int mg_BaseInstanceLowered;
@@ -428,6 +439,10 @@ TEST(DirectGLESSanity, KeepsInstanceIdWhenIndirectDrawsAreConforming) {
auto& caps = MobileGL::MG_Backend::DirectGLES::g_GLESCapabilities;
caps.IndirectDrawInstanceIdIncludesBaseInstance = false;
caps.MaxShaderStorageBufferBindings = 13;
// Set explicitly even though the assertions below would also hold on the degraded path:
// this case is about a CONFORMING driver leaving gl_InstanceID alone, and it would be a
// silent weakening for it to be exercising the no-storage-block fallback instead.
caps.MaxVertexShaderStorageBlocks = 1;
const MobileGL::String source = R"(#version 310 es
highp int mg_BaseInstanceLowered;
@@ -442,6 +457,8 @@ void main() {
EXPECT_EQ(rewritten.find("mg_ZeroBasedInstanceID"), MobileGL::String::npos);
EXPECT_NE(rewritten.find("int instance = gl_InstanceID + mg_BaseInstanceLowered;"), MobileGL::String::npos);
// The indirect view is present on this driver, so the fallback must NOT have fired.
EXPECT_NE(rewritten.find("buffer mg_IndirectParams"), MobileGL::String::npos);
}
TEST(DirectGLESSanity, LeavesDrawParameterGlobalsAloneOutsideVertexShaders) {
@@ -913,6 +930,161 @@ void main() {
MG_Backend::pActiveBackendObject.reset();
}
// KHR-GL43.shader_atomic_counters.basic-glsl-built-in, .basic-buffer-bind and .basic-api-get.
// The atomic-counter limits used to live in two unreconciled tables - glslang compiled every
// shader against ONE binding while glGetIntegerv advertised thirty-six - and three of the enums
// had no case in the getter at all, so the query raised INVALID_ENUM and left the caller reading
// whatever was in its own stack slot.
TEST(GetterSanity, AtomicCounterQueriesMatchShaderCompilerLimits) {
using namespace MobileGL;
namespace Transpiler = MG_Util::ShaderTranspiler;
auto previousContext = Move(MG_State::pGLContext);
auto previousBackend = Move(MG_Backend::pActiveBackendObject);
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
MG_Backend::pActiveBackendObject = MakeUnique<DynamicParameterBackend>(MG_Backend::DynamicBackendParameters{});
GLint reported = -1;
MG_Impl::GLImpl::GetIntegerv(GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS, &reported);
EXPECT_EQ(reported, static_cast<GLint>(Transpiler::MAX_ATOMIC_COUNTER_BUFFER_BINDINGS));
MG_Impl::GLImpl::GetIntegerv(GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE, &reported);
EXPECT_EQ(reported, static_cast<GLint>(Transpiler::MAX_ATOMIC_COUNTER_BUFFER_SIZE));
for (const GLenum pname : {GL_MAX_COMBINED_ATOMIC_COUNTER_BUFFERS, GL_MAX_FRAGMENT_ATOMIC_COUNTER_BUFFERS,
GL_MAX_COMPUTE_ATOMIC_COUNTER_BUFFERS}) {
reported = -1;
MG_Impl::GLImpl::GetIntegerv(pname, &reported);
EXPECT_EQ(reported, static_cast<GLint>(Transpiler::MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE))
<< "pname " << pname;
}
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// glBindBufferBase sets the GENERIC binding point too (GL 4.6 6.1.1), and this is the one
// indexed-buffer family whose non-indexed query had no case.
reported = -1;
MG_Impl::GLImpl::GetIntegerv(GL_ATOMIC_COUNTER_BUFFER_BINDING, &reported);
EXPECT_EQ(reported, 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
GLuint buffer = 0;
MG_Impl::GLImpl::GenBuffers(1, &buffer);
MG_Impl::GLImpl::BindBuffer(GL_ATOMIC_COUNTER_BUFFER, buffer);
MG_Impl::GLImpl::BufferData(GL_ATOMIC_COUNTER_BUFFER, 64, nullptr, GL_STATIC_DRAW);
MG_Impl::GLImpl::BindBufferBase(GL_ATOMIC_COUNTER_BUFFER, 2, buffer);
MG_Impl::GLImpl::GetIntegerv(GL_ATOMIC_COUNTER_BUFFER_BINDING, &reported);
EXPECT_EQ(static_cast<GLuint>(reported), buffer);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// The advertised ceiling is also the one glBindBufferBase and the indexed getter enforce.
// A limit nothing validates against is how these tables drifted apart in the first place:
// the binding-point ARRAY is 36 deep, and it used to be that number an application saw.
constexpr GLuint pastLastBinding = static_cast<GLuint>(Transpiler::MAX_ATOMIC_COUNTER_BUFFER_BINDINGS);
MG_Impl::GLImpl::BindBufferBase(GL_ATOMIC_COUNTER_BUFFER, pastLastBinding, buffer);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), static_cast<GLenum>(GL_INVALID_VALUE));
MG_Impl::GLImpl::GetIntegeri_v(GL_ATOMIC_COUNTER_BUFFER_BINDING, pastLastBinding, &reported);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), static_cast<GLenum>(GL_INVALID_VALUE));
// ...and the shading language has to expand the same numbers. Each array is sized by a
// built-in constant and indexed at its last element with a literal, so the stage only
// compiles when that constant is at least what glGetIntegerv just reported - which it was
// not while the resource table said one.
const String lastBinding = std::to_string(Transpiler::MAX_ATOMIC_COUNTER_BUFFER_BINDINGS - 1);
const String lastBuffer = std::to_string(Transpiler::MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE - 1);
const String source = R"(#version 430 core
out vec4 color;
int mgBindings[gl_MaxAtomicCounterBindings];
int mgCombinedBuffers[gl_MaxCombinedAtomicCounterBuffers];
int mgFragmentBuffers[gl_MaxFragmentAtomicCounterBuffers];
layout(binding = )" + lastBinding + R"(, offset = 0) uniform atomic_uint mgCounter;
void main() {
color = vec4(float(mgBindings[)" + lastBinding + R"(] + mgCombinedBuffers[)" + lastBuffer +
R"(] + mgFragmentBuffers[)" + lastBuffer + R"(] + int(atomicCounterIncrement(mgCounter))));
}
)";
auto compiled = MG_Util::ShaderTranspiler::ShaderCompiler::CompileShader({
.shaderType = GL_FRAGMENT_SHADER,
.sourceStr = source,
});
EXPECT_TRUE(compiled) << (compiled ? "" : compiled.error().log);
MG_Backend::pActiveBackendObject = Move(previousBackend);
MG_State::pGLContext = Move(previousContext);
}
// KHR-GL43.compute_shader.max: the test queries every GL_MAX_COMPUTE_* value through the API and
// then makes a compute shader compare the matching gl_MaxCompute* constant against it. The two
// used to be independent tables and gl_MaxComputeWorkGroupSize.z disagreed - glslang compiled
// against a permissive 1024 while the context advertises the 64 the GL 4.6 minimum (and every ES
// driver) reports.
TEST(GetterSanity, ComputeWorkGroupQueriesMatchShaderCompilerLimits) {
using namespace MobileGL;
auto previousContext = Move(MG_State::pGLContext);
auto previousBackend = Move(MG_Backend::pActiveBackendObject);
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
MG_Backend::pActiveBackendObject = MakeUnique<DynamicParameterBackend>(MG_Backend::DynamicBackendParameters{});
GLint size[3] = {0, 0, 0};
GLint count[3] = {0, 0, 0};
for (GLuint index = 0; index < 3; ++index) {
MG_Impl::GLImpl::GetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_SIZE, index, &size[index]);
MG_Impl::GLImpl::GetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_COUNT, index, &count[index]);
}
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// The compile runs against a captured env, exactly as the pipeline's does. That is the whole
// invariant: the env holds the same floored driver answer GetIntegeri_v just returned, so the
// resource table and the query agree BY CONSTRUCTION rather than by two tables happening to
// carry the same literals.
const auto env = MG_Util::ShaderTranspiler::CaptureCompileEnv();
for (GLuint index = 0; index < 3; ++index) {
EXPECT_EQ(static_cast<GLint>(env->maxComputeWorkGroupSize[index]), size[index]) << "index " << index;
EXPECT_EQ(static_cast<GLint>(env->maxComputeWorkGroupCount[index]), count[index]) << "index " << index;
}
// A negative array size is a compile error, so the stage only compiles when EVERY component
// of both built-in constants equals what the query above reported. Two-sided by construction:
// a resource table that is too permissive fails it exactly like one that is too tight.
const String source = R"(#version 430 core
layout(local_size_x = 1) in;
const int mgAgree = (gl_MaxComputeWorkGroupSize == ivec3()" +
std::to_string(size[0]) + ", " + std::to_string(size[1]) + ", " +
std::to_string(size[2]) + R"() &&
gl_MaxComputeWorkGroupCount == ivec3()" +
std::to_string(count[0]) + ", " + std::to_string(count[1]) + ", " +
std::to_string(count[2]) + R"()) ? 1 : -1;
int mgProbe[mgAgree];
void main() {
mgProbe[0] = 0;
}
)";
auto compiled = MG_Util::ShaderTranspiler::ShaderCompiler::CompileShader({
.shaderType = GL_COMPUTE_SHADER,
.sourceStr = source,
.env = env.get(),
});
EXPECT_TRUE(compiled) << (compiled ? "" : compiled.error().log);
// The z ceiling is also what glslang checks a declared local_size_z against, so it has to
// reject one invocation past the advertised limit and accept the limit itself.
const String atLimit = "#version 430 core\nlayout(local_size_z = " + std::to_string(size[2]) +
") in;\nvoid main() {}\n";
const String pastLimit = "#version 430 core\nlayout(local_size_z = " + std::to_string(size[2] + 1) +
") in;\nvoid main() {}\n";
EXPECT_TRUE(MG_Util::ShaderTranspiler::ShaderCompiler::CompileShader({
.shaderType = GL_COMPUTE_SHADER,
.sourceStr = atLimit,
.env = env.get(),
}));
EXPECT_FALSE(MG_Util::ShaderTranspiler::ShaderCompiler::CompileShader({
.shaderType = GL_COMPUTE_SHADER,
.sourceStr = pastLimit,
.env = env.get(),
}));
MG_Backend::pActiveBackendObject = Move(previousBackend);
MG_State::pGLContext = Move(previousContext);
}
TEST(GetterSanity, ReportsKhrSubgroupDynamicParameters) {
using namespace MobileGL;
@@ -544,14 +544,16 @@ namespace {
// (2) the advertised extension vector, including the fp64 flag's own extension
a.advertisedExtensions = {E_GL_ARB_gpu_shader_fp64, E_GL_KHR_debug};
b.advertisedExtensions = {};
// (3) the compute limits (ValidateComputeLocalSizeLimits only)
a.maxComputeWorkGroupSize[0] = 1024;
a.maxComputeWorkGroupSize[1] = 1024;
a.maxComputeWorkGroupSize[2] = 64;
// (3) the compute INVOCATION limit, and deliberately not the work-group size or
// count any more. Those two used to sit here on the grounds that
// ValidateComputeLocalSizeLimits was their only consumer; wave3 (cb155c5b) made
// BuildTBuiltInResource read them, and glslang expands both into built-in constants
// (gl_MaxComputeWorkGroupSize / gl_MaxComputeWorkGroupCount), so they are now
// front-end inputs and belong in TheFrontendFingerprintMovesWithEveryFrontendLimit
// instead - which is where they moved. The invocation limit is the one that really
// still stops at the pre-parse gate: glslang has no built-in constant for it and
// BuildTBuiltInResource does not read it.
a.maxComputeWorkGroupInvocations = 128;
b.maxComputeWorkGroupSize[0] = 2048;
b.maxComputeWorkGroupSize[1] = 2048;
b.maxComputeWorkGroupSize[2] = 1024;
b.maxComputeWorkGroupInvocations = 2048;
// (4) a spread of DynamicBackendParameters fields the front end never reads
a.params.MaxColorTextureSamples = 1;
@@ -598,8 +600,11 @@ TEST_F(TranslationCacheTest, TwoBackendsCompilingTheSameGlslShareOneL1Entry) {
}
// The other direction, one case per input that was KEPT. Each is a limit the front end
// really consumes - the seven BuildTBuiltInResource copies into TBuiltInResource, plus the
// really consumes - everything BuildTBuiltInResource copies into TBuiltInResource, plus the
// two inputs to the reflection vertex-attrib limit - so each must still split the key.
// KEEP THIS LIST IN STEP WITH BuildTBuiltInResource: a limit that becomes env-derived there
// and is not added here is a silent miscompile with no failing test to catch it, which is
// precisely how the compute work-group cases below arrived.
TEST_F(TranslationCacheTest, TheFrontendFingerprintMovesWithEveryFrontendLimit) {
const CompileEnv base;
const Uint64 baseline = ComputeFrontendCompileEnvFingerprint(base);
@@ -613,6 +618,21 @@ TEST_F(TranslationCacheTest, TheFrontendFingerprintMovesWithEveryFrontendLimit)
{"params.MaxComputeImageUniforms", [](CompileEnv& e) { e.params.MaxComputeImageUniforms += 1; }},
{"params.MaxCombinedImageUniforms", [](CompileEnv& e) { e.params.MaxCombinedImageUniforms += 1; }},
{"params.MaxVertexAttribs", [](CompileEnv& e) { e.params.MaxVertexAttribs += 1; }},
// Env-derived since wave3's cb155c5b: BuildTBuiltInResource copies all seven of
// these into TBuiltInResource, and glslang expands each into a built-in constant a
// compute shader can read (gl_MaxComputeTextureImageUnits,
// gl_MaxComputeWorkGroupSize, gl_MaxComputeWorkGroupCount). A module that reads one
// compiles to different SPIR-V under two different values, so each must split the
// key - one case per COMPONENT, because a per-axis difference is exactly the shape
// real drivers produce (z = 64 on ES against 1024 elsewhere).
{"params.MaxComputeTextureImageUnits",
[](CompileEnv& e) { e.params.MaxComputeTextureImageUnits += 1; }},
{"maxComputeWorkGroupSize[0]", [](CompileEnv& e) { e.maxComputeWorkGroupSize[0] += 1; }},
{"maxComputeWorkGroupSize[1]", [](CompileEnv& e) { e.maxComputeWorkGroupSize[1] += 1; }},
{"maxComputeWorkGroupSize[2]", [](CompileEnv& e) { e.maxComputeWorkGroupSize[2] += 1; }},
{"maxComputeWorkGroupCount[0]", [](CompileEnv& e) { e.maxComputeWorkGroupCount[0] += 1; }},
{"maxComputeWorkGroupCount[1]", [](CompileEnv& e) { e.maxComputeWorkGroupCount[1] += 1; }},
{"maxComputeWorkGroupCount[2]", [](CompileEnv& e) { e.maxComputeWorkGroupCount[2] += 1; }},
// HasBackend(): with no backend the reflection attrib limit falls back to the
// storage capacity rather than the driver's number, so the bit is load-bearing.
{"HasBackend", [](CompileEnv& e) { e.backend = BackendType::DirectGLES; }},
@@ -704,6 +724,44 @@ TEST_F(TranslationCacheTest, AProgramServedFromTheMemoAnswersTheWholeQuerySurfac
}
}
// glGetFragDataLocation on a program served from the memo.
//
// Split out from the case above because it caught a REAL bug that case did not: every
// accessor it checks had already been moved onto the owned reflection snapshot, but
// GetFragmentDataLocation still opened with `if (!Artifacts().program) return -1` and then
// walked the live TProgram's pipe outputs. On a hit there is no TProgram - that is the whole
// point of the memo - so the guard fired and the function reported "this program has no such
// fragment output" for an output that plainly exists. The failure mode was silent and
// asymmetric: the FIRST program with a given source answered correctly and every later one
// answered -1, so nothing that linked a program once could see it.
//
// Both the explicit-request path (glBindFragDataLocation, answered from
// linkedFragDataLocation) and the shader-declared path (layout(location = 0), answered from
// the pipe-output snapshot) are checked, because only the second one reads the field that
// used to come off the TProgram.
TEST_F(TranslationCacheTest, AProgramServedFromTheMemoStillAnswersGetFragDataLocation) {
const SyncCompileScope sync;
const CacheModeScope cacheOn(true);
const String fs = SwizzleLikeFragment("");
const GLuint parsed = LinkProgramFromSources(kVertexSource, fs);
const TranslationCacheStats afterFirst = MG_State::GLState::GetProgramTranslationCache().Stats();
const GLuint fromMemo = LinkProgramFromSources(kVertexSource, fs);
const TranslationCacheStats afterSecond = MG_State::GLState::GetProgramTranslationCache().Stats();
ASSERT_EQ(afterSecond.hits - afterFirst.hits, 1u) << "the second link was not a hit";
const Int parsedLocation = MG_Impl::GLImpl::GetFragDataLocation(parsed, "fragColor");
const Int memoLocation = MG_Impl::GLImpl::GetFragDataLocation(fromMemo, "fragColor");
EXPECT_EQ(parsedLocation, 0) << "the parsed program's own answer moved; this case is testing "
"the wrong thing";
EXPECT_EQ(memoLocation, parsedLocation)
<< "a program served from the L1 memo lost its fragment output location";
// A name that is not an output must still be -1 from both, so the case cannot pass by
// making the accessor answer everything.
EXPECT_EQ(MG_Impl::GLImpl::GetFragDataLocation(fromMemo, "notAnOutput"), -1);
}
// The modules a hit hands out must be the modules a from-scratch translation would have
// produced. Without this the case above would still pass if the cache returned garbage.
TEST_F(TranslationCacheTest, L1HitsAgreeWithACacheDisabledTranslation) {
+477 -12
View File
@@ -377,6 +377,40 @@ TEST_F(TextureTest, ClearTexImageErrorContracts) {
EXPECT_EQ(MG_Impl::GLImpl::GetError(), static_cast<GLenum>(GL_INVALID_ENUM));
}
// GL 4.6 core 8.19: a compressed internal format is INVALID_OPERATION for both clear entry points.
// The generic GL_COMPRESSED_* enums are the half that needs its own tag - MobileGL answers them
// with uncompressed storage on purpose, so by the time the clear runs the level looks like any
// other RGBA8 image unless the REQUEST was recorded alongside it.
TEST_F(TextureTest, ClearTexImageRejectsCompressedTextures) {
GLuint genericTexture = 0;
MG_Impl::GLImpl::GenTextures(1, &genericTexture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, genericTexture);
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_COMPRESSED_RGBA, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::ClearTexImage(genericTexture, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
ExpectSingleGlError(GL_INVALID_OPERATION);
MG_Impl::GLImpl::ClearTexSubImage(genericTexture, 0, 0, 0, 0, 4, 4, 1, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
ExpectSingleGlError(GL_INVALID_OPERATION);
// A specific compressed internalformat is refused through the tag the level already carried...
GLuint specificTexture = 0;
MG_Impl::GLImpl::GenTextures(1, &specificTexture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, specificTexture);
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_COMPRESSED_RED_RGTC1, 8, 8, 0, GL_RED, GL_UNSIGNED_BYTE,
nullptr);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::ClearTexImage(specificTexture, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr);
ExpectSingleGlError(GL_INVALID_OPERATION);
// ...and respecifying the level with an uncompressed format makes it clearable again, because
// AllocateStorage clears both tags.
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_R8, 8, 8, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::ClearTexImage(specificTexture, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT is float state that must answer every numeric query: GetFloatv
// is authoritative and GetIntegerv would otherwise fall through to its INVALID_ENUM default.
TEST_F(TextureTest, MaxTextureMaxAnisotropyIsAnsweredFromTheBackendLimit) {
@@ -1023,6 +1057,32 @@ TEST_F(TextureTest, TexImage2DAcceptsSpecCompliantFormatCombinations) {
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// GL_STENCIL_INDEX is the unsized base format for stencil-only storage, and refusing it as an
// internal format killed the ARB_clear_texture stencil case in its own setup - before it could
// reach the calls it actually tests. The stencil-only transfer format stays paired with
// stencil-only storage in both directions, which is what keeps those clears erroring.
TEST_F(TextureTest, StencilIndexIsATextureInternalFormatPairedOnlyWithStencilStorage) {
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_STENCIL_INDEX, 4, 4, 0, GL_STENCIL_INDEX, GL_UNSIGNED_BYTE,
nullptr);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
const auto textureObject = MG_State::pGLContext->GetTextureObject(texture);
ASSERT_NE(textureObject, nullptr);
EXPECT_EQ(textureObject->GetFormat(), TextureInternalFormat::StencilIndex8);
// A colour transfer format against stencil storage is still INVALID_OPERATION, so the clear
// the conformance case makes next fails the way it is supposed to.
MG_Impl::GLImpl::ClearTexImage(texture, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
ExpectSingleGlError(GL_INVALID_OPERATION);
// ...and the other direction: GL_STENCIL_INDEX against colour storage stays illegal.
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 4, 4, 0, GL_STENCIL_INDEX, GL_UNSIGNED_BYTE, nullptr);
ExpectSingleGlError(GL_INVALID_OPERATION);
}
// Desktop GL table 3.3 lists GREEN and BLUE as TexImage client formats (GL CTS packed_pixels
// rgba8_format_green/blue upload with them and verify the readback): the single input component
// feeds the named channel, the other color channels default to 0 and alpha to 1.
@@ -1317,6 +1377,56 @@ TEST_F(TextureTest, GetTextureImageReadsNamedObjectWithoutBinding) {
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// GL 4.6 core 8.11.4 asks a readback for cube completeness and nothing else, so a mip chain whose
// levels BELOW the requested one were never defined is still readable at that level - which is
// exactly the shape ARB_clear_texture's conformance cases build (they define only the level they
// clear). The whole-chain completeness gate used to answer INVALID_OPERATION here.
TEST_F(TextureTest, GetTexImageReadsALevelWhoseLowerLevelsWereNeverDefined) {
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
const Uint8 pixels[] = {
61, 62, 63, 64,
71, 72, 73, 74,
};
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 2, GL_RGBA8, 2, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
Uint8 output[sizeof(pixels)] = {};
MG_Impl::GLImpl::GetTexImage(GL_TEXTURE_2D, 2, GL_RGBA, GL_UNSIGNED_BYTE, output);
EXPECT_EQ(std::memcmp(output, pixels, sizeof(pixels)), 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// The other half of the same rule: loosening the chain-wide check must not let a level that holds
// no image at all through. Level 0 exists as a chain slot once level 2 is defined, but nothing ever
// gave it an image, so it stays INVALID_OPERATION - as does a level past the end of the chain and a
// texture that was never given any image whatsoever.
TEST_F(TextureTest, GetTexImageStillRejectsALevelThatHoldsNoImage) {
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
Uint8 output[4] = {};
// No image at all yet: the chain carries no levels.
MG_Impl::GLImpl::GetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, output);
ExpectSingleGlError(GL_INVALID_OPERATION);
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 2, GL_RGBA8, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// Inside the chain, but never defined.
MG_Impl::GLImpl::GetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, output);
ExpectSingleGlError(GL_INVALID_OPERATION);
// Past the end of the chain.
MG_Impl::GLImpl::GetTexImage(GL_TEXTURE_2D, 3, GL_RGBA, GL_UNSIGNED_BYTE, output);
ExpectSingleGlError(GL_INVALID_OPERATION);
}
TEST_F(TextureTest, GetTextureSubImageReadsFullNamedLevelWithoutBinding) {
GLuint texture = 0;
GLuint boundTexture = 0;
@@ -1651,6 +1761,56 @@ TEST_F(TextureTest, AnUncompressedRespecificationClearsTheCompressedTag) {
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// The same rule for the 3D entry points, which never recorded the tag at all. Besides the two
// level queries this decides the level's texel BLOCK SIZE, which glCopyImageSubData compares
// against the other endpoint's - an untagged GL_COMPRESSED_RG_RGTC2 array level measured as the
// RG8 storage it resolves to, 2 bytes instead of 16.
TEST_F(TextureTest, TexImage3DAndTexStorage3DTagASpecificCompressedInternalFormat) {
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_ARRAY, texture);
MG_Impl::GLImpl::TexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_COMPRESSED_RG_RGTC2, 8, 8, 2, 0, GL_RG,
GL_UNSIGNED_BYTE, nullptr);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
GLint compressed = GL_FALSE;
MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_2D_ARRAY, 0, GL_TEXTURE_COMPRESSED, &compressed);
EXPECT_EQ(compressed, GL_TRUE);
GLint internalFormat = 0;
MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_2D_ARRAY, 0, GL_TEXTURE_INTERNAL_FORMAT, &internalFormat);
EXPECT_EQ(internalFormat, static_cast<GLint>(GL_COMPRESSED_RG_RGTC2));
// 8x8 in 4x4 blocks of 16 bytes each is 64 bytes a layer, and both layers count.
GLint imageSize = 0;
MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_2D_ARRAY, 0, GL_TEXTURE_COMPRESSED_IMAGE_SIZE, &imageSize);
EXPECT_EQ(imageSize, 128);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// The texel shadow behind the tag keeps the uncompressed storage the format resolves to.
const auto textureObject = MG_State::pGLContext->GetTextureObject(texture);
ASSERT_NE(textureObject, nullptr);
EXPECT_EQ(textureObject->GetFormat(), TextureInternalFormat::RG8);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_ARRAY, 0);
// glTexStorage3D has the same gap and the same fix; immutable storage plus
// glCompressedTexSubImage3D is the modern way to upload a compressed array texture.
GLuint storageTexture = 0;
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D_ARRAY, 1, &storageTexture);
MG_Impl::GLImpl::TextureStorage3D(storageTexture, 1, GL_COMPRESSED_RG_RGTC2, 8, 8, 2);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_ARRAY, storageTexture);
compressed = GL_FALSE;
MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_2D_ARRAY, 0, GL_TEXTURE_COMPRESSED, &compressed);
EXPECT_EQ(compressed, GL_TRUE);
imageSize = 0;
MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_2D_ARRAY, 0, GL_TEXTURE_COMPRESSED_IMAGE_SIZE, &imageSize);
EXPECT_EQ(imageSize, 128);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_ARRAY, 0);
}
namespace {
// A 16x16 RGBA8 texture with exactly `levelCount` levels, defined the way
// KHR-GL43.copy_image.non_existent_mipmap defines its textures - glTexImage2D per
@@ -3320,6 +3480,29 @@ TEST(SharedExponentRGB9E5Test, RawPackedPixelTransferCoversOnlyIdenticalLayouts)
TexturePixelDataType::UnsignedInt5999Rev));
}
TEST(SharedExponentRGB9E5Test, RedundantPackedEncodingIsRGB9E5Only) {
using MG_Util::PixelStoreProcessor::HasRedundantPackedEncoding;
// This is the predicate that decides whether the CPU shadow has to answer glGetTexImage
// instead of a GPU readback, so it must be as narrow as the defect: only the shared exponent
// has several legal encodings of one value.
EXPECT_TRUE(HasRedundantPackedEncoding(TextureInternalFormat::RGB9E5));
// The other three packed 32-bit layouts round-trip through float32 bit-exactly (each field is
// either an integer or a unique float encoding), so a GPU readback still serves them - which
// matters because RGB10_A2 and R11F_G11F_B10F ARE colour-renderable and their shadow can
// legitimately be stale.
EXPECT_FALSE(HasRedundantPackedEncoding(TextureInternalFormat::RGB10A2));
EXPECT_FALSE(HasRedundantPackedEncoding(TextureInternalFormat::RGB10A2UI));
EXPECT_FALSE(HasRedundantPackedEncoding(TextureInternalFormat::R11FG11FB10F));
// Nothing unpacked qualifies, and neither does an unknown format.
EXPECT_FALSE(HasRedundantPackedEncoding(TextureInternalFormat::RGBA8));
EXPECT_FALSE(HasRedundantPackedEncoding(TextureInternalFormat::RGBA32F));
EXPECT_FALSE(HasRedundantPackedEncoding(TextureInternalFormat::RGB8));
EXPECT_FALSE(HasRedundantPackedEncoding(TextureInternalFormat::Unknown));
}
TEST_F(TextureTest, TexImage2DRGB9E5KeepsNonCanonicalClientWords) {
// Upload direction: GL_RGB / GL_UNSIGNED_INT_5_9_9_9_REV into GL_RGB9_E5 stores the client
// words untouched, including the redundant encodings the CTS generates.
@@ -4011,24 +4194,30 @@ TEST_F(TextureTest, CopyTexImage1DReportsUnsupportedInsteadOfTerminating) {
ExpectSingleGlError(GL_INVALID_OPERATION);
}
TEST_F(TextureTest, GetTexLevelParameterOnBufferStorageReportsErrorInsteadOfTerminating) {
// TextureStorageType is {Mipmap, Buffer} and the level queries only answer out of a mipmap
// chain, so every glGetTexLevelParameter* on a GL_TEXTURE_BUFFER texture reached a
// THROW_UNIMPL_EXCEPTION default: label and killed the process.
TEST_F(TextureTest, GetTexLevelParameterAnswersBufferStorageGeometry) {
// TextureStorageType is {Mipmap, Buffer} and the level queries used to answer only out of a
// mipmap chain, so every glGetTexLevelParameter* on a GL_TEXTURE_BUFFER texture reached a
// THROW_UNIMPL_EXCEPTION default: label and killed the process. It now answers out of the
// attached buffer range instead (GL 4.6 core 8.9): a buffer texture is one-dimensional, and
// with no buffer attached it addresses no texels at all.
GLuint texture = 0;
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_BUFFER, 1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_BUFFER, texture);
MG_Impl::GLImpl::TexBuffer(GL_TEXTURE_BUFFER, GL_R8, 0);
DrainPendingGlErrors();
for (const GLenum pname : {GL_TEXTURE_WIDTH, GL_TEXTURE_HEIGHT, GL_TEXTURE_DEPTH}) {
const std::pair<GLenum, GLint> expectations[] = {
{GL_TEXTURE_WIDTH, 0}, {GL_TEXTURE_HEIGHT, 1}, {GL_TEXTURE_DEPTH, 1}};
for (const auto& [pname, expected] : expectations) {
GLint intParam = 0x20202020;
MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_BUFFER, 0, pname, &intParam);
ExpectSingleGlError(GL_INVALID_OPERATION);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), static_cast<GLenum>(GL_NO_ERROR));
EXPECT_EQ(intParam, expected) << "pname " << pname;
GLfloat floatParam = 12345.0f;
MG_Impl::GLImpl::GetTexLevelParameterfv(GL_TEXTURE_BUFFER, 0, pname, &floatParam);
ExpectSingleGlError(GL_INVALID_OPERATION);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), static_cast<GLenum>(GL_NO_ERROR));
EXPECT_EQ(floatParam, static_cast<GLfloat>(expected)) << "pname " << pname;
}
}
@@ -4100,24 +4289,25 @@ namespace {
GLint SrcZ = -1;
GLint DstZ = -1;
GLsizei Depth = -1;
Bool SrcIsRenderbuffer = false;
Bool DstIsRenderbuffer = false;
} g_copyImageSubDataCall;
void RecordCopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture, GLenum srcTarget,
void RecordCopyImageSubData(const MG_Backend::CopyImageEndpoint& src, GLenum srcTarget,
GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture, GLenum dstTarget,
const MG_Backend::CopyImageEndpoint& dst, GLenum dstTarget,
GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLsizei srcWidth,
GLsizei srcHeight, GLsizei srcDepth) {
(void)srcTexture;
(void)srcLevel;
(void)srcX;
(void)srcY;
(void)dstTexture;
(void)dstLevel;
(void)dstX;
(void)dstY;
(void)srcWidth;
(void)srcHeight;
g_copyImageSubDataCall = {true, srcTarget, dstTarget, srcZ, dstZ, srcDepth};
g_copyImageSubDataCall = {true, srcTarget, dstTarget, srcZ,
dstZ, srcDepth, src.IsRenderbuffer(), dst.IsRenderbuffer()};
}
// Two storage-backed 2D textures of the requested formats, so a copy between them is a legal
@@ -4354,9 +4544,13 @@ TEST_F(TextureTest, CopyImageSubDataAcceptsAPlainMutableTexImage2DPair) {
MG_Impl::GLImpl::GenTextures(1, &reusedSrc);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, reusedSrc);
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
MG_Impl::GLImpl::GenTextures(1, &reusedDst);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, reusedDst);
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::CopyImageSubData(reusedSrc, GL_TEXTURE_2D, 0, 0, 0, 0, reusedDst, GL_TEXTURE_2D, 0, 0, 0, 0, 1,
@@ -4388,3 +4582,274 @@ TEST_F(TextureTest, CopyImageSubDataPassesTheRectangleTargetThroughUntranslated)
EXPECT_EQ(g_copyImageSubDataCall.DstTarget, static_cast<GLenum>(GL_TEXTURE_RECTANGLE));
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// GL 4.6 core 18.3.2 accepts GL_RENDERBUFFER as an endpoint target, and a renderbuffer name lives
// in its own namespace. Resolving BOTH names through the texture namespace answered a null object
// for every renderbuffer endpoint, so all 74 conformance cases that name one - the whole
// texture<->renderbuffer half of KHR-GL43.copy_image, plus its smoke test - reported
// GL_INVALID_VALUE. The endpoint is a sum type now; the target picks the namespace.
TEST_F(TextureTest, CopyImageSubDataResolvesARenderbufferEndpointInTheRenderbufferNamespace) {
const ScopedTextureBackendFunctionsOverride backendGuard;
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
g_copyImageSubDataCall = {};
GLuint texture = 0;
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture);
MG_Impl::GLImpl::TextureStorage2D(texture, 1, GL_RGBA8, 8, 8);
GLuint renderbuffer = 0;
MG_Impl::GLImpl::CreateRenderbuffers(1, &renderbuffer);
MG_Impl::GLImpl::NamedRenderbufferStorage(renderbuffer, GL_RGBA8, 8, 8);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::CopyImageSubData(texture, GL_TEXTURE_2D, 0, 0, 0, 0, renderbuffer, GL_RENDERBUFFER, 0, 0, 0, 0,
4, 4, 1);
EXPECT_TRUE(g_copyImageSubDataCall.Called);
EXPECT_FALSE(g_copyImageSubDataCall.SrcIsRenderbuffer);
EXPECT_TRUE(g_copyImageSubDataCall.DstIsRenderbuffer);
EXPECT_EQ(g_copyImageSubDataCall.DstTarget, static_cast<GLenum>(GL_RENDERBUFFER));
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// ...and back the other way, which is the second half of the conformance case's two-copy
// shape (texture -> renderbuffer -> texture).
g_copyImageSubDataCall = {};
MG_Impl::GLImpl::CopyImageSubData(renderbuffer, GL_RENDERBUFFER, 0, 0, 0, 0, texture, GL_TEXTURE_2D, 0, 0, 0, 0,
4, 4, 1);
EXPECT_TRUE(g_copyImageSubDataCall.Called);
EXPECT_TRUE(g_copyImageSubDataCall.SrcIsRenderbuffer);
EXPECT_FALSE(g_copyImageSubDataCall.DstIsRenderbuffer);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// Renderbuffer to renderbuffer, the shape neither endpoint could take before, plus the negative
// that pins which table was consulted: with GL_RENDERBUFFER named, a number that is not a live
// RENDERBUFFER is INVALID_VALUE - the texture table is never asked.
TEST_F(TextureTest, CopyImageSubDataKeepsTheTwoNameNamespacesApart) {
const ScopedTextureBackendFunctionsOverride backendGuard;
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
g_copyImageSubDataCall = {};
GLuint srcRenderbuffer = 0;
GLuint dstRenderbuffer = 0;
MG_Impl::GLImpl::CreateRenderbuffers(1, &srcRenderbuffer);
MG_Impl::GLImpl::CreateRenderbuffers(1, &dstRenderbuffer);
MG_Impl::GLImpl::NamedRenderbufferStorage(srcRenderbuffer, GL_RGBA8, 8, 8);
MG_Impl::GLImpl::NamedRenderbufferStorage(dstRenderbuffer, GL_RGBA8, 8, 8);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::CopyImageSubData(srcRenderbuffer, GL_RENDERBUFFER, 0, 0, 0, 0, dstRenderbuffer,
GL_RENDERBUFFER, 0, 0, 0, 0, 4, 4, 1);
EXPECT_TRUE(g_copyImageSubDataCall.Called);
EXPECT_TRUE(g_copyImageSubDataCall.SrcIsRenderbuffer);
EXPECT_TRUE(g_copyImageSubDataCall.DstIsRenderbuffer);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
g_copyImageSubDataCall = {};
MG_Impl::GLImpl::CopyImageSubData(srcRenderbuffer, GL_RENDERBUFFER, 0, 0, 0, 0, 4243, GL_RENDERBUFFER, 0, 0, 0,
0, 4, 4, 1);
EXPECT_FALSE(g_copyImageSubDataCall.Called);
ExpectSingleGlError(GL_INVALID_VALUE);
}
// A renderbuffer has exactly one image, so any level above zero is the same INVALID_VALUE a
// texture gets for a level it does not have - and an unallocated one is an incomplete image,
// which 18.3.2 spells INVALID_OPERATION.
TEST_F(TextureTest, CopyImageSubDataChecksARenderbufferLevelAndStorage) {
const ScopedTextureBackendFunctionsOverride backendGuard;
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
g_copyImageSubDataCall = {};
GLuint texture = 0;
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture);
MG_Impl::GLImpl::TextureStorage2D(texture, 1, GL_RGBA8, 8, 8);
GLuint renderbuffer = 0;
MG_Impl::GLImpl::CreateRenderbuffers(1, &renderbuffer);
MG_Impl::GLImpl::NamedRenderbufferStorage(renderbuffer, GL_RGBA8, 8, 8);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::CopyImageSubData(texture, GL_TEXTURE_2D, 0, 0, 0, 0, renderbuffer, GL_RENDERBUFFER, 1, 0, 0, 0,
4, 4, 1);
EXPECT_FALSE(g_copyImageSubDataCall.Called);
ExpectSingleGlError(GL_INVALID_VALUE);
g_copyImageSubDataCall = {};
GLuint emptyRenderbuffer = 0;
MG_Impl::GLImpl::CreateRenderbuffers(1, &emptyRenderbuffer);
DrainPendingGlErrors();
MG_Impl::GLImpl::CopyImageSubData(texture, GL_TEXTURE_2D, 0, 0, 0, 0, emptyRenderbuffer, GL_RENDERBUFFER, 0, 0,
0, 0, 4, 4, 1);
EXPECT_FALSE(g_copyImageSubDataCall.Called);
ExpectSingleGlError(GL_INVALID_OPERATION);
}
// A 16-byte RGTC2 block and a 16-byte RGBA32UI texel are in the same size class, so GL 4.6 core
// 18.3.2 requires this copy to succeed. It did not for an ARRAY source: glTexImage3D recorded no
// specific-compressed-format tag, so the level was measured as the 2-byte RG8 storage RGTC2
// resolves to and the compatibility rule saw 2 against 16.
TEST_F(TextureTest, CopyImageSubDataSizesACompressedArrayLevelByItsBlock) {
const ScopedTextureBackendFunctionsOverride backendGuard;
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
g_copyImageSubDataCall = {};
GLuint compressedSource = 0;
MG_Impl::GLImpl::GenTextures(1, &compressedSource);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_ARRAY, compressedSource);
MG_Impl::GLImpl::TexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_COMPRESSED_RG_RGTC2, 8, 8, 1, 0, GL_RG,
GL_UNSIGNED_BYTE, nullptr);
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_BASE_LEVEL, 0);
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAX_LEVEL, 0);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_ARRAY, 0);
GLuint uncompressedDestination = 0;
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D_ARRAY, 1, &uncompressedDestination);
MG_Impl::GLImpl::TextureStorage3D(uncompressedDestination, 1, GL_RGBA32UI, 8, 8, 1);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::CopyImageSubData(compressedSource, GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, uncompressedDestination,
GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, 8, 8, 1);
EXPECT_TRUE(g_copyImageSubDataCall.Called);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// 18.3.2 requires INVALID_OPERATION when either object is an INCOMPLETE TEXTURE, and completeness
// is GL 4.6 core 8.17's - which includes the mip chain whenever the minification filter reads it.
// A mutable texture with level 0 alone still carries the default NEAREST_MIPMAP_LINEAR filter, so
// it is mipmap incomplete; the storage-only IsComplete() this used to ask called it complete and
// let the copy through, which is the whole of KHR-GL43.copy_image.incomplete_tex.
TEST_F(TextureTest, CopyImageSubDataRejectsAMipmapIncompleteTexture) {
const ScopedTextureBackendFunctionsOverride backendGuard;
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
g_copyImageSubDataCall = {};
GLuint incomplete = 0;
MG_Impl::GLImpl::GenTextures(1, &incomplete);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, incomplete);
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0);
GLuint complete = 0;
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &complete);
MG_Impl::GLImpl::TextureStorage2D(complete, 1, GL_RGBA8, 16, 16);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::CopyImageSubData(incomplete, GL_TEXTURE_2D, 0, 0, 0, 0, complete, GL_TEXTURE_2D, 0, 0, 0, 0, 4,
4, 1);
EXPECT_FALSE(g_copyImageSubDataCall.Called);
ExpectSingleGlError(GL_INVALID_OPERATION);
// The destination side is checked the same way.
g_copyImageSubDataCall = {};
MG_Impl::GLImpl::CopyImageSubData(complete, GL_TEXTURE_2D, 0, 0, 0, 0, incomplete, GL_TEXTURE_2D, 0, 0, 0, 0, 4,
4, 1);
EXPECT_FALSE(g_copyImageSubDataCall.Called);
ExpectSingleGlError(GL_INVALID_OPERATION);
// Capping TEXTURE_MAX_LEVEL at the one level that exists is what the conformance suite's
// makeTextureComplete does, and it is enough to make the same object complete.
g_copyImageSubDataCall = {};
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, incomplete);
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0);
DrainPendingGlErrors();
MG_Impl::GLImpl::CopyImageSubData(incomplete, GL_TEXTURE_2D, 0, 0, 0, 0, complete, GL_TEXTURE_2D, 0, 0, 0, 0, 4,
4, 1);
EXPECT_TRUE(g_copyImageSubDataCall.Called);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// The targets that have no mip chain must not be dragged in: GL 4.6 core 8.17 makes q equal to
// level_base for them, so no filter can make them mipmap incomplete. A rectangle texture gets a
// non-mipmapping default filter from the object itself, so it would survive a predicate that
// trusted the sampler alone - it is here because the whole texture path is one branch and this is
// the cheap half of pinning it.
TEST_F(TextureTest, CopyImageSubDataDoesNotApplyMipmapCompletenessToRectangleTextures) {
const ScopedTextureBackendFunctionsOverride backendGuard;
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
g_copyImageSubDataCall = {};
GLuint srcRectangle = 0;
GLuint dstRectangle = 0;
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_RECTANGLE, 1, &srcRectangle);
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_RECTANGLE, 1, &dstRectangle);
MG_Impl::GLImpl::TextureStorage2D(srcRectangle, 1, GL_RGBA8, 8, 8);
MG_Impl::GLImpl::TextureStorage2D(dstRectangle, 1, GL_RGBA8, 8, 8);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::CopyImageSubData(srcRectangle, GL_TEXTURE_RECTANGLE, 0, 0, 0, 0, dstRectangle,
GL_TEXTURE_RECTANGLE, 0, 0, 0, 0, 4, 4, 1);
EXPECT_TRUE(g_copyImageSubDataCall.Called);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// The multisample half, which is the one the target guard actually exists for: a multisample
// texture keeps the shared NEAREST_MIPMAP_LINEAR default in its own sampler state (only the
// rectangle constructor overrides it), so asking the mipmap predicate about it without the target
// guard would report every 8x8 multisample image incomplete and refuse a legal copy.
TEST_F(TextureTest, CopyImageSubDataDoesNotApplyMipmapCompletenessToMultisampleTextures) {
const ScopedTextureBackendFunctionsOverride backendGuard;
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
g_copyImageSubDataCall = {};
GLuint srcMultisample = 0;
GLuint dstMultisample = 0;
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D_MULTISAMPLE, 1, &srcMultisample);
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D_MULTISAMPLE, 1, &dstMultisample);
MG_Impl::GLImpl::TextureStorage2DMultisample(srcMultisample, 1, GL_RGBA8, 8, 8, GL_FALSE);
MG_Impl::GLImpl::TextureStorage2DMultisample(dstMultisample, 1, GL_RGBA8, 8, 8, GL_FALSE);
DrainPendingGlErrors();
// This unit-test binary has no backend behind the renderable-format and sample-count queries,
// so the storage may not have been created at all. Checked on the state objects rather than
// assumed, so the case can only skip or test the real rule.
const auto srcObject = MG_State::pGLContext->GetTextureObject(srcMultisample);
const auto dstObject = MG_State::pGLContext->GetTextureObject(dstMultisample);
ASSERT_NE(srcObject, nullptr);
ASSERT_NE(dstObject, nullptr);
if (!srcObject->IsComplete() || !dstObject->IsComplete()) {
GTEST_SKIP() << "this context could not give the multisample textures storage";
}
MG_Impl::GLImpl::CopyImageSubData(srcMultisample, GL_TEXTURE_2D_MULTISAMPLE, 0, 0, 0, 0, dstMultisample,
GL_TEXTURE_2D_MULTISAMPLE, 0, 0, 0, 0, 4, 4, 1);
EXPECT_TRUE(g_copyImageSubDataCall.Called);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// GL 4.6 core 8.11 makes GL_IMAGE_FORMAT_COMPATIBILITY_TYPE readable through every
// GetTexParameter form. Three of MobileGL's four getters answered it and glGetTexParameterfv did
// not, so the float query raised GL_INVALID_ENUM and left the caller's float uninitialised
// (KHR-GL4x.shader_image_load_store.basic-api-texParam reads it with both iv and fv and compares
// them). Asserted across all four here, because an enum present in three of four parallel
// switches is the drift shape that comes back.
TEST_F(TextureTest, ImageFormatCompatibilityTypeAgreesAcrossEveryTexParameterGetter) {
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
MG_Impl::GLImpl::TexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 4, 4);
DrainPendingGlErrors();
GLint integerValue = 0;
MG_Impl::GLImpl::GetTexParameteriv(GL_TEXTURE_2D, GL_IMAGE_FORMAT_COMPATIBILITY_TYPE, &integerValue);
EXPECT_EQ(integerValue, GL_IMAGE_FORMAT_COMPATIBILITY_BY_SIZE);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
GLfloat floatValue = 0.0f;
MG_Impl::GLImpl::GetTexParameterfv(GL_TEXTURE_2D, GL_IMAGE_FORMAT_COMPATIBILITY_TYPE, &floatValue);
EXPECT_FLOAT_EQ(floatValue, static_cast<GLfloat>(GL_IMAGE_FORMAT_COMPATIBILITY_BY_SIZE));
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
GLint signedValue = 0;
MG_Impl::GLImpl::GetTexParameterIiv(GL_TEXTURE_2D, GL_IMAGE_FORMAT_COMPATIBILITY_TYPE, &signedValue);
EXPECT_EQ(signedValue, GL_IMAGE_FORMAT_COMPATIBILITY_BY_SIZE);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
GLuint unsignedValue = 0;
MG_Impl::GLImpl::GetTexParameterIuiv(GL_TEXTURE_2D, GL_IMAGE_FORMAT_COMPATIBILITY_TYPE, &unsignedValue);
EXPECT_EQ(unsignedValue, static_cast<GLuint>(GL_IMAGE_FORMAT_COMPATIBILITY_BY_SIZE));
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0);
MG_Impl::GLImpl::DeleteTextures(1, &texture);
DrainPendingGlErrors();
}
@@ -692,8 +692,12 @@ namespace MobileGL::MG_Util::BackendLoader {
!f.glUnmapBuffer || !f.glMemoryBarrier || !f.glCreateShader || !f.glCreateProgram) {
return false;
}
GLint maxVertexSsboBlocks = 0;
f.glGetIntegerv(GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS, &maxVertexSsboBlocks);
// Read from caps, not re-queried: the per-stage limits are resolved (and their query
// errors drained) before this probe runs, so asking the driver again would be a second
// round trip that can disagree with the number MobileGL actually advertises - and, on
// the early return below, would leave its own GL_INVALID_ENUM in the queue for the
// application's first glGetError to find.
const GLint maxVertexSsboBlocks = caps.MaxVertexShaderStorageBlocks;
if (maxVertexSsboBlocks < 1) {
// The native indirect machinery cannot read the command buffer from the vertex
// stage on this driver anyway; assume conforming zero-based gl_InstanceID.
@@ -1036,6 +1040,15 @@ namespace MobileGL::MG_Util::BackendLoader {
GLint maxVertexAttribs = 16;
GLint maxComputeShaderStorageBlocks = 8;
GLint maxCombinedShaderStorageBlocks = 32;
// ES 3.2 table 21.44 minimums. Zero for the four graphics stages below fragment is not a
// placeholder - it is what the spec permits and what ARM's GLES driver actually reports,
// so a probe that never runs (pre-ES 3.2, unsupported pname) leaves behind the truthful
// answer rather than an optimistic one.
GLint maxVertexShaderStorageBlocks = 0;
GLint maxTessControlShaderStorageBlocks = 0;
GLint maxTessEvaluationShaderStorageBlocks = 0;
GLint maxGeometryShaderStorageBlocks = 0;
GLint maxFragmentShaderStorageBlocks = 4;
GLint maxComputeUniformBlocks = 12;
GLint maxComputeWorkGroupInvocations = 128;
GLint maxShaderStorageBufferBindings = 8;
@@ -1127,6 +1140,59 @@ namespace MobileGL::MG_Util::BackendLoader {
(caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 2)) {
glesFuncs.glGetIntegerv(GL_MAX_GEOMETRY_IMAGE_UNIFORMS, &maxGeometryImageUniforms);
}
// Per-stage storage-block counts. Deliberately NOT batched with the unconditional probes
// above, for the reason GL_MAX_TEXTURE_BUFFER_SIZE is not: the vertex and fragment pnames
// are ES 3.1, but the tessellation and geometry ones only exist from ES 3.2 on (or under
// EXT_tessellation_shader / EXT_geometry_shader), so on an older context they raise
// GL_INVALID_ENUM, leave the local untouched, and - with nothing draining the queue until
// some later probe - let that error be misattributed to an unrelated query in between, or
// leak into the application's first glGetError.
//
// A stage whose probe does not run keeps the spec minimum, which for all four graphics
// stages is 0. That is the honest answer: DirectGLES emits ESSL 3.10 on an ES 3.1 context,
// where those stages do not exist at all.
{
const auto drainErrors = [&glesFuncs]() {
Bool hadError = false;
if (glesFuncs.glGetError) {
while (glesFuncs.glGetError() != GL_NO_ERROR) hadError = true;
}
return hadError;
};
// Isolate from errors raised by the preceding probes so the drain below reports on
// these queries only.
drainErrors();
glesFuncs.glGetIntegerv(GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS, &maxVertexShaderStorageBlocks);
glesFuncs.glGetIntegerv(GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS, &maxFragmentShaderStorageBlocks);
if (drainErrors()) {
MGLOG_W("Per-stage shader storage block query failed for the vertex/fragment "
"stages; assuming the ES minimums (vertex 0, fragment 4)");
maxVertexShaderStorageBlocks = 0;
maxFragmentShaderStorageBlocks = 4;
}
if (esAtLeast32) {
glesFuncs.glGetIntegerv(GL_MAX_TESS_CONTROL_SHADER_STORAGE_BLOCKS,
&maxTessControlShaderStorageBlocks);
glesFuncs.glGetIntegerv(GL_MAX_TESS_EVALUATION_SHADER_STORAGE_BLOCKS,
&maxTessEvaluationShaderStorageBlocks);
glesFuncs.glGetIntegerv(GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS, &maxGeometryShaderStorageBlocks);
if (drainErrors()) {
MGLOG_W("Per-stage shader storage block query failed for the tessellation/"
"geometry stages; assuming the ES minimum of 0");
maxTessControlShaderStorageBlocks = 0;
maxTessEvaluationShaderStorageBlocks = 0;
maxGeometryShaderStorageBlocks = 0;
}
}
// A driver is free to report a negative or nonsensical count into an untouched
// out-param; clamp before anything downstream treats it as a capacity.
maxVertexShaderStorageBlocks = std::max(maxVertexShaderStorageBlocks, 0);
maxTessControlShaderStorageBlocks = std::max(maxTessControlShaderStorageBlocks, 0);
maxTessEvaluationShaderStorageBlocks = std::max(maxTessEvaluationShaderStorageBlocks, 0);
maxGeometryShaderStorageBlocks = std::max(maxGeometryShaderStorageBlocks, 0);
maxFragmentShaderStorageBlocks = std::max(maxFragmentShaderStorageBlocks, 0);
}
glesFuncs.glGetIntegerv(GL_MAX_DRAW_BUFFERS, &maxDrawBuffers);
glesFuncs.glGetIntegerv(GL_MAX_COLOR_ATTACHMENTS, &maxColorAttachments);
glesFuncs.glGetIntegerv(GL_MAX_CLIP_DISTANCES, &maxClipDistances);
@@ -1271,6 +1337,11 @@ namespace MobileGL::MG_Util::BackendLoader {
caps.MaxVertexAttribs = maxVertexAttribs;
caps.MaxComputeShaderStorageBlocks = maxComputeShaderStorageBlocks;
caps.MaxCombinedShaderStorageBlocks = maxCombinedShaderStorageBlocks;
caps.MaxVertexShaderStorageBlocks = maxVertexShaderStorageBlocks;
caps.MaxTessControlShaderStorageBlocks = maxTessControlShaderStorageBlocks;
caps.MaxTessEvaluationShaderStorageBlocks = maxTessEvaluationShaderStorageBlocks;
caps.MaxGeometryShaderStorageBlocks = maxGeometryShaderStorageBlocks;
caps.MaxFragmentShaderStorageBlocks = maxFragmentShaderStorageBlocks;
caps.MaxComputeUniformBlocks = maxComputeUniformBlocks;
caps.MaxComputeWorkGroupInvocations = maxComputeWorkGroupInvocations;
caps.MaxShaderStorageBufferBindings = maxShaderStorageBufferBindings;
@@ -1348,6 +1419,14 @@ namespace MobileGL::MG_Util::BackendLoader {
MGLOG_I(" GL_MAX_VERTEX_ATTRIBS: %d", caps.MaxVertexAttribs);
MGLOG_I(" GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS: %d", caps.MaxComputeShaderStorageBlocks);
MGLOG_I(" GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS: %d", caps.MaxCombinedShaderStorageBlocks);
// Worth a line each: a zero here is what stops an application's storage block from ever
// working in that stage, and reading it back from an artifact is the difference between
// "MobileGL dropped my draw" and "this driver has no SSBOs outside compute".
MGLOG_I(" GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS: %d", caps.MaxVertexShaderStorageBlocks);
MGLOG_I(" GL_MAX_TESS_CONTROL_SHADER_STORAGE_BLOCKS: %d", caps.MaxTessControlShaderStorageBlocks);
MGLOG_I(" GL_MAX_TESS_EVALUATION_SHADER_STORAGE_BLOCKS: %d", caps.MaxTessEvaluationShaderStorageBlocks);
MGLOG_I(" GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS: %d", caps.MaxGeometryShaderStorageBlocks);
MGLOG_I(" GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS: %d", caps.MaxFragmentShaderStorageBlocks);
MGLOG_I(" GL_MAX_COMPUTE_UNIFORM_BLOCKS: %d", caps.MaxComputeUniformBlocks);
MGLOG_I(" GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS: %d", caps.MaxComputeWorkGroupInvocations);
MGLOG_I(" GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS: %d", caps.MaxShaderStorageBufferBindings);
@@ -1248,6 +1248,17 @@ namespace MobileGL {
Int MaxVertexAttribs = 16;
Int MaxComputeShaderStorageBlocks = 8;
Int MaxCombinedShaderStorageBlocks = 32;
// Per-stage GL_MAX_*_SHADER_STORAGE_BLOCKS as the host GLES driver reports them.
// The defaults are the ES 3.2 minimums (table 21.44): 0 for every graphics stage
// except fragment, which is 4. ES only gained the tessellation and geometry pnames
// in 3.2 (or with EXT_tessellation_shader / EXT_geometry_shader), so those two are
// queried behind a support check and left at the default otherwise - see
// FillInGLESCapabilities.
Int MaxVertexShaderStorageBlocks = 0;
Int MaxTessControlShaderStorageBlocks = 0;
Int MaxTessEvaluationShaderStorageBlocks = 0;
Int MaxGeometryShaderStorageBlocks = 0;
Int MaxFragmentShaderStorageBlocks = 4;
Int MaxComputeUniformBlocks = 12;
Int MaxComputeWorkGroupInvocations = 128;
Int MaxShaderStorageBufferBindings = 8;
@@ -253,6 +253,12 @@ namespace MobileGL {
return TextureInternalFormat::Depth32FStencil8;
case GL_STENCIL_INDEX8:
return TextureInternalFormat::StencilIndex8;
// The unsized stencil base format resolves to the only stencil storage there is, the
// same way the unsized colour and depth base formats below resolve to theirs. Returning
// Unknown made glTexImage2D(GL_STENCIL_INDEX) an error, which killed the negative
// clear-texture cases in their own setup before they could reach the call they test.
case GL_STENCIL_INDEX:
return TextureInternalFormat::StencilIndex8;
case GL_DEPTH_COMPONENT:
return TextureInternalFormat::DepthComponent;
case GL_DEPTH_STENCIL:
@@ -124,6 +124,9 @@ namespace MobileGL {
case TextureInternalFormat::DepthComponent32F:
case TextureInternalFormat::Depth24Stencil8:
case TextureInternalFormat::Depth32FStencil8:
// Already sized: both GL_STENCIL_INDEX8 and the unsized GL_STENCIL_INDEX resolve here,
// and there is only one stencil storage to infer.
case TextureInternalFormat::StencilIndex8:
return internalformat;
// probably we should assume unorm here?
case TextureInternalFormat::RGBA: {
+9 -5
View File
@@ -521,9 +521,11 @@ namespace MobileGL::MG_Util::SelfTest {
builder.Warn("64-bit vertex attributes",
"not supported (ES has no GL_DOUBLE vertex format, and after the fp64 demotion "
"above there is no 64-bit shader input left to feed either); "
"glVertexAttribLFormat / glVertexArrayAttribLFormat report "
"GL_INVALID_OPERATION - feed the attribute with glVertexAttribPointer(GL_FLOAT), "
"which a demoted dvec input reads correctly");
"glVertexAttribLFormat / glVertexArrayAttribLFormat succeed and their state is "
"queryable, but an ENABLED 64-bit array is DROPPED at draw and the attribute "
"reads its generic current value - feed the attribute with "
"glVertexAttribPointer(GL_FLOAT) instead, which a demoted dvec input reads "
"correctly");
if (glesFuncs.glPatchParameteri != nullptr) {
builder.Pass("Tessellation patch parameters",
"glPatchParameteri present (GL_PATCH_VERTICES reaches the driver)");
@@ -2336,8 +2338,10 @@ namespace MobileGL::MG_Util::SelfTest {
builder.Warn("64-bit vertex attributes",
"not supported; there is no 64-bit shader input left to feed after the fp64 demotion "
"above, and no VK_FORMAT_R64*_SFLOAT vertex fetch to feed it with on most devices "
"anyway. glVertexAttribLFormat reports GL_INVALID_OPERATION - feed the attribute with "
"glVertexAttribPointer(GL_FLOAT), which a demoted dvec input reads correctly");
"anyway. glVertexAttribLFormat succeeds and its state is queryable, but an ENABLED "
"64-bit array is DROPPED at pipeline build and the attribute reads its generic "
"current value - feed the attribute with glVertexAttribPointer(GL_FLOAT) instead, "
"which a demoted dvec input reads correctly");
Bool shaderDrawParameters = false;
if (vkGetPhysicalDeviceFeatures2Fn != nullptr && properties.apiVersion >= VK_API_VERSION_1_1) {
@@ -28,6 +28,9 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
HashValue(state, env.maxComputeWorkGroupSize[0]);
HashValue(state, env.maxComputeWorkGroupSize[1]);
HashValue(state, env.maxComputeWorkGroupSize[2]);
HashValue(state, env.maxComputeWorkGroupCount[0]);
HashValue(state, env.maxComputeWorkGroupCount[1]);
HashValue(state, env.maxComputeWorkGroupCount[2]);
HashValue(state, env.maxComputeWorkGroupInvocations);
HashValue(state, env.backend);
// DynamicBackendParameters is a plain aggregate of scalars; hashing its object
@@ -43,11 +46,11 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
Uint64 ComputeFrontendCompileEnvFingerprint(const CompileEnv& env) {
Uint64 state = 0xff51afd7ed558ccdull;
// The seven limits BuildTBuiltInResource copies into TBuiltInResource. Enumerated
// ONE BY ONE rather than hashed as a struct, deliberately: hashing all of
// DynamicBackendParameters would drag ~50 backend-only limits into a key that is
// supposed to be backend-agnostic, and every one of them would be a false miss.
// Keep this list in step with BuildTBuiltInResource.
// The DynamicBackendParameters limits BuildTBuiltInResource copies into
// TBuiltInResource. Enumerated ONE BY ONE rather than hashed as a struct,
// deliberately: hashing all of DynamicBackendParameters would drag ~50 backend-only
// limits into a key that is supposed to be backend-agnostic, and every one of them
// would be a false miss. Keep this list in step with BuildTBuiltInResource.
HashValue(state, env.params.MaxImageUnits);
HashValue(state, env.params.MaxDrawBuffers);
HashValue(state, env.params.MaxVertexImageUniforms);
@@ -55,6 +58,22 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
HashValue(state, env.params.MaxFragmentImageUniforms);
HashValue(state, env.params.MaxComputeImageUniforms);
HashValue(state, env.params.MaxCombinedImageUniforms);
// Added when wave3 (cb155c5b) made this one env-derived. It expands into the
// gl_MaxComputeTextureImageUnits built-in constant, so a compute module that reads
// that constant generates DIFFERENT SPIR-V under two backends that disagree on it.
HashValue(state, env.params.MaxComputeTextureImageUnits);
// The compute work-group limits, likewise added by wave3 (cb155c5b). They used to be
// hardcoded maxima in BuildTBuiltInResource, and the L1 key comment said in so many
// words that the day they became backend-derived they would have to move in here -
// that day is this merge. glslang expands BOTH of them into built-in constants
// (Initialize.cpp: "const ivec3 gl_MaxComputeWorkGroupCount = ivec3(%d,%d,%d)" and the
// same for gl_MaxComputeWorkGroupSize), so this is an INDEPENDENCE break, not merely a
// reachability one: a compute shader that reads gl_MaxComputeWorkGroupSize compiles to
// materially different SPIR-V on a driver reporting z=64 than on one reporting z=1024.
for (Uint index = 0; index < 3; ++index) {
HashValue(state, env.maxComputeWorkGroupSize[index]);
HashValue(state, env.maxComputeWorkGroupCount[index]);
}
// The two inputs to GetReflectionVertexAttribLimit. Hashed as inputs rather than as
// the resolved limit so this stays in one translation unit; that is coarser (two
// envs whose MaxVertexAttribs both exceed the storage capacity resolve to the same
@@ -75,19 +94,23 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
env->advertisedExtensions = activeBackend->GetRendererInfo().RendererGLInfo.Extensions;
}
// GL_MAX_COMPUTE_WORK_GROUP_SIZE. This is a REAL driver call on DirectGLES; it must
// happen here, on the context thread, and exactly once per context. The frontend
// minimum is the floor, matching what GL_Getter reports.
// TODO: Share these exposed compute limit helpers with GL_Getter.cpp instead of duplicating the frontend minima.
constexpr Uint kFrontendMinComputeWorkGroupSizes[3] = {1024, 1024, 64};
// GL_MAX_COMPUTE_WORK_GROUP_SIZE / _COUNT. These are REAL driver calls on DirectGLES; they
// must happen here, on the context thread, and exactly once per context. The frontend
// minimum is the floor, matching what GL_Getter reports - both sides now floor at the
// shared MIN_COMPUTE_WORK_GROUP_* constants rather than at their own copy of them.
for (Uint index = 0; index < 3; ++index) {
Int backendValue = 0;
Int backendSize = 0;
Int backendCount = 0;
if (MG_Backend::gBackendFunctionsTable.GL.GetIntegeri_v) {
MG_Backend::gBackendFunctionsTable.GL.GetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_SIZE, index,
&backendValue);
&backendSize);
MG_Backend::gBackendFunctionsTable.GL.GetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_COUNT, index,
&backendCount);
}
env->maxComputeWorkGroupSize[index] =
std::max(static_cast<Uint>(std::max(backendValue, 0)), kFrontendMinComputeWorkGroupSizes[index]);
std::max(static_cast<Uint>(std::max(backendSize, 0)), MIN_COMPUTE_WORK_GROUP_SIZE[index]);
env->maxComputeWorkGroupCount[index] =
std::max(static_cast<Uint>(std::max(backendCount, 0)), MIN_COMPUTE_WORK_GROUP_COUNT[index]);
}
constexpr Uint64 kFrontendMaxComputeWorkGroupInvocations = 1024;
+57 -17
View File
@@ -12,6 +12,18 @@
#include <MG_Backend/BackendObject.h>
namespace MobileGL::MG_Util::ShaderTranspiler {
// GL_MAX_COMPUTE_WORK_GROUP_COUNT / _SIZE core minimums (GL 4.6 core table 23.45), in ONE
// place because three separate readers have to agree on them: CaptureCompileEnv (which floors
// the backend's answer at them), GL_Getter (which answers the same query the same way) and
// BuildTBuiltInResource (whose gl_MaxComputeWorkGroup* constants a shader compares against
// the query - KHR-GL43.compute_shader.max does exactly that). They used to be three copies,
// and the z one disagreed: glslang compiled against 1024 while the context advertised 64.
inline constexpr Uint MIN_COMPUTE_WORK_GROUP_COUNT[3] = {65535, 65535, 65535};
inline constexpr Uint MIN_COMPUTE_WORK_GROUP_SIZE[3] = {1024, 1024, 64};
// GL_MAX_COMPUTE_UNIFORM_COMPONENTS, the same invariant with no backend input: the number
// glGetIntegerv answers and the number gl_MaxComputeUniformComponents expands to.
inline constexpr Int MAX_COMPUTE_UNIFORM_COMPONENTS = 1024;
// everything outside (stage, source) this reads - advertised extensions and backend limits -
// so the transformation is a pure function of its three arguments and can run on a worker
// thread.
@@ -34,7 +46,13 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
struct CompileEnv {
// --- compute limits: the ONLY former real-driver read in the pipeline ---
// GL_MAX_COMPUTE_WORK_GROUP_SIZE, already max()'d with the frontend minimum.
Uint maxComputeWorkGroupSize[3] = {1024, 1024, 64};
Uint maxComputeWorkGroupSize[3] = {MIN_COMPUTE_WORK_GROUP_SIZE[0], MIN_COMPUTE_WORK_GROUP_SIZE[1],
MIN_COMPUTE_WORK_GROUP_SIZE[2]};
// GL_MAX_COMPUTE_WORK_GROUP_COUNT, likewise. Carried for the same reason the size is:
// gl_MaxComputeWorkGroupCount expands from it at parse time, so the compile pipeline
// needs the number the context advertises without reaching back to the live backend.
Uint maxComputeWorkGroupCount[3] = {MIN_COMPUTE_WORK_GROUP_COUNT[0], MIN_COMPUTE_WORK_GROUP_COUNT[1],
MIN_COMPUTE_WORK_GROUP_COUNT[2]};
// GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS, likewise.
Uint64 maxComputeWorkGroupInvocations = 1024;
@@ -54,18 +72,38 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
// shader translation memo keys on, because L1 is backend-agnostic BY CONTRACT: two
// contexts on different GPUs compiling the same GLSL must share one L1 entry.
//
// THE LINE THIS DRAWS. "Backend-agnostic" means BACKEND IDENTITY is out - the vendor,
// the extension list, which of DirectGLES/DirectVulkan is active, every capability bit
// that merely steers the transpile. It does NOT mean backend-DERIVED VALUES are out: a
// resource limit that glslang enforces at parse, or expands into a built-in constant,
// is a front-end INPUT no matter where the number came from, and dropping it would be
// a silent miscompile rather than a backend leak. A driver with 16 vertex attribs and
// one with 32 genuinely reflect the same GLSL differently.
//
// WHAT IS IN IT (audited; re-audit whenever a new env read appears in the front end):
// * the seven DynamicBackendParameters fields BuildTBuiltInResource actually copies
// into TBuiltInResource - MaxImageUnits, MaxDrawBuffers, MaxVertexImageUniforms,
// * the DynamicBackendParameters fields BuildTBuiltInResource copies into
// TBuiltInResource - MaxImageUnits, MaxDrawBuffers, MaxVertexImageUniforms,
// MaxGeometryImageUniforms, MaxFragmentImageUniforms, MaxComputeImageUniforms,
// MaxCombinedImageUniforms. glslang enforces those at parse, so they decide
// whether a shader compiles at all and can change the link result.
// MaxCombinedImageUniforms, MaxComputeTextureImageUnits. glslang enforces those at
// parse, so they decide whether a shader compiles at all and can change the link
// result.
// * maxComputeWorkGroupSize and maxComputeWorkGroupCount, all three components each.
// These moved IN at the dev merge that brought wave3's cb155c5b, which made
// BuildTBuiltInResource read them from the env instead of hardcoding a permissive
// cap - exactly the migration the old exclusion note said would force them in
// here. They are not merely a reject gate: glslang expands both into built-in
// CONSTANTS (gl_MaxComputeWorkGroupSize, gl_MaxComputeWorkGroupCount), so a
// compute module that reads one generates different SPIR-V under two drivers that
// report different numbers.
// * MaxVertexAttribs and the HasBackend() bit: the two inputs to ProgramLinkTask's
// GetReflectionVertexAttribLimit, which bounds how many vertex input locations
// reflection records - so they change the REFLECTION the memo carries.
// Both are backend-DERIVED but front-end-CONSUMED. Dropping them would be a
// miscompile, not a backend leak: a driver with 16 vertex attribs and one with 32
// genuinely reflect the same GLSL differently.
//
// The sharding this costs is nil in practice and worth naming so nobody re-litigates
// it: a process has ONE active backend at a time and CompileEnv is re-captured when
// that changes, so no live run ever has two of these fingerprints competing for the
// same L1 entries. The cost would only appear on a future cross-device DISK tier,
// where it is the correct cost - those devices really do compile that GLSL differently.
//
// WHAT IS DELIBERATELY OUT:
// * `backend` beyond the HasBackend() bit. Nothing in the parse, the link or
@@ -81,15 +119,17 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
// fp64 GLSL translates identically with the flag on or off.)
// * the other ~50 DynamicBackendParameters fields: read by the GL getters and by
// the backends, never by the parse, the link or reflection.
// * maxComputeWorkGroupSize / maxComputeWorkGroupInvocations. Consumed ONLY by
// ValidateComputeLocalSizeLimits, a pre-parse ACCEPT/REJECT gate. A rejected
// shader fails its compile, so its program never reaches the tail of the link and
// no L1 entry is ever created under a rejecting environment; an accepted one
// produces the same SPIR-V under any limits, because BuildTBuiltInResource
// HARDCODES the compute maxima instead of reading these.
// THIS ONE IS A REACHABILITY ARGUMENT, NOT AN INDEPENDENCE ONE. If the TODO in
// BuildTBuiltInResource ("Drive glslang compute resource limits from the active
// backend") is ever done, these MUST move into this fingerprint.
// * maxComputeWorkGroupInvocations - and ONLY this one; its two former companions
// moved into the list above at the wave3 merge. glslang has no
// gl_MaxComputeWorkGroupInvocations built-in and BuildTBuiltInResource does not
// read this field, so its sole consumer is still ValidateComputeLocalSizeLimits, a
// pre-parse ACCEPT/REJECT gate. A rejected shader fails its compile, so its
// program never reaches the tail of the link and no L1 entry is ever created under
// a rejecting environment; an accepted one parses identically at any value.
// THIS ONE IS A REACHABILITY ARGUMENT, NOT AN INDEPENDENCE ONE, and it is now the
// only such argument left in this classification. The moment anything hands this
// value to glslang - a TBuiltInResource field, a built-in constant - it MUST move
// into the fingerprint, exactly as its companions just did.
Uint64 frontendFingerprint = 0; // set by CaptureCompileEnv()
Bool HasBackend() const { return backend != BackendType::Unknown; }
@@ -83,23 +83,11 @@ namespace MobileGL {
Resources.minProgramTexelOffset = -8;
Resources.maxProgramTexelOffset = 7;
Resources.maxClipDistances = 8;
Resources.maxComputeWorkGroupCountX = 65535;
Resources.maxComputeWorkGroupCountY = 65535;
Resources.maxComputeWorkGroupCountZ = 65535;
Resources.maxComputeWorkGroupSizeX = 1024;
Resources.maxComputeWorkGroupSizeY = 1024;
// TODO: Drive glslang compute resource limits from the active backend instead of this permissive cap.
// WHEN THAT IS DONE: CompileEnv::maxComputeWorkGroupSize and
// maxComputeWorkGroupInvocations must also be added to
// ComputeFrontendCompileEnvFingerprint(). They are out of the L1 memo key today
// ONLY because these maxima are hardcoded here - see the classification comment
// on CompileEnv::frontendFingerprint.
Resources.maxComputeWorkGroupSizeZ = 1024;
Resources.maxComputeUniformComponents = 1024;
Resources.maxComputeUniformComponents = MAX_COMPUTE_UNIFORM_COMPONENTS;
Resources.maxComputeTextureImageUnits = 16;
Resources.maxComputeImageUniforms = 8;
Resources.maxComputeAtomicCounters = 8;
Resources.maxComputeAtomicCounterBuffers = 1;
Resources.maxComputeAtomicCounters = MAX_ATOMIC_COUNTERS_PER_STAGE;
Resources.maxComputeAtomicCounterBuffers = MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE;
Resources.maxVaryingComponents = 60;
Resources.maxVertexOutputComponents = 64;
Resources.maxGeometryInputComponents = 64;
@@ -137,16 +125,22 @@ namespace MobileGL {
Resources.maxTessControlAtomicCounters = 0;
Resources.maxTessEvaluationAtomicCounters = 0;
Resources.maxGeometryAtomicCounters = 0;
Resources.maxFragmentAtomicCounters = 8;
Resources.maxCombinedAtomicCounters = 8;
Resources.maxAtomicCounterBindings = 1;
Resources.maxFragmentAtomicCounters = MAX_ATOMIC_COUNTERS_PER_STAGE;
Resources.maxCombinedAtomicCounters = MAX_ATOMIC_COUNTERS_PER_STAGE;
// Every atomic-counter limit below is the one glGetIntegerv answers; the shared
// constants in Types.h are what keeps the two sides from drifting apart again.
// gl_MaxAtomicCounterBindings and gl_MaxAtomicCounterBufferSize expand from these
// (Initialize.cpp), and the binding count is also the ceiling glslang checks a
// `layout(binding = N) uniform atomic_uint` against - it was 1, so every counter
// outside binding 0 failed to compile.
Resources.maxAtomicCounterBindings = MAX_ATOMIC_COUNTER_BUFFER_BINDINGS;
Resources.maxVertexAtomicCounterBuffers = 0;
Resources.maxTessControlAtomicCounterBuffers = 0;
Resources.maxTessEvaluationAtomicCounterBuffers = 0;
Resources.maxGeometryAtomicCounterBuffers = 0;
Resources.maxFragmentAtomicCounterBuffers = 1;
Resources.maxCombinedAtomicCounterBuffers = 1;
Resources.maxAtomicCounterBufferSize = 16384;
Resources.maxFragmentAtomicCounterBuffers = MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE;
Resources.maxCombinedAtomicCounterBuffers = MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE;
Resources.maxAtomicCounterBufferSize = MAX_ATOMIC_COUNTER_BUFFER_SIZE;
Resources.maxTransformFeedbackBuffers = 4;
Resources.maxTransformFeedbackInterleavedComponents = 64;
Resources.maxCullDistances = 8;
@@ -165,6 +159,14 @@ namespace MobileGL {
// Resource checking must describe the same backend contract exposed through
// glGetIntegerv. Keeping this copy local also avoids racing on a process-global
// TBuiltInResource when Iris compiles shaders concurrently.
//
// MEMO-HAZARD RULE FOR THIS BLOCK. Everything below is an env-derived value that
// glslang enforces at parse AND expands into a built-in constant, so every one of
// them can change the SPIR-V a module generates. EVERY LINE BELOW MUST BE HASHED
// BY ComputeFrontendCompileEnvFingerprint(), which is the L1 shader-translation
// memo's environment key - adding a read here without adding it there is a silent
// miscompile, not a slow path. See the classification on
// CompileEnv::frontendFingerprint.
const MG_Backend::DynamicBackendParameters fallbackParameters{};
const auto& activeBackend = MG_Backend::pActiveBackendObject;
const auto& dynamicParameters =
@@ -178,6 +180,25 @@ namespace MobileGL {
Resources.maxFragmentImageUniforms = dynamicParameters.MaxFragmentImageUniforms;
Resources.maxComputeImageUniforms = dynamicParameters.MaxComputeImageUniforms;
Resources.maxCombinedImageUniforms = dynamicParameters.MaxCombinedImageUniforms;
Resources.maxComputeTextureImageUnits = dynamicParameters.MaxComputeTextureImageUnits;
// The compute work-group limits are the env's, not the backend parameters': they
// are the only ones that come from a REAL indexed driver query, which
// CaptureCompileEnv already issued once on the GL thread and floored at the core
// minimum exactly as GL_Getter does. Reading the same snapshot here is what makes
// gl_MaxComputeWorkGroupSize and glGetIntegeri_v agree by construction
// (KHR-GL43.compute_shader.max compares them); the z component was 1024 here
// against the 64 every ES driver reports. A null env is the standalone/test entry
// point, which has no context to have queried one - the core minimums stand, which
// is what a default-constructed CompileEnv carries anyway.
const Uint* maxWorkGroupSize = env ? env->maxComputeWorkGroupSize : MIN_COMPUTE_WORK_GROUP_SIZE;
const Uint* maxWorkGroupCount = env ? env->maxComputeWorkGroupCount : MIN_COMPUTE_WORK_GROUP_COUNT;
Resources.maxComputeWorkGroupSizeX = static_cast<int>(maxWorkGroupSize[0]);
Resources.maxComputeWorkGroupSizeY = static_cast<int>(maxWorkGroupSize[1]);
Resources.maxComputeWorkGroupSizeZ = static_cast<int>(maxWorkGroupSize[2]);
Resources.maxComputeWorkGroupCountX = static_cast<int>(maxWorkGroupCount[0]);
Resources.maxComputeWorkGroupCountY = static_cast<int>(maxWorkGroupCount[1]);
Resources.maxComputeWorkGroupCountZ = static_cast<int>(maxWorkGroupCount[2]);
Resources.limits.nonInductiveForLoops = true;
Resources.limits.whileLoops = true;
@@ -10,6 +10,7 @@
#include <algorithm>
#include <cctype>
#include <cerrno>
#include <climits>
#include <cstdlib>
#include <initializer_list>
@@ -818,6 +819,116 @@ namespace {
ReplaceIdentifier(source, "GL_ARB_gpu_shader_int64", "MG_DISABLED_GL_ARB_gpu_shader_int64");
}
// GLSL 4.30 4.1.9 allows an interface-block member array to be left unsized when it is NOT the
// last member; it is then implicitly sized by the largest constant index the shader uses.
// glslang implements the SIZING - adoptImplicitArraySizes, at link - but computes the block's
// member OFFSETS at DECLARATION time (fixBlockUniformOffsets), where the array is still
// unsized and so contributes zero bytes. Every member after it is therefore laid out on top of
// it: `vec4 a[]; vec4 b;` puts BOTH at offset 0, and a shader reading `b` gets `a[0]`
// (KHR-GL43.shader_storage_buffer_object.basic-syntax iteration 6, whose degenerate triangle
// rasterizes nothing at all).
//
// The source level is the only place the two can be reconciled, because the offset pass runs
// before a single statement has been parsed. Deliberately narrow: it fires only on a `buffer`
// block (no other block kind may hold an unsized member at all), only on a member that is not
// the last one, and only when every subscript of that member's name in the source is a decimal
// literal. Anything outside that shape is left exactly as it was - and the shape itself has no
// correct behaviour today, so the rewrite cannot take a working case away.
void SizeNonFinalUnsizedBufferBlockMembers(MobileGL::String& source) {
// Both tokens must be present for the shape to exist, and "[]" is absent from essentially
// every real shader source, so this is the whole cost for them.
if (source.find("[]") == MobileGL::String::npos || source.find("buffer") == MobileGL::String::npos) {
return;
}
const auto isDecimalInteger = [](const String& text) {
return !text.empty() && std::all_of(text.begin(), text.end(), [](char ch) {
return ch >= '0' && ch <= '9';
});
};
const Vector<CodeToken> tokens = TokenizeCode(source);
const SizeT count = tokens.size();
// Pass 1: for every identifier, the largest literal index it is subscripted with (as a
// count, i.e. index + 1), or -1 once it is subscripted with anything that is not a literal.
// The declaration's own empty `[]` is neither.
MobileGL::UnorderedMap<String, long long> subscriptExtent;
for (SizeT i = 1; i < count; ++i) {
if (tokens[i].text != "[" || !IsIdentifierToken(tokens[i - 1])) continue;
if (i + 1 < count && tokens[i + 1].text == "]") continue; // the unsized declarator itself
long long& extent = subscriptExtent[tokens[i - 1].text];
if (i + 2 < count && isDecimalInteger(tokens[i + 1].text) && tokens[i + 2].text == "]") {
if (extent >= 0) {
extent = std::max(extent, std::strtoll(tokens[i + 1].text.c_str(), nullptr, 10) + 1);
}
} else {
extent = -1;
}
}
// Pass 2: one edit per repairable member, applied back to front so earlier offsets stand.
struct SizeEdit {
SizeT pos;
String text;
};
Vector<SizeEdit> edits;
for (SizeT i = 0; i < count; ++i) {
if (tokens[i].text != "buffer") continue;
SizeT cursor = i + 1;
// `buffer` is also a member MEMORY qualifier ("buffer vec4 position0;"), which is why
// the block body has to be found rather than assumed.
if (cursor < count && IsIdentifierToken(tokens[cursor])) ++cursor;
if (cursor >= count || tokens[cursor].text != "{") continue;
const SizeT bodyBegin = cursor + 1;
SizeT bodyEnd = bodyBegin;
int depth = 1;
while (bodyEnd < count) {
if (tokens[bodyEnd].text == "{") {
++depth;
} else if (tokens[bodyEnd].text == "}") {
--depth;
if (depth == 0) break;
}
++bodyEnd;
}
if (depth != 0) continue; // unterminated; glslang will have the last word
Vector<std::pair<SizeT, SizeT>> members; // [begin, end) of each member, ';' excluded
SizeT memberBegin = bodyBegin;
for (SizeT m = bodyBegin; m < bodyEnd; ++m) {
if (tokens[m].text != ";") continue;
members.emplace_back(memberBegin, m);
memberBegin = m + 1;
}
// The LAST member is deliberately untouched: an unsized array there is a run-time
// sized array, which is both legal and correctly laid out already.
for (SizeT index = 0; index + 1 < members.size(); ++index) {
const SizeT begin = members[index].first;
const SizeT end = members[index].second;
if (end < begin + 3) continue;
if (tokens[end - 1].text != "]" || tokens[end - 2].text != "[") continue;
if (!IsIdentifierToken(tokens[end - 3])) continue;
// A multi-declarator member would need one size per declarator; out of scope.
bool multipleDeclarators = false;
for (SizeT t = begin; t < end; ++t) {
if (tokens[t].text == ",") multipleDeclarators = true;
}
if (multipleDeclarators) continue;
const auto known = subscriptExtent.find(tokens[end - 3].text);
if (known == subscriptExtent.end() || known->second <= 0) continue;
edits.push_back({tokens[end - 1].begin, std::to_string(known->second)});
}
i = bodyEnd;
}
for (auto it = edits.rbegin(); it != edits.rend(); ++it) {
source.insert(it->pos, it->text);
}
}
// Rewrite the `packed` / `shared` block-packing qualifiers inside layout(...) declarations to
// `std140`. Desktop GL leaves the memory layout of such blocks to the implementation and the
// app must query member offsets; MobileGL's SPIR-V pipeline always lays uniform blocks out as
@@ -962,6 +1073,11 @@ namespace MobileGL {
FilterUnsupportedGpuShaderInt64(env, source);
CoerceUniformBlockPackingToStd140(source);
// After the packing coercion: that one rewrites `packed`/`shared` in place and so
// cannot move an offset this pass depends on, and reading the block declarations
// once both qualifiers are normalized keeps the two passes' notions of a block
// declaration identical.
SizeNonFinalUnsizedBufferBlockMembers(source);
RenameBuiltinShadowingFunctions(source);
@@ -1113,10 +1229,65 @@ namespace MobileGL {
return false;
}
bool IsDecimalIntegerToken(const String& text) {
if (text.empty()) return false;
return std::all_of(text.begin(), text.end(),
[](char ch) { return ch >= '0' && ch <= '9'; });
// One GLSL integer literal, spelled the C way: "0x"/"0X" is hexadecimal, a leading
// '0' is OCTAL, everything else decimal, and a single trailing 'u'/'U' is legal.
// strtoll with base 0 already implements exactly that detection, so the only work
// here is deciding what the tail is allowed to be.
//
// Never guesses, which is the discipline every caller depends on: a float ("1.0"),
// an unknown suffix ("3f"), an out-of-range run and a negative value all return
// false, and the caller skips the declaration rather than recording a wrong number.
bool ParseGlslIntegerLiteral(const String& text, long long& out) {
if (text.empty() || text.front() < '0' || text.front() > '9') return false;
errno = 0;
char* tail = nullptr;
const long long value = std::strtoll(text.c_str(), &tail, 0);
if (tail == text.c_str() || errno == ERANGE || value < 0) return false;
const String suffix = text.substr(static_cast<SizeT>(tail - text.c_str()));
if (!suffix.empty() && suffix != "u" && suffix != "U") return false;
out = value;
return true;
}
// glslang reflects an array-of-arrays default-block uniform as ONE RECORD PER
// outer-index tuple, carrying the innermost array type: `float u[2][3]` becomes
// "u[0][0]" and "u[1][0]" (that last "[0]" is EShReflectionBasicArraySuffix). The
// linker resolves such a name by stripping the single trailing "[0]", so it looks
// up "u[1]" - a key the root entry alone cannot answer, and the whole declaration
// silently loses its explicit location.
//
// Emit those pre-flattened keys here, next to the root, so the result is
// order-independent: each carries the location its own element starts at (element
// i of `float u[2][3]` at location L starts at L + i*3). Identifiers cannot
// contain brackets, so a synthesized key never collides with a real uniform name,
// and a 1-D array needs none of this - stripping "[0]" already reaches the root.
void RecordArrayOfArraysElementLocations(const String& name, const Vector<long long>& dimensions,
long long baseLocation,
MobileGL::UnorderedMap<String, MobileGL::Int>& locations) {
if (dimensions.size() < 2) return;
// A pathological declaration must not be able to blow up the map; past the cap
// only the root entry stands, which is what every case used to get.
constexpr long long kMaxSynthesizedKeys = 4096;
const long long innerSpan = dimensions.back();
const SizeT outerDimensions = dimensions.size() - 1;
long long elementCount = 1;
for (SizeT d = 0; d < outerDimensions; ++d) {
elementCount *= dimensions[d];
if (elementCount > kMaxSynthesizedKeys) return;
}
for (long long element = 0; element < elementCount; ++element) {
String key = name;
long long remainder = element;
for (SizeT d = 0; d < outerDimensions; ++d) {
long long stride = 1;
for (SizeT inner = d + 1; inner < outerDimensions; ++inner) stride *= dimensions[inner];
key += "[" + std::to_string(remainder / stride) + "]";
remainder %= stride;
}
locations.emplace(key, static_cast<MobileGL::Int>(
std::min(baseLocation + element * innerSpan,
static_cast<long long>(INT_MAX / 2))));
}
}
// Parses one brace-free depth-0 statement [begin, end) and records its
@@ -1129,6 +1300,7 @@ namespace MobileGL {
MobileGL::UnorderedMap<String, MobileGL::Int>& locations) {
using MobileGL::Int;
long long location = -1;
long long literal = 0;
bool sawUniform = false;
SizeT declaratorBegin = end;
@@ -1144,9 +1316,9 @@ namespace MobileGL {
} else if (layoutToken == ")") {
--parenDepth;
} else if (parenDepth == 1 && layoutToken == "location" && j + 2 < end &&
tokens[j + 1].text == "=" && IsDecimalIntegerToken(tokens[j + 2].text)) {
location = std::min(std::strtoll(tokens[j + 2].text.c_str(), nullptr, 10),
static_cast<long long>(INT_MAX / 2));
tokens[j + 1].text == "=" &&
ParseGlslIntegerLiteral(tokens[j + 2].text, literal)) {
location = std::min(literal, static_cast<long long>(INT_MAX / 2));
j += 2;
}
++j;
@@ -1175,21 +1347,25 @@ namespace MobileGL {
const String& name = tokens[k].text;
++k;
long long span = 1;
Vector<long long> dimensions;
while (k < end && tokens[k].text == "[") {
++k;
long long dimension = 1;
if (k < end && IsDecimalIntegerToken(tokens[k].text)) {
dimension = std::strtoll(tokens[k].text.c_str(), nullptr, 10);
if (k < end && ParseGlslIntegerLiteral(tokens[k].text, literal)) {
dimension = literal;
++k;
}
if (k >= end || tokens[k].text != "]") return; // sized by expression; bail out
++k;
span *= std::max(1ll, std::min(dimension, static_cast<long long>(INT_MAX / 2)));
dimensions.push_back(
std::max(1ll, std::min(dimension, static_cast<long long>(INT_MAX / 2))));
span *= dimensions.back();
}
// Keep the first sighting: a duplicate can only come from alternative
// preprocessor branches declaring the same name.
locations.emplace(name, static_cast<Int>(std::min(
nextLocation, static_cast<long long>(INT_MAX / 2))));
RecordArrayOfArraysElementLocations(name, dimensions, nextLocation, locations);
nextLocation += span;
if (k >= end) break;
if (tokens[k].text == "=") { // skip an initializer up to the declarator comma
@@ -1225,6 +1401,7 @@ namespace MobileGL {
MobileGL::UnorderedMap<String, MobileGL::Uint>& bindings) {
using MobileGL::Int;
long long binding = -1;
long long literal = 0;
bool sawUniform = false;
SizeT declaratorBegin = end;
@@ -1240,9 +1417,9 @@ namespace MobileGL {
} else if (layoutToken == ")") {
--parenDepth;
} else if (parenDepth == 1 && layoutToken == "binding" && j + 2 < end &&
tokens[j + 1].text == "=" && IsDecimalIntegerToken(tokens[j + 2].text)) {
binding = std::min(std::strtoll(tokens[j + 2].text.c_str(), nullptr, 10),
static_cast<long long>(INT_MAX / 2));
tokens[j + 1].text == "=" &&
ParseGlslIntegerLiteral(tokens[j + 2].text, literal)) {
binding = std::min(literal, static_cast<long long>(INT_MAX / 2));
j += 2;
}
++j;
@@ -1275,7 +1452,7 @@ namespace MobileGL {
++k;
while (k < end && tokens[k].text == "[") {
++k;
if (k < end && IsDecimalIntegerToken(tokens[k].text)) ++k;
if (k < end && ParseGlslIntegerLiteral(tokens[k].text, literal)) ++k;
if (k >= end || tokens[k].text != "]") return; // sized by expression; bail out
++k;
}
@@ -1332,6 +1509,100 @@ namespace MobileGL {
return bindings;
}
namespace {
// Binding points a storage-block declaration starting at `bufferPos` occupies.
// One for a scalar instance (and for the "layout(...) buffer;" default-qualifier
// form, which declares no block at all); the element count for an instance array,
// whose elements take base, base+1, ... (GLSL 4.30 4.4.5). -1 means "the grammar
// here is outside this scanner's narrow subset", i.e. do not judge this one.
long long StorageBlockBindingPointCount(const Vector<CodeToken>& tokens, SizeT bufferPos,
SizeT count) {
SizeT k = bufferPos + 1;
if (k < count && IsIdentifierToken(tokens[k])) ++k; // block type name
if (k >= count || tokens[k].text != "{") return 1;
MobileGL::Int braceDepth = 0;
while (k < count) {
if (tokens[k].text == "{") {
++braceDepth;
} else if (tokens[k].text == "}") {
--braceDepth;
if (braceDepth == 0) {
++k;
break;
}
}
++k;
}
if (braceDepth != 0) return -1; // unterminated block: not this scanner's business
if (k < count && IsIdentifierToken(tokens[k])) ++k; // instance name
if (k >= count || tokens[k].text != "[") return 1;
long long elementCount = 0;
if (k + 2 < count && ParseGlslIntegerLiteral(tokens[k + 1].text, elementCount) &&
tokens[k + 2].text == "]") {
return std::max<long long>(1, elementCount);
}
return -1; // sized by an expression, or unsized
}
} // namespace
std::optional<String> FindShaderStorageBindingViolation(const String& source, Int maxBindings) {
// A backend that advertises nothing has no ceiling to enforce.
if (maxBindings <= 0) return std::nullopt;
// Fast path: no storage block, nothing to check. Both keywords are required for a
// violation to exist, and the pair is absent from almost every shader-pack source.
if (source.find("buffer") == String::npos || source.find("binding") == String::npos) {
return std::nullopt;
}
const Vector<CodeToken> tokens = TokenizeCode(source);
const SizeT count = tokens.size();
// The binding the qualifier run currently being scanned declared, -1 for none.
// Several layout(...) lists may precede one declaration and the later one wins,
// which is the same accumulate-then-consume shape the extractors above use.
long long binding = -1;
long long literal = 0;
for (SizeT pos = 0; pos < count; ++pos) {
const String& text = tokens[pos].text;
if (text == "layout" && pos + 1 < count && tokens[pos + 1].text == "(") {
SizeT j = pos + 2;
Int parenDepth = 1;
while (j < count && parenDepth > 0) {
const String& layoutToken = tokens[j].text;
if (layoutToken == "(") {
++parenDepth;
} else if (layoutToken == ")") {
--parenDepth;
} else if (parenDepth == 1 && layoutToken == "binding" && j + 2 < count &&
tokens[j + 1].text == "=" &&
ParseGlslIntegerLiteral(tokens[j + 2].text, literal)) {
binding = std::min(literal, static_cast<long long>(INT_MAX / 2));
j += 2;
}
++j;
}
pos = j - 1;
continue;
}
if (text == "buffer") {
const long long points = binding >= 0 ? StorageBlockBindingPointCount(tokens, pos, count) : -1;
if (points > 0 && binding + points > static_cast<long long>(maxBindings)) {
return "ERROR: invalid value " + std::to_string(binding) +
" for layout specifier 'binding': a shader storage block occupying " +
std::to_string(points) + " binding point(s) from there passes " +
"GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS (" + std::to_string(maxBindings) + ").";
}
binding = -1;
continue;
}
// Qualifiers may sit between the layout list and the `buffer` keyword; anything
// else ends the run, so a binding never leaks onto an unrelated declaration.
if (!IsNonLayoutQualifierKeyword(text)) binding = -1;
}
return std::nullopt;
}
UnorderedMap<String, Int> ExtractExplicitUniformLocations(const String& source) {
UnorderedMap<String, Int> locations;
// Fast path: without the qualifier keyword there is nothing to extract.
@@ -64,6 +64,17 @@ namespace MobileGL {
// mapIO can capture them, so they are recovered lexically (same narrow
// grammar discipline as ExtractExplicitUniformLocations).
UnorderedMap<String, Uint> ExtractExplicitOpaqueBindings(const String& source);
// A shader storage block whose layout(binding = N) reaches or passes
// GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS is a compile-time error in GL 4.3 core 4.4.5,
// and an arrayed block instance takes CONSECUTIVE points, so the last element is what
// has to fit. glslang cannot raise it for MobileGL: every shader is parsed as a Vulkan
// client under relaxed rules, where the GL ceilings do not apply, and TBuiltInResource
// has no storage-buffer binding field to check against in the first place. Returns the
// compile-error text for the first violation, or nullopt for a clean source.
// `maxBindings` is what glGetIntegerv answers for that pname; a non-positive value
// means "nothing to check against" and every declaration passes.
std::optional<String> FindShaderStorageBindingViolation(const String& source, Int maxBindings);
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
@@ -326,6 +326,55 @@ namespace MobileGL {
SPVC_CHK_RETURN
}
// "gl_AtomicCounterBlock_5" -> 5, -1 for anything that is not one of those blocks.
// The suffix is the GL atomic-counter binding the application declared, and after
// the relaxed lowering it is the only place that number still exists.
static Int AtomicCounterBlockBinding(const char* blockName) {
if (blockName == nullptr) return -1;
const SizeT prefixLength = std::strlen(ATOMIC_COUNTER_BLOCK_PREFIX);
const String name = blockName;
if (name.length() <= prefixLength + 1) return -1;
if (name.compare(0, prefixLength, ATOMIC_COUNTER_BLOCK_PREFIX) != 0) return -1;
if (name[prefixLength] != '_') return -1;
Int binding = 0;
for (SizeT i = prefixLength + 1; i < name.length(); ++i) {
if (name[i] < '0' || name[i] > '9') return -1;
binding = binding * 10 + (name[i] - '0');
if (binding > 0x0FFFFFFF) return -1;
}
return binding;
}
spvc_result SpvcSession::SetAtomicCounterBlockBindings(Int topBinding, Vector<Int>& outGlBindings) {
if (!(usage & SessionUsageBit::Transpile)) return SPVC_ERROR_INVALID_ARGUMENT;
SPVC_CHK_INIT
const spvc_reflected_resource* list = nullptr;
size_t count = 0;
SPVC_CHK_RESULT(spvc_resources_get_resource_list_for_type(
resources, SPVC_RESOURCE_TYPE_STORAGE_BUFFER, &list, &count));
for (size_t i = 0; i < count; ++i) {
auto& resource = list[i];
// The block TYPE name: glslang gives the synthesized block an EMPTY instance
// name, so resource.name carries nothing to match on. Read before Compile(),
// which is where SPIRV-Cross renames the reserved "gl_" prefix away.
const Int glBinding = AtomicCounterBlockBinding(
spvc_compiler_get_name(compiler, resource.base_type_id));
if (glBinding < 0) continue;
const Int esslBinding = topBinding - glBinding;
if (esslBinding < 0) {
MGLOG_E_ONCE("Atomic counter binding %d needs more shader storage binding points than this "
"driver has; its counters will not be updated.",
glBinding);
continue;
}
spvc_compiler_set_decoration(compiler, resource.id, SpvDecorationBinding,
static_cast<unsigned>(esslBinding));
outGlBindings.push_back(glBinding);
}
SPVC_CHK_RETURN
}
spvc_result SpvcSession::Compile(const char** result) {
if (!(usage & SessionUsageBit::Transpile)) return SPVC_ERROR_INVALID_ARGUMENT;
SPVC_CHK_INIT
@@ -105,6 +105,21 @@ namespace MobileGL {
// arrayed block's elements are separate GL resources spelled "B[0]", "B[1]").
// Entries with a negative value mean "never rebound" and are skipped.
spvc_result SetShaderStorageBlockBinding(const UnorderedMap<String, Int>& bindings);
// Points every synthesized atomic-counter block at a RESERVED storage-block
// binding and reports which GL atomic-counter bindings the module declares.
//
// glslang's relaxed parse rewrote each atomic_uint into a member of
// gl_AtomicCounterBlock_<N>, where N is the GL binding the application declared;
// the block itself was then auto-mapped to whatever storage-block binding was
// free, which has no relation to N and can collide with an SSBO the application
// binds itself. Slot N is taken from the TOP of the driver's range downwards
// (`topBinding - N`) so the reserved window never overlaps the low bindings
// applications use, and a block whose slot would be negative is left alone and
// NOT reported - the caller binds nothing there rather than aliasing.
//
// `outGlBindings` is appended to, so one vector can collect a whole program's
// stages; it may repeat a binding declared by several of them.
spvc_result SetAtomicCounterBlockBindings(Int topBinding, Vector<Int>& outGlBindings);
spvc_result Compile(const char** result);
const SpvcMetadata& GetMetadata() const;
const char* GetLastErrorString() const;
@@ -119,6 +119,7 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
builder.Value(inputs.maxDepthTextureSamples);
builder.Value(inputs.advertisedMaxSamples);
builder.Value(static_cast<Uint32>(inputs.esslVersion));
builder.Value(inputs.atomicCounterEsslBindingTop);
builder.Value(static_cast<Uint8>(inputs.enableSpirvValidation));
static const std::set<String> kEmptySet;
builder.NameSet(inputs.xfbCaptureBlockNames ? *inputs.xfbCaptureBlockNames : kEmptySet);
@@ -135,6 +136,7 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
SizeT EsslTranslationResultBytes(const EsslTranslationResult& result) {
SizeT bytes = result.essl.size();
for (const String& name : result.flattenedXfbBlockNames) bytes += name.size();
bytes += result.atomicCounterGlBindings.size() * sizeof(Int);
return bytes;
}
@@ -428,6 +428,8 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
// derived from LIVE glBindImageTexture state and is the one genuinely
// per-draw-state input in here;
// * the storage-block binding overrides handed to SPIRV-Cross;
// * the atomic-counter binding top, which SetAtomicCounterBlockBindings turns into the
// layout(binding=) qualifier every synthesized counter block is printed with;
// * the ESSL version SPIRV-Cross targets (ResolveBackendEsslVersion, i.e. the
// driver's GLES version) - the remaining two SPIRV-Cross options are
// compile-time constants (GLSL_ES true, VULKAN_SEMANTICS false);
@@ -442,6 +444,13 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
// feedback capture list follows them, so a payload that dropped them would
// silently un-rename every capture on a cache hit.
std::set<String> flattenedXfbBlockNames;
// Which GL atomic-counter binding points THIS stage's synthesized
// gl_AtomicCounterBlock_<N> blocks named, as SetAtomicCounterBlockBindings reported
// them. Same contract as the XFB names above and here for the same reason: the draw
// path re-issues exactly these as storage-buffer bindings, so a payload that dropped
// them would leave every counter buffer unbound on a hit - a program that renders but
// never increments a counter, which is far harder to notice than a broken shader.
Vector<Int> atomicCounterGlBindings;
};
using EsslTranslationResultPtr = SharedPtr<const EsslTranslationResult>;
@@ -462,6 +471,13 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
const UnorderedMap<String, Uint>* glFormatByUniformName = nullptr;
const UnorderedMap<String, Int>* storageBlockBindingOverrides = nullptr;
// The top of the reserved storage-block window atomic-counter blocks are moved into
// (`top - N` for GL binding N). Derived from the driver's
// GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS, so it differs per driver, and it is PRINTED
// INTO the emitted ESSL as a layout(binding=) qualifier - which makes it key material,
// not just a caller's bookkeeping.
Int atomicCounterEsslBindingTop = -1;
// --- SPIRV-Cross options ---
Uint esslVersion = 300;
+29
View File
@@ -14,6 +14,35 @@ namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
inline const char* GLOBAL_UBO_NAME = "MGL_GLOBAL_UBO";
// glslang's Vulkan-relaxed parse rewrites every atomic_uint into a member of a
// synthesized storage block named "<this>_<GL atomic-counter binding>"
// (ParseContextBase::growAtomicCounterBlock). That block IS the GL atomic counter
// buffer, and the trailing number is the only place the GL binding survives.
inline constexpr const char* ATOMIC_COUNTER_BLOCK_PREFIX = "gl_AtomicCounterBlock";
// Atomic-counter limits, in ONE place because GL 4.6 requires glGetIntegerv and the
// shading language's gl_MaxAtomicCounter* constants to report the same numbers
// (KHR-GL43.shader_atomic_counters.basic-glsl-built-in compares them directly).
// They used to be two unreconciled tables: BuildTBuiltInResource compiled against one
// binding and glGetIntegerv advertised thirty-six.
//
// The binding count is what the backends can actually serve. glslang lowers every
// atomic_uint onto a storage block, so one counter BUFFER costs one of the ES
// driver's shader-storage binding points, and DirectGLES reserves this many at the
// top of that range (see AtomicCounterEsslBindingTop in the DirectGLES managers).
inline constexpr Int MAX_ATOMIC_COUNTER_BUFFER_BINDINGS = 8;
// GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE, in basic machine units. Independent of the
// counter COUNTS below - it bounds the byte offset a counter may be declared at, and
// the conformance suite declares counters well past the eighth one (offsets 32 and
// 128 in a two-counter buffer). KHR-GL44.multi_bind splits it evenly across every
// advertised binding point and binds them all in one glBindBuffersRange, so it must
// stay a multiple of, and comfortably larger than, four times the binding count.
inline constexpr Int MAX_ATOMIC_COUNTER_BUFFER_SIZE = 16384;
// GL_MAX_{FRAGMENT,COMPUTE,COMBINED}_ATOMIC_COUNTER_BUFFERS and the matching
// _ATOMIC_COUNTERS. Eight is the GL 4.6 core minimum for the compute stage
// (table 23.45) and every other stage this implementation serves counters on.
inline constexpr Int MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE = 8;
inline constexpr Int MAX_ATOMIC_COUNTERS_PER_STAGE = 8;
struct EmptyType {};
@@ -138,6 +138,11 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
case TextureInternalFormat::DepthComponent32F:
out = {1, ShadowComponent::Float32, false};
return true;
// Stencil is the one single-channel INTEGER shadow that is not a colour format: eight
// bits, held as an unsigned index rather than a normalized value.
case TextureInternalFormat::StencilIndex8:
out = {1, ShadowComponent::UInt8, true};
return true;
case TextureInternalFormat::R8:
case TextureInternalFormat::Red: out = {1, ShadowComponent::UNorm8, false}; return true;
@@ -336,8 +341,13 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
case TextureInputFormat::BGRAInteger: out = {{2, 1, 0, 3}, 4, true}; return true;
// A depth value converts like a single normalized/float channel.
case TextureInputFormat::DepthComponent: out = {{0, -1, -1, -1}, 1, false}; return true;
// A stencil index is a single INTEGER channel (GL 4.6 core 8.4.4.3). Without this the
// upload fell to the raw-memcpy branch, which copies the client element width into the
// one-byte STENCIL_INDEX8 shadow verbatim - right for GL_UNSIGNED_BYTE and wrong for
// every wider type. The state layer keeps this paired with stencil-only storage.
case TextureInputFormat::StencilIndex: out = {{0, -1, -1, -1}, 1, true}; return true;
default:
return false; // stencil / packed depth-stencil / unknown
return false; // packed depth-stencil / unknown
}
}
@@ -806,6 +816,14 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
return IsRawPackedPixelPair(packedInternal.kind, clientFormat, clientType);
}
Bool HasRedundantPackedEncoding(TextureInternalFormat internalFormat) {
InternalPackedLayout packedInternal{};
if (!GetInternalPackedLayout(internalFormat, packedInternal)) {
return false;
}
return packedInternal.kind == PackedInternalKind::FloatRGB9E5;
}
// assume 8 bit per channel
// swizzle.size() == channel count
void ProcessColorSwizzle(void* data, SizeT pixelCount, const Vector<TextureSwizzleParam>& swizzle) {
@@ -990,6 +1008,11 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
const void* inputPixel,
Vector<Uint8>& outputPixel) {
outputPixel.clear();
// A stencil index became a transferable format when STENCIL_INDEX8 texture storage did (see
// GetUnpackChannelMapping), but this helper serves glClearBufferData, whose internal formats
// are all colour (GL 4.6 core table 8.20): a stencil pattern would otherwise pass the size
// check and land silently in an equally-sized colour store.
if (textureInputFormat == TextureInputFormat::StencilIndex) return false;
if (inputPixel == nullptr || !IsValidUnpackPixelPair(textureInputFormat, inputDataType)) return false;
PixelStoreParameters params{};
@@ -44,6 +44,17 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
Bool IsRawPackedPixelTransfer(TextureInternalFormat internalFormat, TextureInputFormat clientFormat,
TexturePixelDataType clientType);
// True when a packed internal format has REDUNDANT encodings, so decoding a texel and
// re-encoding it keeps the VALUE but not the BITS. Only RGB9_E5 does: its shared exponent can
// be lowered with the mantissas shifted up to match, and the spec's encoder always emits the
// canonical form. RGB10_A2, RGB10_A2UI and R11F_G11F_B10F round-trip through float32
// bit-exactly, so a GPU readback can answer for them.
//
// This is what decides whether the CPU shadow has to stay authoritative for a format: a
// readback of an RGB9_E5 level through a colour attachment cannot return the stored words, no
// matter how well behaved the driver is.
Bool HasRedundantPackedEncoding(TextureInternalFormat internalFormat);
// Decodes the canonical shadow-mip storage of `internalFormat` into wide RGBA texels for CPU
// readback (GetTexImage of non-renderable formats). Non-integer formats fill outWide with
// 4 Floats per texel; integer formats fill it with 4 Uint32/Int32 per texel and set