[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 { namespace MG_State::GLState {
class FramebufferObject; class FramebufferObject;
class ITextureObject; class ITextureObject;
class RenderbufferObject;
} }
enum class BackendType { enum class BackendType {
@@ -24,6 +25,19 @@ namespace MobileGL {
}; };
namespace MG_Backend { 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 { enum class FormatCapability : Uint64 {
Creatable = 1ull << 0, Creatable = 1ull << 0,
@@ -160,9 +174,9 @@ namespace MobileGL {
GLsizei height, GLint border); GLsizei height, GLint border);
void (*CopyTexSubImage2D)(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, void (*CopyTexSubImage2D)(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y,
GLsizei width, GLsizei height); 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, 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, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth); GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
void (*GenerateMipmap)(GLenum target); void (*GenerateMipmap)(GLenum target);
@@ -326,6 +340,22 @@ namespace MobileGL {
Int MaxVertexAttribs = 16; Int MaxVertexAttribs = 16;
Int MaxComputeShaderStorageBlocks = 8; Int MaxComputeShaderStorageBlocks = 8;
Int MaxCombinedShaderStorageBlocks = 32; 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 MaxComputeUniformBlocks = 12;
Int MaxComputeWorkGroupInvocations = 128; Int MaxComputeWorkGroupInvocations = 128;
Int MaxShaderStorageBufferBindings = 8; Int MaxShaderStorageBufferBindings = 8;
@@ -1251,9 +1251,31 @@ namespace MobileGL::MG_Backend::DirectGLES {
static_cast<Int>(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS)); static_cast<Int>(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS));
m_dynamicParameters.MaxComputeShaderStorageBlocks = m_GLESCapabilities.MaxComputeShaderStorageBlocks; m_dynamicParameters.MaxComputeShaderStorageBlocks = m_GLESCapabilities.MaxComputeShaderStorageBlocks;
m_dynamicParameters.MaxCombinedShaderStorageBlocks = m_GLESCapabilities.MaxCombinedShaderStorageBlocks; 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.MaxComputeUniformBlocks = m_GLESCapabilities.MaxComputeUniformBlocks;
m_dynamicParameters.MaxComputeWorkGroupInvocations = m_GLESCapabilities.MaxComputeWorkGroupInvocations; 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 // 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 // on a host without buffer textures it is knowingly a floor MobileGL cannot honour rather
// than a driver answer (m_GLESCapabilities.MaxTextureBufferSizeIsDriverReported says // 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) { void SyncBoundBuffer(BufferTarget target, GLenum glTarget) {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); 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) { void SyncImageTextureBinding(Uint unit) {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif #endif
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(unit)); auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(unit));
TrackWritableImageBufferUnit(unit, IsWritableImageBufferTexture(imageBinding)); TrackWritableImageBufferUnit(unit, IsWritableImageBufferTexture(imageBinding));
if (imageBinding.Texture && unit + 1 > g_imageUnitHighWaterMark) {
g_imageUnitHighWaterMark = unit + 1;
}
if (!imageBinding.Texture) { if (!imageBinding.Texture) {
g_GLESFuncs.glBindImageTexture(unit, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8); g_GLESFuncs.glBindImageTexture(unit, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
return; return;
@@ -1453,6 +1507,40 @@ namespace MobileGL::MG_Backend::DirectGLES {
SyncImageTextureBinding(unit); 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 TextureImpl
namespace FramebufferImpl { namespace FramebufferImpl {
@@ -2396,6 +2484,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
syncBit & DrawSyncBit::IndirectBuffer); syncBit & DrawSyncBit::IndirectBuffer);
VertexArrayImpl::SyncCurrentVAO(currentVAO, vaoTwin); VertexArrayImpl::SyncCurrentVAO(currentVAO, vaoTwin);
TextureImpl::SyncNeccessaryTextures(textureKeys); 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 // 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. // stores into a buffer texture from the FRAGMENT stage, not from a dispatch.
TextureImpl::MarkWritableImageBufferTexturesGpuWritten(); 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 #ifdef TRACY_ENABLE
ZoneScopedNC("BindSamplerUnit", TRACY_ZONECOLOR_BACKEND); ZoneScopedNC("BindSamplerUnit", TRACY_ZONECOLOR_BACKEND);
@@ -5683,15 +5786,27 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_GLESFuncs.glDispatchComputeIndirect(indirect); 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) { void MemoryBarrier(GLbitfield barriers) {
g_GLESFuncs.glMemoryBarrier(barriers); g_GLESFuncs.glMemoryBarrier(LowerAtomicCounterBarrierBits(barriers));
if (g_GLESCapabilities.IsAngleRenderer) { if (g_GLESCapabilities.IsAngleRenderer) {
g_GLESFuncs.glFlush(); g_GLESFuncs.glFlush();
} }
} }
void MemoryBarrierByRegion(GLbitfield barriers) { 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. // 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 // 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 // ES 2D array that backs it addresses them with z/depth, so the two axes swap with the
// target. // 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 { struct GLESCopyImageEndpoint {
GLenum target = GL_TEXTURE_2D; 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 x = 0;
GLint y = 0; GLint y = 0;
GLint z = 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) { // The renderbuffer twin of TextureImpl::SyncTextureObjectToBackend: the same
const TextureTarget stateTarget = MG_Util::ConvertGLEnumToTextureTarget(appTarget); // find-or-create-then-sync the framebuffer attachment walk does (see SyncAttachmentObject),
GLESCopyImageEndpoint endpoint{}; // reachable from a path that has a renderbuffer but no framebuffer.
endpoint.target = TextureImpl::ConvertTextureTargetToBackendGLEnum(stateTarget); static SharedPtr<RenderbufferImpl::BackendRenderbufferObject> SyncRenderbufferObjectToBackend(
if (stateTarget == TextureTarget::Texture1DArray) { const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbufferObject) {
endpoint.x = x; if (!renderbufferObject) return nullptr;
endpoint.y = 0; SharedPtr<RenderbufferImpl::BackendRenderbufferObject> backendRenderbufferObject;
endpoint.z = y; if (auto* slot = RenderbufferImpl::g_backendRenderbufferObjects.Find(renderbufferObject.get())) {
return endpoint; backendRenderbufferObject = *slot;
} else {
auto& newSlot = RenderbufferImpl::g_backendRenderbufferObjects.GetOrCreate(renderbufferObject);
if (!newSlot) {
newSlot = MakeShared<RenderbufferImpl::BackendRenderbufferObject>();
}
backendRenderbufferObject = newSlot;
} }
endpoint.x = x; backendRenderbufferObject->SyncToBackend(renderbufferObject);
endpoint.y = y; return backendRenderbufferObject;
endpoint.z = z; }
return endpoint;
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 // 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); 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, 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, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) { GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
// BY VALUE, not by reference. SyncTextureObjectToBackend hands back a reference to a GLESCopyImageEndpoint src{};
// slot inside the backend texture registry, and the second call mutates that very map: GLESCopyImageEndpoint dst{};
// 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);
// The DirectVulkan half of this entry point died exactly here, on a texture whose sync // 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 // 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 // supposed to catch it expands to nothing. The four Name() calls below are the same
// are the same dereference. The frontend validator is what keeps this unreachable and // dereference. The frontend validator is what keeps this unreachable and what reports
// what reports the error the application is owed; declining is only how a future gap up // the error the application is owed; declining is only how a future gap up there stops
// there stops being a crash. See the level guard in VulkanRenderer::CopyImageSubData. // being a crash. See the level guard in VulkanRenderer::CopyImageSubData.
if (!srcBackendTexture || !dstBackendTexture) { if (!MakeGLESCopyImageEndpoint(srcEndpoint, srcTarget, srcX, srcY, srcZ, src) ||
MGLOG_E_ONCE("%s: source or destination texture failed to sync; declining the copy", __func__); !MakeGLESCopyImageEndpoint(dstEndpoint, dstTarget, dstX, dstY, dstZ, dst)) {
MGLOG_E_ONCE("%s: source or destination image failed to sync; declining the copy", __func__);
return; return;
} }
const GLESCopyImageEndpoint src = MakeGLESCopyImageEndpoint(srcTarget, srcX, srcY, srcZ);
const GLESCopyImageEndpoint dst = MakeGLESCopyImageEndpoint(dstTarget, dstX, dstY, dstZ);
GLsizei copyHeight = srcHeight; GLsizei copyHeight = srcHeight;
GLsizei copyDepth = srcDepth; GLsizei copyDepth = srcDepth;
ApplyGLESCopyImageExtent(srcTarget, dstTarget, copyHeight, copyDepth); ApplyGLESCopyImageExtent(srcTarget, dstTarget, copyHeight, copyDepth);
const Bool srcIsDepth = MG_Util::IsDepthFormatInternalFormat(srcTexture->GetFormat()); const TextureInternalFormat srcFormat = GetCopyImageEndpointFormat(srcEndpoint);
const Bool dstIsDepth = MG_Util::IsDepthFormatInternalFormat(dstTexture->GetFormat()); const TextureInternalFormat dstFormat = GetCopyImageEndpointFormat(dstEndpoint);
const Bool srcStencil = MG_Util::IsStencilFormatInternalFormat(srcTexture->GetFormat()); // Both emulation fallbacks below are written against TEXTURE ids and texture targets, so
const Bool dstStencil = MG_Util::IsStencilFormatInternalFormat(dstTexture->GetFormat()); // an endpoint that is a renderbuffer takes the native ES copy - which accepts
if (srcIsDepth || dstIsDepth || srcStencil || dstStencil) { // 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, MOBILEGL_ASSERT(srcIsDepth && dstIsDepth && !srcStencil && !dstStencil,
"DirectGLES CopyImageSubData only supports depth-only image copies."); "DirectGLES CopyImageSubData only supports depth-only image copies.");
MOBILEGL_ASSERT(src.target == GL_TEXTURE_2D && dst.target == GL_TEXTURE_2D, MOBILEGL_ASSERT(src.target == GL_TEXTURE_2D && dst.target == GL_TEXTURE_2D,
"DirectGLES depth CopyImageSubData only supports GL_TEXTURE_2D."); "DirectGLES depth CopyImageSubData only supports GL_TEXTURE_2D.");
MOBILEGL_ASSERT(src.z == 0 && dst.z == 0 && copyDepth == 1, MOBILEGL_ASSERT(src.z == 0 && dst.z == 0 && copyDepth == 1,
"DirectGLES depth CopyImageSubData only supports single-layer copies."); "DirectGLES depth CopyImageSubData only supports single-layer copies.");
BlitDepthTexture2D(srcBackendTexture->GetBackendTextureId(), srcLevel, src.x, src.y, srcWidth, copyHeight, BlitDepthTexture2D(src.Name(), srcLevel, src.x, src.y, srcWidth, copyHeight,
dstBackendTexture->GetBackendTextureId(), dstLevel, dst.x, dst.y, srcWidth, copyHeight); dst.Name(), dstLevel, dst.x, dst.y, srcWidth, copyHeight);
return; return;
} }
if (srcTexture->GetFormat() == TextureInternalFormat::R32F || if (!anyRenderbuffer &&
dstTexture->GetFormat() == TextureInternalFormat::R32F) { (srcFormat == TextureInternalFormat::R32F || dstFormat == TextureInternalFormat::R32F)) {
// The single glGetError below decides the fallback dispatch, and // The single glGetError below decides the fallback dispatch, and
// ErrorLopper::Clear is compiled out at the default log level - drain // ErrorLopper::Clear is compiled out at the default log level - drain
// with the always-live helper so a stale flag cannot misroute a // with the always-live helper so a stale flag cannot misroute a
// succeeded native copy into the 2D-only fallback. // succeeded native copy into the 2D-only fallback.
ClearGLErrors(); ClearGLErrors();
g_GLESFuncs.glCopyImageSubData(srcBackendTexture->GetBackendTextureId(), src.target, srcLevel, src.x, src.y, src.z, g_GLESFuncs.glCopyImageSubData(src.Name(), src.target, srcLevel, src.x, src.y, src.z,
dstBackendTexture->GetBackendTextureId(), dst.target, dstLevel, dst.x, dst.y, dst.z, dst.Name(), dst.target, dstLevel, dst.x, dst.y, dst.z,
srcWidth, copyHeight, copyDepth); srcWidth, copyHeight, copyDepth);
const GLenum copyImageError = g_GLESFuncs.glGetError(); const GLenum copyImageError = g_GLESFuncs.glGetError();
if (copyImageError == GL_NO_ERROR) { if (copyImageError == GL_NO_ERROR) {
return; 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."); "DirectGLES CopyImageSubData only supports color-only or depth-only copies.");
MOBILEGL_ASSERT(src.target == GL_TEXTURE_2D && dst.target == GL_TEXTURE_2D, MOBILEGL_ASSERT(src.target == GL_TEXTURE_2D && dst.target == GL_TEXTURE_2D,
"DirectGLES color CopyImageSubData only supports GL_TEXTURE_2D."); "DirectGLES color CopyImageSubData only supports GL_TEXTURE_2D.");
MOBILEGL_ASSERT(src.z == 0 && dst.z == 0 && copyDepth == 1, MOBILEGL_ASSERT(src.z == 0 && dst.z == 0 && copyDepth == 1,
"DirectGLES color CopyImageSubData only supports single-layer copies."); "DirectGLES color CopyImageSubData only supports single-layer copies.");
CopyR32FTexture2D(srcBackendTexture->GetBackendTextureId(), srcLevel, src.x, src.y, srcWidth, copyHeight, CopyR32FTexture2D(src.Name(), srcLevel, src.x, src.y, srcWidth, copyHeight,
dstBackendTexture->GetBackendTextureId(), dst.target, dstLevel, dst.x, dst.y); dst.Name(), dst.target, dstLevel, dst.x, dst.y);
return; return;
} }
ClearGLErrors(); ClearGLErrors();
g_GLESFuncs.glCopyImageSubData(srcBackendTexture->GetBackendTextureId(), src.target, srcLevel, src.x, src.y, src.z, g_GLESFuncs.glCopyImageSubData(src.Name(), src.target, srcLevel, src.x, src.y, src.z,
dstBackendTexture->GetBackendTextureId(), dst.target, dstLevel, dst.x, dst.y, dst.z, dst.Name(), dst.target, dstLevel, dst.x, dst.y, dst.z,
srcWidth, copyHeight, copyDepth); srcWidth, copyHeight, copyDepth);
// Every error condition glCopyImageSubData has was already ruled out by the frontend // 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 // 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(dst.target).c_str(),
MG_Util::ConvertGLEnumToString(dstTarget).c_str()); MG_Util::ConvertGLEnumToString(dstTarget).c_str());
MOBILEGL_ASSERT(false, "glCopyImageSubData failed after frontend validation accepted the request."); 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, tempFB, GL_READ_FRAMEBUFFER, backendTexId,
backendAttachTarget == GL_UNKNOWN_MGL ? target : backendAttachTarget, level, backendAttachTarget == GL_UNKNOWN_MGL ? target : backendAttachTarget, level,
/*withStencil=*/format == GL_DEPTH_STENCIL); /*withStencil=*/format == GL_DEPTH_STENCIL);
} else if (backendAttachTarget == GL_TEXTURE_3D || backendAttachTarget == GL_TEXTURE_2D_ARRAY) { } else if (backendAttachTarget == GL_TEXTURE_3D || backendAttachTarget == GL_TEXTURE_2D_ARRAY ||
// ES cannot attach 3D/array textures through glFramebufferTexture2D; read layer 0. Reads backendAttachTarget == GL_TEXTURE_CUBE_MAP_ARRAY) {
// of deeper slices are served from the CPU shadow instead (see the shadow-first branch). // 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); ScratchFBOImpl::EnsureColorAttachmentLayer(tempFB, GL_READ_FRAMEBUFFER, backendTexId, level, 0);
} else { } else {
ScratchFBOImpl::EnsureColorAttachment2D( ScratchFBOImpl::EnsureColorAttachment2D(
@@ -7742,6 +8017,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
auto size = textureMipmapObject->GetMipmapTexelSize(MG_Util::ConvertGLEnumToTextureUploadTarget(target), level); 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()); 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 // 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()); TextureImpl::BackendTextureFormatAddsAlpha(textureObject->GetFormat(), textureObject->GetTarget());
// GL_PACK_IMAGE_HEIGHT/GL_PACK_SKIP_IMAGES only apply to 3D/array image // 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 // readbacks (cube-map arrays address as arrays); 2D targets must ignore
// them (GL 3.3 section 6.1.4). // them (GL 3.3 section 6.1.4). A 1D ARRAY is one of those 2D targets: GL hands it back
const Bool applyPackImageParams = backendAttachTarget == GL_TEXTURE_3D || // as a single two-dimensional image whose ROWS are the layers, so the layer stride is
backendAttachTarget == GL_TEXTURE_2D_ARRAY || // one packed row and the image parameters do not enter into it - even though the ES
backendAttachTarget == GL_TEXTURE_CUBE_MAP_ARRAY; // 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 GLsizei sliceCount = std::max(size.z(), 1);
const Bool multiSlice = 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 // 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 // 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 // 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 // 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. // for the formats the FBO cannot represent at all.
if (multiSlice && tempFBOComplete && 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 // 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 // GL_PACK_SKIP_IMAGES / GL_PACK_IMAGE_HEIGHT itself - this walks the destination
// over them, using the same layout StoreWideRowsToClient computes. // 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); GLsizei height, GLint border);
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
GLsizei height); 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, 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, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth); GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
void GenerateMipmap(GLenum target); 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* INDIRECT_PARAMS_BLOCK_NAME = "mg_IndirectParams";
constexpr const char* ZERO_BASED_INSTANCE_ID_NAME = "mg_ZeroBasedInstanceID"; 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() { static Bool IsAngleLlvmpipeRenderer() {
return g_GLESCapabilities.IsAngleLlvmpipeRenderer; return g_GLESCapabilities.IsAngleLlvmpipeRenderer;
} }
@@ -145,6 +155,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
[] { std::atexit(+[] { g_processTeardown = true; }); }); [] { 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) { String EmulateBaseInstanceInVertexShader(String source, GLenum shaderType) {
if (shaderType != GL_VERTEX_SHADER || source.find("gl_BaseInstance") == String::npos) { if (shaderType != GL_VERTEX_SHADER || source.find("gl_BaseInstance") == String::npos) {
return source; return source;
@@ -217,10 +239,48 @@ namespace MobileGL::MG_Backend::DirectGLES {
const Int paramsBinding = g_GLESCapabilities.MaxShaderStorageBufferBindings > 0 const Int paramsBinding = g_GLESCapabilities.MaxShaderStorageBufferBindings > 0
? g_GLESCapabilities.MaxShaderStorageBufferBindings - 1 ? g_GLESCapabilities.MaxShaderStorageBufferBindings - 1
: 0; : 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; String machinery;
if (source.find(String("uniform highp int ") + BASE_INSTANCE_UNIFORM_NAME + ";") == String::npos) { if (source.find(String("uniform highp int ") + BASE_INSTANCE_UNIFORM_NAME + ";") == String::npos) {
machinery += String("uniform highp int ") + BASE_INSTANCE_UNIFORM_NAME + ";\n"; 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("uniform highp int ") + BASE_INSTANCE_WORD_INDEX_UNIFORM_NAME + ";\n";
machinery += String("layout(std430, binding = ") + std::to_string(paramsBinding) + machinery += String("layout(std430, binding = ") + std::to_string(paramsBinding) +
") readonly buffer " + INDIRECT_PARAMS_BLOCK_NAME + ") readonly buffer " + INDIRECT_PARAMS_BLOCK_NAME +
@@ -1718,14 +1778,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
m_syncedAttributeVersions[attribIndex].BufferVersion; m_syncedAttributeVersions[attribIndex].BufferVersion;
if (!needsSyncFormat && !needsSyncBuffer && !needsSyncBaseInstance) continue; if (!needsSyncFormat && !needsSyncBuffer && !needsSyncBaseInstance) continue;
// Defence in depth. The frontend already declines glVertexAttribLFormat on this // This is where a 64-bit array actually stops. glVertexAttribLFormat is a legal call
// backend (SupportsFloat64VertexAttributes is false - ES has no GL_DOUBLE vertex // in a GL 4.3 context and the frontend RECORDS its format (the state queries have to
// format and ESSL has no fp64 type), so IsLong should never arrive here; if it ever // answer), so IsLong does arrive here - what this backend cannot do is FEED it:
// did, passing GL_DOUBLE to glVertexAttribPointer would only raise GL_INVALID_ENUM on // SupportsFloat64VertexAttributes is false because ES has no GL_DOUBLE vertex format
// the real driver. Disabling rather than merely skipping matters: becoming long bumps // and ESSL has no fp64 type, and passing GL_DOUBLE to glVertexAttribPointer would
// FormatVersion, not SwitchVersion, so the enable/disable block above will not run // only raise GL_INVALID_ENUM on the real driver. Disabling rather than merely
// again and an already-enabled array would stay enabled with no pointer and no // skipping matters: becoming long bumps FormatVersion, not SwitchVersion, so the
// ARRAY_BUFFER binding - which ES 3.1+ makes an INVALID_OPERATION at draw. // 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 // 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 // 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(); 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( void BackendTextureObject::SyncMipmapsToBackend(
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) { const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {
if (!stateTextureObject) { if (!stateTextureObject) {
@@ -2484,8 +2566,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
// 3. Size changed // 3. Size changed
// 4. Mipmap levels changed // 4. Mipmap levels changed
if (!stateTextureObject->IsComplete()) { // IsComplete() is the sampling predicate, and it calls a chain whose lower levels are
MGLOG_D("Texture object with ID: %u is not complete, skipping sync.", // 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()); stateTextureObject->GetExternalIndex());
return; return;
} }
@@ -2587,6 +2675,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
for (auto& uploadTarget : uploadTargets) { for (auto& uploadTarget : uploadTargets) {
for (SizeT level = m_prevTextureInfo.mipmapLevels; level < mipmapCount; ++level) { for (SizeT level = m_prevTextureInfo.mipmapLevels; level < mipmapCount; ++level) {
auto levelTexelSize = textureMipmapObject->GetMipmapTexelSize(uploadTarget, 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); auto levelByteSize = textureMipmapObject->GetMipmapByteSize(uploadTarget, level);
bool levelDirty = textureMipmapObject->IsStorageDirty(uploadTarget, level); bool levelDirty = textureMipmapObject->IsStorageDirty(uploadTarget, level);
auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget); auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget);
@@ -2814,6 +2909,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
for (auto& uploadTarget : uploadTargets) { for (auto& uploadTarget : uploadTargets) {
for (SizeT level = 0; level < mipmapCount; ++level) { for (SizeT level = 0; level < mipmapCount; ++level) {
auto levelTexelSize = textureMipmapObject->GetMipmapTexelSize(uploadTarget, 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); auto levelByteSize = textureMipmapObject->GetMipmapByteSize(uploadTarget, level);
bool levelDirty = textureMipmapObject->IsStorageDirty(uploadTarget, level); bool levelDirty = textureMipmapObject->IsStorageDirty(uploadTarget, level);
auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget); auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget);
@@ -4665,6 +4767,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
ImageFormatBakeInputs CollectImageFormatBakeInputs( ImageFormatBakeInputs CollectImageFormatBakeInputs(
const MG_State::GLState::ProgramObject& stateProgramObject) { const MG_State::GLState::ProgramObject& stateProgramObject) {
ImageFormatBakeInputs inputs; 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(); const Uint maxUniformLoc = stateProgramObject.GetMaxUniformLocation();
for (Uint loc = 0; loc <= maxUniformLoc; ++loc) { for (Uint loc = 0; loc <= maxUniformLoc; ++loc) {
const auto& name = stateProgramObject.GetUniformName(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. // still needs the extension directive to survive the ES compiler.
if (!IsCoreEsslLayoutFormat(static_cast<glslang::TLayoutFormat>(type.layoutFormat))) { if (!IsCoreEsslLayoutFormat(static_cast<glslang::TLayoutFormat>(type.layoutFormat))) {
inputs.needsExtendedImageFormats = true; 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; 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 " 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.", "core cannot spell and this driver has no GL_NV_image_formats for.",
name.c_str(), unit, boundFormat); name.c_str(), unit, boundFormat);
recordUnspellableFormat(
name, MG_Util::ShaderTranspiler::ShaderCompiler::EsslImageFormatSpelling(boundFormat));
continue; continue;
} }
inputs.needsExtendedImageFormats = true; inputs.needsExtendedImageFormats = true;
@@ -4743,6 +4871,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
for (const auto& name : textCompleted) { for (const auto& name : textCompleted) {
inputs.glFormatByUniformName.erase(name); 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; return inputs;
} }
@@ -4789,8 +4929,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
const Vector<unsigned int>& spirvCode, const GLenum glShaderType, const Vector<unsigned int>& spirvCode, const GLenum glShaderType,
const std::set<String>& xfbCaptureBlockNames, const ImageFormatBakeInputs& imageFormatBake, const std::set<String>& xfbCaptureBlockNames, const ImageFormatBakeInputs& imageFormatBake,
const UnorderedMap<String, Int>& storageBlockBindingOverrides, const UnorderedMap<String, Int>& storageBlockBindingOverrides,
const Bool enableSpirvValidation, String& outSource, const Int atomicCounterEsslBindingTop, const Bool enableSpirvValidation, String& outSource,
std::set<String>& outFlattenedXfbBlockNames, String& outError) const { std::set<String>& outFlattenedXfbBlockNames, Vector<Int>& outAtomicCounterGlBindings,
String& outError) const {
// ESSL cannot express gl_DrawID/gl_BaseInstance/gl_BaseVertex; demote them to // ESSL cannot express gl_DrawID/gl_BaseInstance/gl_BaseVertex; demote them to
// plain globals (mg_*) before handing the module to SPIRV-Cross. // plain globals (mg_*) before handing the module to SPIRV-Cross.
Vector<unsigned int> loweredSpirv; Vector<unsigned int> loweredSpirv;
@@ -5021,6 +5162,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
spvcSession.SetShaderStorageBlockBinding(storageBlockBindingOverrides); 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; const char* result = nullptr;
spvcSession.Compile(&result); 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. // this build current - the draw path compares the signature and rebuilds on a change.
const auto& storageBlockBindingOverrides = stateProgramObject->GetShaderStorageBlockBindingOverrides(); const auto& storageBlockBindingOverrides = stateProgramObject->GetShaderStorageBlockBindingOverrides();
m_shaderStorageBlockBindingSignature = ComputeShaderStorageBlockBindingSignature(*stateProgramObject); 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 // 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 // compiles to depends on live glBindImageTexture state, so the pairs it was built
// against are recorded here and compared per draw (ImageUnitFormatsStillMatch). // against are recorded here and compared per draw (ImageUnitFormatsStillMatch).
@@ -5195,6 +5358,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
esslKeyInputs.glFormatByUniformName = &imageFormatBake.glFormatByUniformName; esslKeyInputs.glFormatByUniformName = &imageFormatBake.glFormatByUniformName;
esslKeyInputs.storageBlockBindingOverrides = &storageBlockBindingOverrides; esslKeyInputs.storageBlockBindingOverrides = &storageBlockBindingOverrides;
esslKeyInputs.esslVersion = ResolveBackendEsslVersion(); esslKeyInputs.esslVersion = ResolveBackendEsslVersion();
esslKeyInputs.atomicCounterEsslBindingTop = m_atomicCounterEsslBindingTop;
esslKeyInputs.enableSpirvValidation = enableSpirvValidation; esslKeyInputs.enableSpirvValidation = enableSpirvValidation;
auto& esslCache = MG_Util::ShaderTranspiler::GetEsslTranslationCache(); auto& esslCache = MG_Util::ShaderTranspiler::GetEsslTranslationCache();
@@ -5204,17 +5368,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
} }
std::set<String> stageFlattenedXfbBlockNames; 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 = const MG_Util::ShaderTranspiler::EsslTranslationResultPtr esslHit =
esslCacheKey.Valid() ? esslCache.Find(esslCacheKey) : nullptr; esslCacheKey.Valid() ? esslCache.Find(esslCacheKey) : nullptr;
if (esslHit) { if (esslHit) {
source = esslHit->essl; source = esslHit->essl;
stageFlattenedXfbBlockNames = esslHit->flattenedXfbBlockNames; stageFlattenedXfbBlockNames = esslHit->flattenedXfbBlockNames;
stageAtomicCounterGlBindings = esslHit->atomicCounterGlBindings;
} else { } else {
String transpileError; String transpileError;
if (!TranspileSpirvToEssl(spirvCode, glShaderType, xfbCaptureBlockNames, if (!TranspileSpirvToEssl(spirvCode, glShaderType, xfbCaptureBlockNames,
imageFormatBake, storageBlockBindingOverrides, imageFormatBake, storageBlockBindingOverrides,
m_atomicCounterEsslBindingTop,
enableSpirvValidation, source, enableSpirvValidation, source,
stageFlattenedXfbBlockNames, transpileError)) { stageFlattenedXfbBlockNames,
stageAtomicCounterGlBindings, transpileError)) {
// MGLOG_E, unlatched, like the compile- and link-failure diagnostics // MGLOG_E, unlatched, like the compile- and link-failure diagnostics
// below: one line per failing stage is bounded by program count and // below: one line per failing stage is bounded by program count and
// naming the stage is the entire diagnostic value. A stage that never // 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>(); auto payload = MakeShared<MG_Util::ShaderTranspiler::EsslTranslationResult>();
payload->essl = source; payload->essl = source;
payload->flattenedXfbBlockNames = stageFlattenedXfbBlockNames; payload->flattenedXfbBlockNames = stageFlattenedXfbBlockNames;
payload->atomicCounterGlBindings = stageAtomicCounterGlBindings;
const SizeT payloadBytes = const SizeT payloadBytes =
MG_Util::ShaderTranspiler::EsslTranslationResultBytes(*payload); MG_Util::ShaderTranspiler::EsslTranslationResultBytes(*payload);
esslCache.Insert( esslCache.Insert(
@@ -5248,6 +5422,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
// the transpile so a cache HIT contributes its names too. // the transpile so a cache HIT contributes its names too.
flattenedXfbBlockNames.insert(stageFlattenedXfbBlockNames.begin(), flattenedXfbBlockNames.insert(stageFlattenedXfbBlockNames.begin(),
stageFlattenedXfbBlockNames.end()); 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 // 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 // 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()); 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 // Transform feedback capture runs on the real driver (see XfbImpl in
// DirectGLES.cpp), so the capture set has to be declared on the backend // 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 // 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 EmulateBaseInstanceInVertexShader(String source, GLenum shaderType);
String PromoteDrawParameterGlobalsToUniforms(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 // 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 // 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 // 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 BindBufferBaseCached(GLenum glTarget, Uint index, Uint id);
void BindBufferRangeCached(GLenum glTarget, Uint index, Uint id, GLintptr offset, GLsizeiptr size); void BindBufferRangeCached(GLenum glTarget, Uint index, Uint id, GLintptr offset, GLsizeiptr size);
void InvalidateIndexedBufferBindingCache(); 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 // Buffer-storage pool maintenance. TrimBufferPool evicts over-budget entries
// (called once per frame from Present); ClearBufferPool drops all pooled ids // (called once per frame from Present); ClearBufferPool drops all pooled ids
// without glDeleteBuffers (called when the ES context is going away). // 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 // qualifier, so the overrides are baked into the source). A mismatch means the
// program is stale exactly like the clamp masks above. // program is stale exactly like the clamp masks above.
Uint64 GetShaderStorageBlockBindingSignature() const { return m_shaderStorageBlockBindingSignature; } 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; } Bool HasGlobalUboBlock() const { return m_globalUboBackendBlockIndex >= 0; }
const Vector<Int>& GetUniformBlockBackendIndices() const { return m_uniformBlockBackendIndices; } const Vector<Int>& GetUniformBlockBackendIndices() const { return m_uniformBlockBackendIndices; }
@@ -1222,9 +1246,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
const std::set<String>& xfbCaptureBlockNames, const std::set<String>& xfbCaptureBlockNames,
const ImageFormatBakeInputs& imageFormatBake, const ImageFormatBakeInputs& imageFormatBake,
const UnorderedMap<String, Int>& storageBlockBindingOverrides, const UnorderedMap<String, Int>& storageBlockBindingOverrides,
Bool enableSpirvValidation, String& outSource, Int atomicCounterEsslBindingTop, Bool enableSpirvValidation,
String& outSource,
std::set<String>& outFlattenedXfbBlockNames, std::set<String>& outFlattenedXfbBlockNames,
String& outError) const; Vector<Int>& outAtomicCounterGlBindings, String& outError) const;
Uint m_backendProgramId = 0; Uint m_backendProgramId = 0;
// GL name of the frontend program this was last synced from; diagnostics only, so // 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; Uint m_fragColorBroadcastCount = 1;
// 0 is the signature of an empty override set, i.e. what almost every program has. // 0 is the signature of an empty override set, i.e. what almost every program has.
Uint64 m_shaderStorageBlockBindingSignature = 0; Uint64 m_shaderStorageBlockBindingSignature = 0;
Vector<Int> m_atomicCounterGlBindings;
Int m_atomicCounterEsslBindingTop = -1;
Bool m_isInitialized = false; Bool m_isInitialized = false;
Bool m_backendProgramUsable = 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 // 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 // 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. // 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, String BuildImageDeclaration(const ImageUniformDecl& decl, const char* memoryQualifier,
const String& variableName) { const String& variableName, Bool forceCoherent = false) {
String out = "layout(" + decl.layout + ") uniform "; String out = "layout(" + decl.layout + ") uniform ";
if (forceCoherent && !ContainsIdentifier(decl.qualifiers, "coherent")) {
out += "coherent ";
}
out += memoryQualifier; out += memoryQualifier;
out += ' '; out += ' ';
if (!decl.qualifiers.empty()) { if (!decl.qualifiers.empty()) {
@@ -1008,9 +1018,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
decl.writeName = MakeImageWriteAliasName(decl.name, glslCode, takenAliases); decl.writeName = MakeImageWriteAliasName(decl.name, glslCode, takenAliases);
takenAliases.push_back(decl.writeName); takenAliases.push_back(decl.writeName);
decl.split = true; 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, edits.push_back({decl.declStart, decl.declLength,
BuildImageDeclaration(decl, "readonly", decl.name) + "\n" + BuildImageDeclaration(decl, "readonly", decl.name, /*forceCoherent=*/true) +
BuildImageDeclaration(decl, "writeonly", decl.writeName)}); "\n" +
BuildImageDeclaration(decl, "writeonly", decl.writeName,
/*forceCoherent=*/true)});
} else if (decl.stored) { } else if (decl.stored) {
edits.push_back({decl.declStart, decl.declLength, edits.push_back({decl.declStart, decl.declLength,
BuildImageDeclaration(decl, "writeonly", decl.name)}); BuildImageDeclaration(decl, "writeonly", decl.name)});
+12 -5
View File
@@ -196,11 +196,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
// * loaded only -> add `readonly` // * loaded only -> add `readonly`
// * stored only -> add `writeonly` // * stored only -> add `writeonly`
// * both -> emit TWO declarations on the same binding and of the // * both -> emit TWO declarations on the same binding and of the
// same type, `readonly <name>` and `writeonly // same type, `coherent readonly <name>` and `coherent
// <IMAGE_WRITE_ALIAS_PREFIX><name>`, and point every // writeonly <IMAGE_WRITE_ALIAS_PREFIX><name>`, and point
// imageStore at the second one. Several image variables // every imageStore at the second one. Several image
// may share an image unit as long as they have the same // variables may share an image unit as long as they have
// type and format, which is exactly what the pair is. // 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 // 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 // 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 = m_dynamicParameters.MaxShaderStorageBufferBindings =
clampLimit("GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS", m_vulkanCaps.MaxShaderStorageBufferBindings, clampLimit("GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS", m_vulkanCaps.MaxShaderStorageBufferBindings,
kMaxAdvertisedBufferBlocks); 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( m_dynamicParameters.MaxTextureBufferSize = clampLimit(
"GL_MAX_TEXTURE_BUFFER_SIZE", m_vulkanCaps.MaxTextureBufferSize, kMaxAdvertisedTextureBufferSize); "GL_MAX_TEXTURE_BUFFER_SIZE", m_vulkanCaps.MaxTextureBufferSize, kMaxAdvertisedTextureBufferSize);
m_dynamicParameters.TextureBufferOffsetAlignment = m_vulkanCaps.TextureBufferOffsetAlignment; 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"); MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::CopyTexSubImage2D called with null GL context");
pVulkanRenderer->CopyTexSubImage2D(target, level, xoffset, yoffset, x, y, width, height); 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, 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, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) { GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::CopyImageSubData called with null VulkanRenderer"); MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::CopyImageSubData called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::CopyImageSubData called with null GL context"); MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::CopyImageSubData called with null GL context");
pVulkanRenderer->CopyImageSubData(srcTexture, srcTarget, srcLevel, srcX, srcY, srcZ, pVulkanRenderer->CopyImageSubData(src, srcTarget, srcLevel, srcX, srcY, srcZ,
dstTexture, dstTarget, dstLevel, dstX, dstY, dstZ, dst, dstTarget, dstLevel, dstX, dstY, dstZ,
srcWidth, srcHeight, srcDepth); srcWidth, srcHeight, srcDepth);
} }
void GenerateMipmap(GLenum target) { void GenerateMipmap(GLenum target) {
@@ -82,9 +82,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
GLsizei height, GLint border); GLsizei height, GLint border);
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
GLsizei height); 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, 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, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth); GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
void GenerateMipmap(GLenum target); void GenerateMipmap(GLenum target);
@@ -8,6 +8,7 @@
#include "VertexInputStateFactory.h" #include "VertexInputStateFactory.h"
#include "MG_Util/Converters/MGToStr/DataTypeConverter.h" #include "MG_Util/Converters/MGToStr/DataTypeConverter.h"
#include <MG_Backend/BackendObjects.h>
#include <utility> #include <utility>
namespace MobileGL::MG_Backend::DirectVulkan { 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 // 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, // while shaderFloat64 is not. Both halves key off nothing but the attribute being long,
// so they always agree without extra plumbing. // 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; if (!isLong || isInteger || normalized) return VK_FORMAT_UNDEFINED;
switch (size) { switch (size) {
case 1: return VK_FORMAT_R32G32_UINT; 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 auto* mipTexture = MG_State::GLState::AsMipmapTexture(&texture);
const Uint32 mipLevelCount = mipTexture != nullptr ? mipTexture->GetMipmapLevelCount() : 0u; const Uint32 mipLevelCount = mipTexture != nullptr ? mipTexture->GetMipmapLevelCount() : 0u;
return resource.syncedContentVersion != texture.GetContentVersion() || return resource.syncedContentVersion != texture.GetContentVersion() ||
resource.syncedShapeVersion != texture.GetShapeVersion() ||
resource.syncedTextureParamsVersion != texture.GetTextureParamsVersion() || resource.syncedTextureParamsVersion != texture.GetTextureParamsVersion() ||
resource.syncedMipLevelCount != mipLevelCount; resource.syncedMipLevelCount != mipLevelCount;
} }
@@ -1593,11 +1594,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool VkTextureManager::SyncTexture(MG_State::GLState::ITextureObject &texture, Bool VkTextureManager::SyncTexture(MG_State::GLState::ITextureObject &texture,
TextureResource &outResource) { TextureResource &outResource) {
// Cross-draw fast path: if the resource is already built and neither the texture's // 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 // pixel content (bumped in MarkStorageDirty), its SHAPE (bumped in BumpShapeVersion)
// sync, there is nothing to re-check or re-upload - skip CheckMipmapCompleteness, // nor its params changed since the last sync, there is nothing to re-check or
// SyncTextureResource, SyncTextureViews and the per-level dirty scan. Layout is // re-upload - skip CheckMipmapCompleteness, SyncTextureResource, SyncTextureViews and
// maintained separately by the transition path, so the resource still reflects truth. // 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 syncingContentVersion = texture.GetContentVersion();
const Uint64 syncingShapeVersion = texture.GetShapeVersion();
const auto* syncingMipTexture = MG_State::GLState::AsMipmapTexture(&texture); const auto* syncingMipTexture = MG_State::GLState::AsMipmapTexture(&texture);
const Uint32 syncingMipLevelCount = const Uint32 syncingMipLevelCount =
syncingMipTexture != nullptr ? syncingMipTexture->GetMipmapLevelCount() : 0u; syncingMipTexture != nullptr ? syncingMipTexture->GetMipmapLevelCount() : 0u;
@@ -1609,6 +1615,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_storageImageTextures.find(MakeTextureIdentity(&texture)) != m_storageImageTextures.end(); m_storageImageTextures.find(MakeTextureIdentity(&texture)) != m_storageImageTextures.end();
if (outResource.image != VK_NULL_HANDLE && !storageUpgradePending && if (outResource.image != VK_NULL_HANDLE && !storageUpgradePending &&
outResource.syncedContentVersion == syncingContentVersion && outResource.syncedContentVersion == syncingContentVersion &&
outResource.syncedShapeVersion == syncingShapeVersion &&
outResource.syncedTextureParamsVersion == texture.GetTextureParamsVersion() && outResource.syncedTextureParamsVersion == texture.GetTextureParamsVersion() &&
outResource.syncedMipLevelCount == syncingMipLevelCount) { outResource.syncedMipLevelCount == syncingMipLevelCount) {
return true; return true;
@@ -1629,6 +1636,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return false; 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)) { if (!SyncTextureResource(texture, uploadTarget, texelSize, byteSize, mipLevelCount, outResource)) {
MGLOG_D("%s: SyncTextureResource failed", __func__); MGLOG_D("%s: SyncTextureResource failed", __func__);
return false; return false;
@@ -1660,6 +1673,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (!hasDirtyMipLevel) { if (!hasDirtyMipLevel) {
outResource.syncedContentVersion = syncingContentVersion; outResource.syncedContentVersion = syncingContentVersion;
outResource.syncedMipLevelCount = syncingMipLevelCount; outResource.syncedMipLevelCount = syncingMipLevelCount;
outResource.syncedShapeVersion = syncingShapeVersion;
return true; return true;
} }
@@ -1669,6 +1683,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
outResource.syncedContentVersion = syncingContentVersion; outResource.syncedContentVersion = syncingContentVersion;
outResource.syncedMipLevelCount = syncingMipLevelCount; outResource.syncedMipLevelCount = syncingMipLevelCount;
outResource.syncedShapeVersion = syncingShapeVersion;
return true; return true;
} }
@@ -2536,7 +2551,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
uploadItem.target = target; uploadItem.target = target;
uploadItem.level = level; uploadItem.level = level;
uploadItem.baseArrayLayer = ResolveUploadArrayLayer(target); 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.source = source;
uploadItem.offset = stagingSize; uploadItem.offset = stagingSize;
uploadItem.uploadByteSize = byteSize; uploadItem.uploadByteSize = byteSize;
@@ -2574,6 +2595,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
uploadItem.uploadByteSize = rectTexels * uploadItem.texelBytes; 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) { if (formatInfo.expandRgbToRgba) {
@@ -22,6 +22,25 @@ class ITextureObject;
namespace MobileGL::MG_Backend::DirectVulkan { namespace MobileGL::MG_Backend::DirectVulkan {
enum class SamplerNumericDomain : Uint8; 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 { class VkTextureManager {
public: public:
// Monotonic epoch bumped whenever a texture VkImage is (re)created. The render-pass // 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) // 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. // busts the skip even if it failed to bump the content version.
Uint32 syncedMipLevelCount = 0; 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() = default;
TextureResource(const TextureResource&) = delete; TextureResource(const TextureResource&) = delete;
@@ -237,6 +262,7 @@ public:
std::swap(this->lastRecordingGeneration, that.lastRecordingGeneration); std::swap(this->lastRecordingGeneration, that.lastRecordingGeneration);
std::swap(this->syncedContentVersion, that.syncedContentVersion); std::swap(this->syncedContentVersion, that.syncedContentVersion);
std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount); std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount);
std::swap(this->syncedShapeVersion, that.syncedShapeVersion);
} }
void Reset() { void Reset() {
@@ -300,6 +326,7 @@ public:
syncedTextureParamsVersion = 0; syncedTextureParamsVersion = 0;
syncedContentVersion = 0; syncedContentVersion = 0;
syncedMipLevelCount = 0; syncedMipLevelCount = 0;
syncedShapeVersion = 0;
} }
~TextureResource() { ~TextureResource() {
@@ -8869,7 +8869,7 @@ void main() {
// A mixed 2D-array <-> 3D pair is legal because maintenance1 - core since Vulkan 1.1 - // 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 // relaxed the old "layerCounts must match" rule into "the 3D side's extent.depth must
// equal the array side's layerCount". // 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. // True for a VK_IMAGE_TYPE_3D image, i.e. slices ride the z axis, not the layer axis.
Bool slicesAreDepth = false; Bool slicesAreDepth = false;
// The GL z offset, kept in whichever field this endpoint's image type reads it from. // 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; } Int32 OffsetZ() const { return slicesAreDepth ? static_cast<Int32>(baseSlice) : 0; }
}; };
Bool TryResolveCopyImageEndpoint(TextureTarget target, // The Vulkan image one glCopyImageSubData endpoint names, after the two object kinds GL
const VkTextureManager::TextureResource& resource, Uint32 mipLevel, // 4.6 core 18.3.2 allows have been collapsed onto the fields this copy reads. A
GLint glZ, GLsizei glDepth, CopyImageEndpoint& outEndpoint) { // 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) { if (glZ < 0 || glDepth <= 0) {
return false; return false;
} }
const Uint32 baseSlice = static_cast<Uint32>(glZ); 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) { switch (target) {
case TextureTarget::Texture1D: case TextureTarget::Texture1D:
case TextureTarget::Texture2D: case TextureTarget::Texture2D:
@@ -8897,12 +8919,12 @@ void main() {
case TextureTarget::Texture2DMultisample: case TextureTarget::Texture2DMultisample:
// Not layered at all: GL still requires the z/depth pair, and it can only name the // Not layered at all: GL still requires the z/depth pair, and it can only name the
// one slice these targets have. // one slice these targets have.
outEndpoint = {}; outMapping = {};
return baseSlice == 0 && glDepth == 1; return baseSlice == 0 && glDepth == 1;
case TextureTarget::Texture3D: case TextureTarget::Texture3D:
outEndpoint.slicesAreDepth = true; outMapping.slicesAreDepth = true;
outEndpoint.baseSlice = baseSlice; outMapping.baseSlice = baseSlice;
outEndpoint.availableSlices = std::max(1u, resource.depth >> mipLevel); outMapping.availableSlices = std::max(1u, image.depth >> mipLevel);
return true; return true;
case TextureTarget::Texture2DArray: case TextureTarget::Texture2DArray:
case TextureTarget::Texture2DMultisampleArray: case TextureTarget::Texture2DMultisampleArray:
@@ -8911,9 +8933,9 @@ void main() {
// A cube map is an array of six faces here (see TryResolveTextureShapeInfo), and GL // 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 // numbers its faces on the same z axis an array texture numbers its layers, so both
// arrive as a plain layer range. // arrive as a plain layer range.
outEndpoint.slicesAreDepth = false; outMapping.slicesAreDepth = false;
outEndpoint.baseSlice = baseSlice; outMapping.baseSlice = baseSlice;
outEndpoint.availableSlices = resource.arrayLayers; outMapping.availableSlices = image.arrayLayers;
return true; return true;
default: default:
// GL_TEXTURE_1D_ARRAY carries its layers on the Y axis (srcY/srcHeight), which // GL_TEXTURE_1D_ARRAY carries its layers on the Y axis (srcY/srcHeight), which
@@ -8923,15 +8945,20 @@ void main() {
return false; return false;
} }
} }
Uint CopyImageEndpointName(const CopyImageEndpoint& endpoint) {
if (endpoint.IsRenderbuffer()) return endpoint.Renderbuffer->GetExternalIndex();
return endpoint.Texture ? endpoint.Texture->GetExternalIndex() : 0u;
}
} // namespace } // 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, 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, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) { GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
MOBILEGL_ASSERT(srcTexture != nullptr && dstTexture != nullptr, MOBILEGL_ASSERT(srcEndpoint.Exists() && dstEndpoint.Exists(),
"CopyImageSubData requires valid source and destination textures."); "CopyImageSubData requires valid source and destination images.");
// The frontend already declines a zero or negative extent, so anything else here is a // 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 // caller MobileGL wrote - but it still reaches vkCmdCopyImage in a release build, and a
// zero extent.depth is as invalid as a zero width. // 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 // 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, // 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. // on a tiler, a copy whose source has already been overwritten.
if (srcTexture.get() == dstTexture.get()) { if (srcEndpoint.Texture == dstEndpoint.Texture && srcEndpoint.Renderbuffer == dstEndpoint.Renderbuffer) {
MGLOG_E_ONCE("%s: in-place copy on textureId=%d is not supported; declining the copy", __func__, MGLOG_E_ONCE("%s: in-place copy on objectId=%u is not supported; declining the copy", __func__,
srcTexture->GetExternalIndex()); CopyImageEndpointName(srcEndpoint));
return; return;
} }
@@ -8963,8 +8990,42 @@ void main() {
VkRenderPassManager::EndRenderPass(frame.commandBuffer); VkRenderPassManager::EndRenderPass(frame.commandBuffer);
} }
auto* srcResource = m_textureManager->SyncTextureAndGetDescriptor(*srcTexture); // One resolver for both object kinds. The texture arm is the same
auto* dstResource = m_textureManager->SyncTextureAndGetDescriptor(*dstTexture); // 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 // 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 // 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 // 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 // The frontend validator (ValidateTextureLevelExists) is what produces the
// GL_INVALID_VALUE the application is actually owed. This guard exists so the next gap // 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. // up there declines a copy instead of taking the process down.
if (srcResource == nullptr || dstResource == nullptr) { if (!srcResolved || !dstResolved) {
MGLOG_E_ONCE("%s: source or destination texture failed to sync; declining the copy", __func__); MGLOG_E_ONCE("%s: source or destination image failed to sync; declining the copy", __func__);
return; return;
} }
if (srcLevel < 0 || dstLevel < 0 || static_cast<Uint32>(srcLevel) >= srcResource->mipLevels || if (srcLevel < 0 || dstLevel < 0 || static_cast<Uint32>(srcLevel) >= srcImage.mipLevels ||
static_cast<Uint32>(dstLevel) >= dstResource->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__, 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; return;
} }
const VkImageAspectFlags copyAspectMask = 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); (VK_IMAGE_ASPECT_COLOR_BIT | VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT);
MOBILEGL_ASSERT(copyAspectMask != 0 && MOBILEGL_ASSERT(copyAspectMask != 0 &&
(srcResource->aspect & copyAspectMask) == srcResource->aspect && (srcImage.aspect & copyAspectMask) == srcImage.aspect &&
(dstResource->aspect & copyAspectMask) == dstResource->aspect, (dstImage.aspect & copyAspectMask) == dstImage.aspect,
"CopyImageSubData source and destination aspects are incompatible."); "CopyImageSubData source and destination aspects are incompatible.");
const Uint32 srcMipLevel = static_cast<Uint32>(srcLevel); const Uint32 srcMipLevel = static_cast<Uint32>(srcLevel);
const Uint32 dstMipLevel = static_cast<Uint32>(dstLevel); const Uint32 dstMipLevel = static_cast<Uint32>(dstLevel);
const Uint32 srcMipWidth = std::max(1u, srcResource->extent.width >> srcMipLevel); const Uint32 srcMipWidth = std::max(1u, srcImage.extent.width >> srcMipLevel);
const Uint32 srcMipHeight = std::max(1u, srcResource->extent.height >> srcMipLevel); const Uint32 srcMipHeight = std::max(1u, srcImage.extent.height >> srcMipLevel);
const Uint32 dstMipWidth = std::max(1u, dstResource->extent.width >> dstMipLevel); const Uint32 dstMipWidth = std::max(1u, dstImage.extent.width >> dstMipLevel);
const Uint32 dstMipHeight = std::max(1u, dstResource->extent.height >> 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: // 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 // 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, // 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. // 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 // 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. // in a release build and the unsupported shape reached vkCmdCopyImage anyway.
CopyImageEndpoint srcEndpoint; CopyImageSliceMapping srcSlices;
CopyImageEndpoint dstEndpoint; CopyImageSliceMapping dstSlices;
if (!TryResolveCopyImageEndpoint(srcTextureTarget, *srcResource, srcMipLevel, srcZ, srcDepth, srcEndpoint) || if (!TryResolveCopyImageSliceMapping(srcTextureTarget, srcImage, srcMipLevel, srcZ, srcDepth, srcSlices) ||
!TryResolveCopyImageEndpoint(dstTextureTarget, *dstResource, dstMipLevel, dstZ, srcDepth, dstEndpoint)) { !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", 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(), __func__, MG_Util::ConvertTextureTargetToString(srcTextureTarget).c_str(),
MG_Util::ConvertTextureTargetToString(dstTextureTarget).c_str(), srcZ, dstZ, srcDepth); 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 // shrinks) and a 3D texture by the selected level's depth (which every level halves), so
// both come from the endpoint that resolved them. // both come from the endpoint that resolved them.
const Uint32 copySliceCount = static_cast<Uint32>(srcDepth); const Uint32 copySliceCount = static_cast<Uint32>(srcDepth);
if (srcEndpoint.baseSlice + copySliceCount > srcEndpoint.availableSlices || if (srcSlices.baseSlice + copySliceCount > srcSlices.availableSlices ||
dstEndpoint.baseSlice + copySliceCount > dstEndpoint.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); " MGLOG_E_ONCE("%s: slice range outside image bounds (srcZ=%d of %u, dstZ=%d of %u, depth=%d); "
"declining the copy", "declining the copy",
__func__, srcZ, srcEndpoint.availableSlices, dstZ, dstEndpoint.availableSlices, srcDepth); __func__, srcZ, srcSlices.availableSlices, dstZ, dstSlices.availableSlices, srcDepth);
return; return;
} }
const Bool clearReady = MaterializePendingClearForTexture(frame.commandBuffer, *srcTexture); const auto materializeClear = [this, &frame](const CopyImageEndpoint& endpoint) {
MOBILEGL_ASSERT(clearReady, "%s: failed to materialize pending clear for source textureId=%d", if (endpoint.IsRenderbuffer()) {
__func__, srcTexture->GetExternalIndex()); 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 // A clear still parked on the destination would otherwise materialize AFTER this copy and
// wipe the texels it just wrote. // wipe the texels it just wrote.
const Bool dstClearReady = MaterializePendingClearForTexture(frame.commandBuffer, *dstTexture); const Bool dstClearReady = materializeClear(dstEndpoint);
MOBILEGL_ASSERT(dstClearReady, "%s: failed to materialize pending clear for destination textureId=%d", MOBILEGL_ASSERT(dstClearReady, "%s: failed to materialize pending clear for destination objectId=%u",
__func__, dstTexture->GetExternalIndex()); __func__, CopyImageEndpointName(dstEndpoint));
const VkImageLayout srcOriginalLayout = srcResource->layout; const VkImageLayout srcOriginalLayout = *srcImage.trackedLayout;
const VkImageLayout dstOriginalLayout = dstResource->layout; const VkImageLayout dstOriginalLayout = *dstImage.trackedLayout;
// A layout of UNDEFINED means nothing has ever been written to the image, which on the // 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 // 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 // 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 // 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. // 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) { if (originalLayout != VK_IMAGE_LAYOUT_UNDEFINED) {
return originalLayout; return originalLayout;
} }
return (copyAspectMask & (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) != 0 const Bool depthStencil =
? VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL (copyAspectMask & (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) != 0;
: VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; 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 srcRestoreLayout = resolveRestoreLayout(srcOriginalLayout, srcImage.isRenderbuffer);
const VkImageLayout dstRestoreLayout = resolveRestoreLayout(dstOriginalLayout); const VkImageLayout dstRestoreLayout = resolveRestoreLayout(dstOriginalLayout, dstImage.isRenderbuffer);
VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
VkAccessFlags srcAccessMask = 0; VkAccessFlags srcAccessMask = 0;
@@ -9083,15 +9157,15 @@ void main() {
// [baseSlice, baseSlice + depth) the slice mapping above hands the copy. // [baseSlice, baseSlice + depth) the slice mapping above hands the copy.
if (srcOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) { if (srcOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
Bool srcReady = VkTextureManager::TransitionImageLayout( 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, srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
srcAccessMask, VK_ACCESS_TRANSFER_READ_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__); MOBILEGL_ASSERT(srcReady, "%s: failed to transition undefined source image", __func__);
srcCopyLayout = srcResource->layout; srcCopyLayout = *srcImage.trackedLayout;
} else { } else {
Bool srcReady = VkTextureManager::TransitionImageLayout( 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, srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, copyAspectMask, srcMipLevel, 1); srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, copyAspectMask, srcMipLevel, 1);
MOBILEGL_ASSERT(srcReady, "%s: failed to transition source image", __func__); MOBILEGL_ASSERT(srcReady, "%s: failed to transition source image", __func__);
@@ -9103,15 +9177,15 @@ void main() {
VkImageLayout dstCopyLayout = dstOriginalLayout; VkImageLayout dstCopyLayout = dstOriginalLayout;
if (dstOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) { if (dstOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
Bool dstReady = VkTextureManager::TransitionImageLayout( 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, dstStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
dstAccessMask, VK_ACCESS_TRANSFER_WRITE_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__); MOBILEGL_ASSERT(dstReady, "%s: failed to transition undefined destination image", __func__);
dstCopyLayout = dstResource->layout; dstCopyLayout = *dstImage.trackedLayout;
} else { } else {
Bool dstReady = VkTextureManager::TransitionImageLayout( 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, dstStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
dstAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT, copyAspectMask, dstMipLevel, 1); dstAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT, copyAspectMask, dstMipLevel, 1);
MOBILEGL_ASSERT(dstReady, "%s: failed to transition destination image", __func__); 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 // 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 // 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. // 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{}; VkImageCopy copyRegion{};
copyRegion.srcSubresource.aspectMask = copyAspectMask; copyRegion.srcSubresource.aspectMask = copyAspectMask;
copyRegion.srcSubresource.mipLevel = srcMipLevel; copyRegion.srcSubresource.mipLevel = srcMipLevel;
copyRegion.srcSubresource.baseArrayLayer = srcEndpoint.BaseArrayLayer(); copyRegion.srcSubresource.baseArrayLayer = srcSlices.BaseArrayLayer();
copyRegion.srcSubresource.layerCount = srcEndpoint.slicesAreDepth ? 1u : copySliceCount; copyRegion.srcSubresource.layerCount = srcSlices.slicesAreDepth ? 1u : copySliceCount;
copyRegion.srcOffset = {srcX, srcY, srcEndpoint.OffsetZ()}; copyRegion.srcOffset = {srcX, srcY, srcSlices.OffsetZ()};
copyRegion.dstSubresource.aspectMask = copyAspectMask; copyRegion.dstSubresource.aspectMask = copyAspectMask;
copyRegion.dstSubresource.mipLevel = dstMipLevel; copyRegion.dstSubresource.mipLevel = dstMipLevel;
copyRegion.dstSubresource.baseArrayLayer = dstEndpoint.BaseArrayLayer(); copyRegion.dstSubresource.baseArrayLayer = dstSlices.BaseArrayLayer();
copyRegion.dstSubresource.layerCount = dstEndpoint.slicesAreDepth ? 1u : copySliceCount; copyRegion.dstSubresource.layerCount = dstSlices.slicesAreDepth ? 1u : copySliceCount;
copyRegion.dstOffset = {dstX, dstY, dstEndpoint.OffsetZ()}; copyRegion.dstOffset = {dstX, dstY, dstSlices.OffsetZ()};
copyRegion.extent = {static_cast<Uint32>(srcWidth), static_cast<Uint32>(srcHeight), copyRegion.extent = {static_cast<Uint32>(srcWidth), static_cast<Uint32>(srcHeight),
copyCrossesDepthAxis ? copySliceCount : 1u}; copyCrossesDepthAxis ? copySliceCount : 1u};
MGLOG_D("CopyImageSubData: src(target=%s level=%u layer=%u+%u z=%d) -> dst(target=%s level=%u layer=%u+%u " 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.dstSubresource.baseArrayLayer, copyRegion.dstSubresource.layerCount,
copyRegion.dstOffset.z, srcWidth, srcHeight, copyRegion.extent.depth); copyRegion.dstOffset.z, srcWidth, srcHeight, copyRegion.extent.depth);
vkCmdCopyImage(frame.commandBuffer, vkCmdCopyImage(frame.commandBuffer,
srcResource->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, srcImage.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
dstResource->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, dstImage.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
1, &copyRegion); 1, &copyRegion);
VkPipelineStageFlags srcRestoreStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkPipelineStageFlags srcRestoreStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
@@ -9152,14 +9226,14 @@ void main() {
GetImageTransitionDestinationState(srcRestoreLayout, srcRestoreStageMask, srcRestoreAccessMask); GetImageTransitionDestinationState(srcRestoreLayout, srcRestoreStageMask, srcRestoreAccessMask);
if (srcOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) { if (srcOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
Bool srcRestored = VkTextureManager::TransitionImageLayout( 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_PIPELINE_STAGE_TRANSFER_BIT, srcRestoreStageMask,
VK_ACCESS_TRANSFER_READ_BIT, srcRestoreAccessMask, 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__); MOBILEGL_ASSERT(srcRestored, "%s: failed to restore undefined source image layout", __func__);
} else { } else {
Bool srcRestored = VkTextureManager::TransitionImageLayout( Bool srcRestored = VkTextureManager::TransitionImageLayout(
frame.commandBuffer, srcResource->image, srcCopyLayout, srcRestoreLayout, frame.commandBuffer, srcImage.image, srcCopyLayout, srcRestoreLayout,
VK_PIPELINE_STAGE_TRANSFER_BIT, srcRestoreStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, srcRestoreStageMask,
VK_ACCESS_TRANSFER_READ_BIT, srcRestoreAccessMask, copyAspectMask, srcMipLevel, 1); VK_ACCESS_TRANSFER_READ_BIT, srcRestoreAccessMask, copyAspectMask, srcMipLevel, 1);
MOBILEGL_ASSERT(srcRestored, "%s: failed to restore source image layout", __func__); MOBILEGL_ASSERT(srcRestored, "%s: failed to restore source image layout", __func__);
@@ -9170,14 +9244,14 @@ void main() {
GetImageTransitionDestinationState(dstRestoreLayout, dstRestoreStageMask, dstRestoreAccessMask); GetImageTransitionDestinationState(dstRestoreLayout, dstRestoreStageMask, dstRestoreAccessMask);
if (dstOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) { if (dstOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
Bool dstRestored = VkTextureManager::TransitionImageLayout( 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_PIPELINE_STAGE_TRANSFER_BIT, dstRestoreStageMask,
VK_ACCESS_TRANSFER_WRITE_BIT, dstRestoreAccessMask, 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__); MOBILEGL_ASSERT(dstRestored, "%s: failed to restore undefined destination image layout", __func__);
} else { } else {
Bool dstRestored = VkTextureManager::TransitionImageLayout( Bool dstRestored = VkTextureManager::TransitionImageLayout(
frame.commandBuffer, dstResource->image, dstCopyLayout, dstRestoreLayout, frame.commandBuffer, dstImage.image, dstCopyLayout, dstRestoreLayout,
VK_PIPELINE_STAGE_TRANSFER_BIT, dstRestoreStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, dstRestoreStageMask,
VK_ACCESS_TRANSFER_WRITE_BIT, dstRestoreAccessMask, copyAspectMask, dstMipLevel, 1); VK_ACCESS_TRANSFER_WRITE_BIT, dstRestoreAccessMask, copyAspectMask, dstMipLevel, 1);
MOBILEGL_ASSERT(dstRestored, "%s: failed to restore destination image layout", __func__); MOBILEGL_ASSERT(dstRestored, "%s: failed to restore destination image layout", __func__);
@@ -9739,7 +9813,7 @@ void main() {
VkImageAspectFlags imageAspect, Uint32 mipLevel, VkImageAspectFlags imageAspect, Uint32 mipLevel,
Uint32 baseArrayLayer, GLint x, GLint y, GLsizei width, Uint32 baseArrayLayer, GLint x, GLint y, GLsizei width,
GLsizei height, GLenum format, GLenum type, void* pixels, GLsizei height, GLenum format, GLenum type, void* pixels,
Bool defaultFramebufferOrientation) { Bool defaultFramebufferOrientation, Uint32 sourceLayerCount) {
const Bool wantDepth = format != GL_STENCIL_INDEX; const Bool wantDepth = format != GL_STENCIL_INDEX;
const Bool wantStencil = format != GL_DEPTH_COMPONENT; const Bool wantStencil = format != GL_DEPTH_COMPONENT;
auto& frame = m_frameContext.GetCurrent(); auto& frame = m_frameContext.GetCurrent();
@@ -9818,6 +9892,10 @@ void main() {
if (!mapped) return; 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]{}; VkBufferImageCopy regions[2]{};
Uint32 regionCount = 0; Uint32 regionCount = 0;
if (wantDepth) { if (wantDepth) {
@@ -9826,9 +9904,9 @@ void main() {
region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
region.imageSubresource.mipLevel = mipLevel; region.imageSubresource.mipLevel = mipLevel;
region.imageSubresource.baseArrayLayer = baseArrayLayer; region.imageSubresource.baseArrayLayer = baseArrayLayer;
region.imageSubresource.layerCount = 1; region.imageSubresource.layerCount = copyLayerCount;
region.imageOffset = {copyOffset.x, copyOffset.y, 0}; region.imageOffset = {copyOffset.x, copyOffset.y, 0};
region.imageExtent = {copyExtent.width, copyExtent.height, 1}; region.imageExtent = {copyExtent.width, copyRowCount, 1};
} }
if (wantStencil) { if (wantStencil) {
auto& region = regions[regionCount++]; auto& region = regions[regionCount++];
@@ -9836,9 +9914,9 @@ void main() {
region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_STENCIL_BIT; region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_STENCIL_BIT;
region.imageSubresource.mipLevel = mipLevel; region.imageSubresource.mipLevel = mipLevel;
region.imageSubresource.baseArrayLayer = baseArrayLayer; region.imageSubresource.baseArrayLayer = baseArrayLayer;
region.imageSubresource.layerCount = 1; region.imageSubresource.layerCount = copyLayerCount;
region.imageOffset = {copyOffset.x, copyOffset.y, 0}; 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(), vkCmdCopyImageToBuffer(frame.commandBuffer, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, readback.GetHandle(),
regionCount, regions); regionCount, regions);
@@ -10073,9 +10151,17 @@ void main() {
? static_cast<Uint32>(textureUploadTarget) - ? static_cast<Uint32>(textureUploadTarget) -
static_cast<Uint32>(TextureUploadTarget::CubeMapPositiveX) static_cast<Uint32>(TextureUploadTarget::CubeMapPositiveX)
: 0; : 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, ReadDepthStencilImageToClient(resource->image, resource->format, &resource->layout, resource->aspect,
static_cast<Uint32>(level), arrayLayer, 0, 0, levelSize.x(), static_cast<Uint32>(level), arrayLayer, 0, 0, levelSize.x(),
levelSize.y(), format, type, pixels); levelSize.y(), format, type, pixels,
/*defaultFramebufferOrientation=*/false, sourceLayers);
} else { } else {
MGLOG_E_ONCE("DirectVulkan::GetTexImage skipped: color query of a non-color texture"); 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). // destination layout (GL 3.3 section 6.1.4).
const auto imageTextureTarget = textureObject->GetTarget(); const auto imageTextureTarget = textureObject->GetTarget();
const Bool is3dImage = imageTextureTarget == TextureTarget::Texture3D; 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::Texture2DArray ||
imageTextureTarget == TextureTarget::TextureCubeMapArray; imageTextureTarget == TextureTarget::TextureCubeMapArray;
const GLsizei depthSlices = is3dImage ? std::max<GLsizei>(texelSize.z(), 1) : 1; const GLsizei depthSlices = is3dImage ? std::max<GLsizei>(texelSize.z(), 1) : 1;
const GLsizei arrayLayers = isArrayImage ? static_cast<GLsizei>(resource->arrayLayers) : 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) { if (bufSize >= 0) {
const Int dstChannels = GetReadbackChannelCount(format); const Int dstChannels = GetReadbackChannelCount(format);
if ((type == GL_UNSIGNED_BYTE || type == GL_FLOAT) && dstChannels > 0) { 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.mipLevel = static_cast<Uint32>(level);
copyRegion.imageSubresource.baseArrayLayer = 0; copyRegion.imageSubresource.baseArrayLayer = 0;
copyRegion.imageSubresource.layerCount = static_cast<Uint32>(arrayLayers); 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)}; static_cast<Uint32>(depthSlices)};
vkCmdCopyImageToBuffer(frame.commandBuffer, resource->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, vkCmdCopyImageToBuffer(frame.commandBuffer, resource->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
readback.GetHandle(), 1, &copyRegion); readback.GetHandle(), 1, &copyRegion);
@@ -23,6 +23,7 @@
#include "VkTimerQueryManager.h" #include "VkTimerQueryManager.h"
#include "MG_Util/Math/VectorTypes.h" #include "MG_Util/Math/VectorTypes.h"
#include <Includes.h> #include <Includes.h>
#include <MG_Backend/BackendObject.h>
#include <vk_mem_alloc.h> #include <vk_mem_alloc.h>
#include "../VkIncludes.h" #include "../VkIncludes.h"
@@ -197,9 +198,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
GLbitfield mask, GLenum filter); GLbitfield mask, GLenum filter);
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset,
GLint x, GLint y, GLsizei width, GLsizei height); 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, 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, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth); GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
void GenerateMipmap(GLenum target); 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 // 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 // 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. // 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, void ReadDepthStencilImageToClient(VkImage image, VkFormat vkFormat, VkImageLayout* trackedLayout,
VkImageAspectFlags imageAspect, Uint32 mipLevel, Uint32 baseArrayLayer, VkImageAspectFlags imageAspect, Uint32 mipLevel, Uint32 baseArrayLayer,
GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, 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 // Same-extent depth blit between images of different depth formats: host
// round-trip with a per-texel re-encode (see BlitNamedFramebuffer). // round-trip with a per-texel re-encode (see BlitNamedFramebuffer).
Bool BlitDepthAcrossFormats(FrameContext::FrameData& frame, VkImage srcImage, VkFormat srcFormat, 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(Buffer)
add_subdirectory(Driver) add_subdirectory(Driver)
add_subdirectory(Container) 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/GLToStr/GLEnumConverter.h>
#include <MG_Util/Converters/MGToGL/BufferEnumConverter.h> #include <MG_Util/Converters/MGToGL/BufferEnumConverter.h>
#include <MG_Util/Converters/MGToStr/BufferEnumConverter.h> #include <MG_Util/Converters/MGToStr/BufferEnumConverter.h>
#include <MG_Util/ShaderTranspiler/Types.h>
namespace MobileGL::MG_Impl::GLImpl::BufferImpl { namespace MobileGL::MG_Impl::GLImpl::BufferImpl {
Bool ValidateBufferTarget(BufferTarget target) { Bool ValidateBufferTarget(BufferTarget target) {
@@ -67,6 +68,13 @@ namespace MobileGL::MG_Impl::GLImpl::BufferImpl {
// binding points in GL 3.3 (no ARB_transform_feedback3). // binding points in GL 3.3 (no ARB_transform_feedback3).
pointCount = std::min<SizeT>(pointCount, 4); 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; return pointCount;
} }
} // namespace } // 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) { void Clear_Backend(GLbitfield mask) {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif #endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.Clear(mask); MG_Backend::gBackendFunctionsTable.GL.Clear(mask);
} }
@@ -337,6 +350,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif #endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.DrawElements(mode, count, type, indices); MG_Backend::gBackendFunctionsTable.GL.DrawElements(mode, count, type, indices);
} }
@@ -345,6 +359,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif #endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElements(mode, count, type, indices, drawcount); MG_Backend::gBackendFunctionsTable.GL.MultiDrawElements(mode, count, type, indices, drawcount);
} }
@@ -353,6 +368,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif #endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsBaseVertex(mode, count, type, indices, drawcount, MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsBaseVertex(mode, count, type, indices, drawcount,
basevertex); basevertex);
} }
@@ -361,6 +377,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif #endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.DrawArrays(mode, first, count); MG_Backend::gBackendFunctionsTable.GL.DrawArrays(mode, first, count);
} }
@@ -368,6 +385,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif #endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.MultiDrawArrays(mode, first, count, drawcount); MG_Backend::gBackendFunctionsTable.GL.MultiDrawArrays(mode, first, count, drawcount);
} }
@@ -376,6 +394,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif #endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.DrawElementsBaseVertex(mode, count, type, indices, basevertex); MG_Backend::gBackendFunctionsTable.GL.DrawElementsBaseVertex(mode, count, type, indices, basevertex);
} }
@@ -384,6 +403,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif #endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirect(mode, type, indirect, drawcount, stride); MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirect(mode, type, indirect, drawcount, stride);
} }
@@ -391,6 +411,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif #endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirect(mode, indirect, drawcount, stride); MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirect(mode, indirect, drawcount, stride);
} }
@@ -399,6 +420,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif #endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirectCount(mode, type, indirect, drawcount, MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirectCount(mode, type, indirect, drawcount,
maxdrawcount, stride); maxdrawcount, stride);
} }
@@ -408,6 +430,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif #endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirectCount(mode, indirect, drawcount, maxdrawcount, MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirectCount(mode, indirect, drawcount, maxdrawcount,
stride); stride);
} }
@@ -417,6 +440,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif #endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.DrawRangeElementsBaseVertex(mode, start, end, count, type, indices, MG_Backend::gBackendFunctionsTable.GL.DrawRangeElementsBaseVertex(mode, start, end, count, type, indices,
basevertex); basevertex);
} }
@@ -426,6 +450,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif #endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.DrawRangeElements(mode, start, end, count, type, indices); MG_Backend::gBackendFunctionsTable.GL.DrawRangeElements(mode, start, end, count, type, indices);
} }
@@ -435,6 +460,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif #endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseVertexBaseInstance( MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseVertexBaseInstance(
mode, count, type, indices, instancecount, basevertex, baseinstance); mode, count, type, indices, instancecount, basevertex, baseinstance);
} }
@@ -444,6 +470,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif #endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseVertex(mode, count, type, indices, instancecount, MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseVertex(mode, count, type, indices, instancecount,
basevertex); basevertex);
} }
@@ -453,6 +480,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif #endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseInstance(mode, count, type, indices, MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseInstance(mode, count, type, indices,
instancecount, baseinstance); instancecount, baseinstance);
} }
@@ -462,6 +490,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif #endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstanced(mode, count, type, indices, instancecount); MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstanced(mode, count, type, indices, instancecount);
} }
@@ -469,6 +498,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif #endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.DrawElementsIndirect(mode, type, indirect); MG_Backend::gBackendFunctionsTable.GL.DrawElementsIndirect(mode, type, indirect);
} }
void DrawArraysInstancedBaseInstance_Backend(GLenum mode, GLint first, GLsizei count, GLsizei instancecount, void DrawArraysInstancedBaseInstance_Backend(GLenum mode, GLint first, GLsizei count, GLsizei instancecount,
@@ -476,6 +506,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif #endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.DrawArraysInstancedBaseInstance(mode, first, count, instancecount, MG_Backend::gBackendFunctionsTable.GL.DrawArraysInstancedBaseInstance(mode, first, count, instancecount,
baseinstance); baseinstance);
} }
@@ -484,6 +515,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif #endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.DrawArraysInstanced(mode, first, count, instancecount); MG_Backend::gBackendFunctionsTable.GL.DrawArraysInstanced(mode, first, count, instancecount);
} }
@@ -491,6 +523,7 @@ namespace MobileGL::MG_Impl::GLImpl {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif #endif
if (ConditionalRenderDiscardsCommand()) return;
MG_Backend::gBackendFunctionsTable.GL.DrawArraysIndirect(mode, indirect); MG_Backend::gBackendFunctionsTable.GL.DrawArraysIndirect(mode, indirect);
} }
@@ -519,6 +552,9 @@ namespace MobileGL::MG_Impl::GLImpl {
return; 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); dispatchCompute(numGroupsX, numGroupsY, numGroupsZ);
} }
@@ -570,6 +606,7 @@ namespace MobileGL::MG_Impl::GLImpl {
return; return;
} }
if (!ValidateCurrentProgramForCompute(__func__)) return; if (!ValidateCurrentProgramForCompute(__func__)) return;
if (ConditionalRenderDiscardsCommand()) return;
dispatchComputeIndirect(indirect); 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, 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_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_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_HEAD(void, BeginConditionalRender, GLuint id, GLenum mode) DECLARE_GL_FUNCTION_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, 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, 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, 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) 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, 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, 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_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, 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, 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) 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) { 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); MG_Backend::gBackendFunctionsTable.GL.ClearBufferfi(buffer, drawbuffer, depth, stencil);
} }
void ClearBufferfv_Backend(GLenum buffer, GLint drawbuffer, const GLfloat* value) { 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); MG_Backend::gBackendFunctionsTable.GL.ClearBufferfv(buffer, drawbuffer, value);
} }
void ClearBufferuiv_Backend(GLenum buffer, GLint drawbuffer, const GLuint* 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); MG_Backend::gBackendFunctionsTable.GL.ClearBufferuiv(buffer, drawbuffer, value);
} }
void ClearBufferiv_Backend(GLenum buffer, GLint drawbuffer, const GLint* 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); 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_State/GLState/FramebufferState/FramebufferObject.h>
#include <MG_Util/Texture/TextureFormatProcessor.h> #include <MG_Util/Texture/TextureFormatProcessor.h>
#include <MG_Util/Async/ShaderCompilePool.h> #include <MG_Util/Async/ShaderCompilePool.h>
#include <MG_Util/ShaderTranspiler/Types.h>
#include <MG_Backend/BackendObjects.h> #include <MG_Backend/BackendObjects.h>
namespace MobileGL::MG_Impl::GLImpl { namespace MobileGL::MG_Impl::GLImpl {
@@ -46,13 +47,29 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
} }
constexpr GLint kFrontendMaxComputeUniformComponents = 1024; // Shared with the glslang resource table for the same reason as the atomic-counter
constexpr GLint kFrontendMaxComputeAtomicCounters = 8; // limits below: gl_MaxComputeUniformComponents expands from BuildTBuiltInResource.
constexpr GLint kFrontendMaxComputeAtomicCounterBuffers = 8; 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 kFrontendMaxComputeSharedMemorySize = 32768;
constexpr GLint kFrontendMaxComputeWorkGroupInvocations = 1024; constexpr GLint kFrontendMaxComputeWorkGroupInvocations = 1024;
constexpr GLint kFrontendMaxCombinedAtomicCounters = 8; constexpr GLint kFrontendMaxCombinedAtomicCounters =
constexpr GLint kFrontendMaxFragmentAtomicCounters = 8; 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 kFrontendMaxGeometryAtomicCounters = 0;
constexpr GLint kFrontendMaxTessControlAtomicCounters = 0; constexpr GLint kFrontendMaxTessControlAtomicCounters = 0;
constexpr GLint kFrontendMaxTessEvaluationAtomicCounters = 0; constexpr GLint kFrontendMaxTessEvaluationAtomicCounters = 0;
@@ -66,10 +83,11 @@ namespace MobileGL::MG_Impl::GLImpl {
constexpr GLint kFrontendMaxTessControlAtomicCounterBuffers = 0; constexpr GLint kFrontendMaxTessControlAtomicCounterBuffers = 0;
constexpr GLint kFrontendMaxTessEvaluationAtomicCounterBuffers = 0; constexpr GLint kFrontendMaxTessEvaluationAtomicCounterBuffers = 0;
constexpr GLint kFrontendMaxVertexAtomicCounterBuffers = 0; constexpr GLint kFrontendMaxVertexAtomicCounterBuffers = 0;
// One atomic counter is a uint, and a buffer never has to hold more counters than the // GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE: the byte offset ceiling a counter may be declared
// combined limit the frontend advertises. GL 4.6 table 23.63 floors this at 32 bytes. // 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 = 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 // 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. // limits they advertise still have to be legal.
constexpr GLint kFrontendMaxDebugGroupStackDepth = 64; constexpr GLint kFrontendMaxDebugGroupStackDepth = 64;
@@ -103,12 +121,16 @@ namespace MobileGL::MG_Impl::GLImpl {
constexpr GLint kFrontendSubpixelBits = 4; constexpr GLint kFrontendSubpixelBits = 4;
constexpr GLint kFrontendMaxSamples = 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) { 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) { 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, GLint GetMaxCombinedUniformComponents(GLint maxDefaultUniformComponents, GLint maxUniformBlocks,
@@ -186,6 +208,16 @@ namespace MobileGL::MG_Impl::GLImpl {
MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxShaderStorageBufferBindings; MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxShaderStorageBufferBindings;
return std::min(frontendCount, static_cast<SizeT>(std::max(backendCount, 0))); 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; return frontendCount;
} }
@@ -213,6 +245,23 @@ namespace MobileGL::MG_Impl::GLImpl {
return ClampBlockCountToBindingPoints(blockCount, BufferTarget::ShaderStorage); 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) { bool TryDecodeDrawBufferQuery(GLenum pname, SizeT& drawBufferIndex) {
if (pname == GL_DRAW_BUFFER) { if (pname == GL_DRAW_BUFFER) {
drawBufferIndex = 0; drawBufferIndex = 0;
@@ -1532,6 +1581,9 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_MAX_COMBINED_ATOMIC_COUNTERS: case GL_MAX_COMBINED_ATOMIC_COUNTERS:
*params = kFrontendMaxCombinedAtomicCounters; *params = kFrontendMaxCombinedAtomicCounters;
return; return;
case GL_MAX_COMBINED_ATOMIC_COUNTER_BUFFERS:
*params = kFrontendMaxCombinedAtomicCounterBuffers;
return;
case GL_MAX_COMBINED_UNIFORM_BLOCKS: case GL_MAX_COMBINED_UNIFORM_BLOCKS:
*params = ClampUniformBlockCount(kFrontendMaxCombinedUniformBlocks); *params = ClampUniformBlockCount(kFrontendMaxCombinedUniformBlocks);
return; return;
@@ -1547,8 +1599,11 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_MAX_FRAGMENT_ATOMIC_COUNTERS: case GL_MAX_FRAGMENT_ATOMIC_COUNTERS:
*params = kFrontendMaxFragmentAtomicCounters; *params = kFrontendMaxFragmentAtomicCounters;
return; return;
case GL_MAX_FRAGMENT_ATOMIC_COUNTER_BUFFERS:
*params = kFrontendMaxFragmentAtomicCounterBuffers;
return;
case GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS: case GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS:
*params = ClampStorageBlockCount(16); // TODO *params = StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxFragmentShaderStorageBlocks);
return; return;
case GL_MAX_FRAGMENT_INPUT_COMPONENTS: case GL_MAX_FRAGMENT_INPUT_COMPONENTS:
*params = kFrontendMaxFragmentInputComponents; *params = kFrontendMaxFragmentInputComponents;
@@ -1574,7 +1629,7 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = kFrontendMaxGeometryAtomicCounterBuffers; *params = kFrontendMaxGeometryAtomicCounterBuffers;
return; return;
case GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS: case GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS:
*params = ClampStorageBlockCount(16); // TODO *params = StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxGeometryShaderStorageBlocks);
return; return;
case GL_MAX_GEOMETRY_INPUT_COMPONENTS: case GL_MAX_GEOMETRY_INPUT_COMPONENTS:
*params = kFrontendMaxGeometryInputComponents; *params = kFrontendMaxGeometryInputComponents;
@@ -1645,16 +1700,18 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = 0; *params = 0;
return; return;
case GL_MAX_TESS_CONTROL_SHADER_STORAGE_BLOCKS: case GL_MAX_TESS_CONTROL_SHADER_STORAGE_BLOCKS:
*params = ClampStorageBlockCount(16); // TODO *params = StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxTessControlShaderStorageBlocks);
return; return;
case GL_MAX_TESS_EVALUATION_SHADER_STORAGE_BLOCKS: case GL_MAX_TESS_EVALUATION_SHADER_STORAGE_BLOCKS:
*params = ClampStorageBlockCount(16); // TODO *params =
StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxTessEvaluationShaderStorageBlocks);
return; return;
case GL_MAX_TEXTURE_LOD_BIAS: case GL_MAX_TEXTURE_LOD_BIAS:
*params = 15; // TODO *params = 15; // TODO
return; return;
case GL_MAX_UNIFORM_LOCATIONS: 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; return;
case GL_MAX_VARYING_COMPONENTS: case GL_MAX_VARYING_COMPONENTS:
*params = kFrontendMaxVaryingComponents; *params = kFrontendMaxVaryingComponents;
@@ -1674,7 +1731,7 @@ namespace MobileGL::MG_Impl::GLImpl {
: MG_Backend::DynamicBackendParameters{}.MaxVertexImageUniforms; : MG_Backend::DynamicBackendParameters{}.MaxVertexImageUniforms;
return; return;
case GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS: case GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS:
*params = ClampStorageBlockCount(16); // TODO *params = StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxVertexShaderStorageBlocks);
return; return;
case GL_MAX_VERTEX_UNIFORM_COMPONENTS: case GL_MAX_VERTEX_UNIFORM_COMPONENTS:
*params = kFrontendMaxVertexUniformComponents; *params = kFrontendMaxVertexUniformComponents;
@@ -1984,6 +2041,24 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_UNIFORM_BUFFER_START: case GL_UNIFORM_BUFFER_START:
RecordIndexedOnlyGetterError(__func__, pname); RecordIndexedOnlyGetterError(__func__, pname);
return; 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: case GL_UNPACK_ALIGNMENT:
*params = MG_State::pGLContext->GetPixelStoreParam(PixelStoreParam::UnpackAlignment); *params = MG_State::pGLContext->GetPixelStoreParam(PixelStoreParam::UnpackAlignment);
return; return;
@@ -2219,18 +2294,19 @@ namespace MobileGL::MG_Impl::GLImpl {
static_cast<Uint64>(INT32_MAX))); static_cast<Uint64>(INT32_MAX)));
break; break;
case GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS: 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)); *params = static_cast<GLint>(GetIndexedBufferQueryPointCount(BufferTarget::AtomicCounter));
break; break;
case GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE: case GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE:
// The conformance suite splits this evenly across every advertised binding point and // The conformance suite splits this evenly across every advertised binding point and
// binds all of them in one glBindBuffersRange // binds all of them in one glBindBuffersRange
// (KHR-GL44.multi_bind.functional_bind_buffers_range), so the pair has to divide: // (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 // a zero-sized range is INVALID_VALUE before BindBufferRange binds anything. The
// rejects with INVALID_VALUE before it binds anything. Floor the advertised size at // shared constant is 16384 over 8 binding points, which divides.
// one counter per binding point. *params = kFrontendMaxAtomicCounterBufferSize;
*params = std::max<GLint>(
kFrontendMaxAtomicCounterBufferSize,
static_cast<GLint>(GetIndexedBufferQueryPointCount(BufferTarget::AtomicCounter) * sizeof(GLuint)));
break; break;
case GL_MAX_TEXTURE_BUFFER_SIZE: case GL_MAX_TEXTURE_BUFFER_SIZE:
*params = dynamicParameters.MaxTextureBufferSize; *params = dynamicParameters.MaxTextureBufferSize;
+74 -1
View File
@@ -645,7 +645,13 @@ namespace MobileGL::MG_Impl::GLImpl {
break; break;
} }
case GL_ACTIVE_ATOMIC_COUNTER_BUFFERS: 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); MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
break; break;
case GL_ACTIVE_ATTRIBUTES: case GL_ACTIVE_ATTRIBUTES:
@@ -2838,6 +2844,73 @@ namespace MobileGL::MG_Impl::GLImpl {
return ProgramInterface::GetResourceLocationIndex(*programObject, programInterface, name); 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> // 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. // - 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 // 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); const GLenum* props, GLsizei bufSize, GLsizei* length, GLint* params);
GLint GetProgramResourceLocation(GLuint program, GLenum programInterface, const GLchar* name); GLint GetProgramResourceLocation(GLuint program, GLenum programInterface, const GLchar* name);
GLint GetProgramResourceLocationIndex(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 ShaderStorageBlockBinding(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding);
void Uniform1d(GLint location, GLdouble v0); void Uniform1d(GLint location, GLdouble v0);
void Uniform1dv(GLint location, GLsizei count, const GLdouble* value); 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 // "<getAtomicCounterBlockName()>_<binding>" (ParseContextBase.cpp), one per GL
// atomic-counter binding point. That block IS the GL_ATOMIC_COUNTER_BUFFER resource // 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. // 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 { enum class BlockKind {
Uniform, // a real GL uniform block Uniform, // a real GL uniform block
@@ -565,6 +565,75 @@ namespace MobileGL::MG_Impl::GLImpl {
queryObject->ended = true; 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) { void GetQueryiv(GLenum target, GLenum pname, GLint* params) {
if (!params) { if (!params) {
return; return;
+5
View File
@@ -29,6 +29,11 @@ namespace MobileGL::MG_Impl::GLImpl {
void GetQueryBufferObjecti64v(GLuint id, GLuint buffer, GLenum pname, GLintptr offset); void GetQueryBufferObjecti64v(GLuint id, GLuint buffer, GLenum pname, GLintptr offset);
void GetQueryBufferObjectui64v(GLuint id, GLuint buffer, GLenum pname, GLintptr offset); void GetQueryBufferObjectui64v(GLuint id, GLuint buffer, GLenum pname, GLintptr offset);
void QueryCounter(GLuint id, GLenum target); 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. // Destroys every still-registered query object exactly as DeleteQueries would.
// GL requires queries to die with their context; called only from full library // GL requires queries to die with their context; called only from full library
// teardown (DestroyImpl), where no context survives on any thread, so the // 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.")); "Compressed texture formats are not supported."));
} }
// glGetTexLevelParameter{i,f}v answers WIDTH/HEIGHT/DEPTH out of the mipmap chain. The only // GL_TEXTURE_WIDTH of a buffer texture: how many texels of the texture's internal format fit
// other storage type the state layer knows is GL_TEXTURE_BUFFER (TextureStorageType is // in the buffer range it addresses, CLAMPED to GL_MAX_TEXTURE_BUFFER_SIZE. Attaching a larger
// {Mipmap, Buffer}), whose level geometry this stack does not track yet. Report that instead // buffer is legal (GL 4.6 core 8.9) - the texture simply addresses the first
// of throwing: THROW_UNIMPL_EXCEPTION unwinds a C++ exception through the C GL ABI and takes // MAX_TEXTURE_BUFFER_SIZE texels of it, and that clamped count is what WIDTH reports.
// the process down, which is never an acceptable answer to a query - see the same reasoning //
// above for the compressed-format path. // 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) { void RecordUnsupportedLevelQueryStorage(const char* caller, GLenum pname) {
MGLOG_W_ONCE("%s: glGetTexLevelParameter(pname=%s) is not implemented for texture-buffer " MGLOG_W_ONCE("%s: glGetTexLevelParameter(pname=%s) is not implemented for this texture's "
"storage; recording GL_INVALID_OPERATION instead of terminating", "storage class; recording GL_INVALID_OPERATION instead of terminating",
caller, MG_Util::ConvertGLEnumToString(pname).c_str()); caller, MG_Util::ConvertGLEnumToString(pname).c_str());
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation, ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>( MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", caller, "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 } // namespace
@@ -691,6 +712,34 @@ namespace MobileGL::MG_Impl::GLImpl {
return textureObject; 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 { namespace {
void RecordClearTextureError(const char* caller, ErrorCode code, const String& message) { void RecordClearTextureError(const char* caller, ErrorCode code, const String& message) {
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
@@ -726,6 +775,21 @@ namespace MobileGL::MG_Impl::GLImpl {
std::format("Texture level {} is not defined.", level)); std::format("Texture level {} is not defined.", level));
return nullptr; 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; return mipmapTexture;
} }
@@ -2197,6 +2261,26 @@ namespace MobileGL::MG_Impl::GLImpl {
} else { } else {
DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level); DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level);
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, height, depth}, internalBytes}); 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) { if (!originalPixels) {
@@ -2343,6 +2427,13 @@ namespace MobileGL::MG_Impl::GLImpl {
textureUploadTarget, level, static_cast<GLenum>(internalformat), nullptr, textureUploadTarget, level, static_cast<GLenum>(internalformat), nullptr,
MG_Util::CalculateCompressedTextureImageSize(compressedInfo, {width, height, 1})); 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) { if (!originalPixels) {
@@ -2431,6 +2522,13 @@ namespace MobileGL::MG_Impl::GLImpl {
if (!isProxy) { if (!isProxy) {
DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level); DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level);
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, 1, 1}, internalBytes}); 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) { if (!originalPixels) {
@@ -2982,6 +3080,15 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = textureObject->GetSamplerObject()->GetMaxAnisotropy(); *params = textureObject->GetSamplerObject()->GetMaxAnisotropy();
} }
break; 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: case GL_DEPTH_STENCIL_TEXTURE_MODE:
if (params) { if (params) {
*params = static_cast<GLfloat>(textureObject->GetDepthStencilTextureMode()); *params = static_cast<GLfloat>(textureObject->GetDepthStencilTextureMode());
@@ -3031,6 +3138,9 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).x(); *params = textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).x();
break; break;
} }
case TextureStorageType::Buffer:
*params = GetBufferTextureTexelWidth(textureObject.get());
break;
default: default:
RecordUnsupportedLevelQueryStorage("GetTexLevelParameteriv_State", pname); RecordUnsupportedLevelQueryStorage("GetTexLevelParameteriv_State", pname);
break; break;
@@ -3046,6 +3156,9 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).y(); *params = textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).y();
break; break;
} }
case TextureStorageType::Buffer:
*params = 1; // a buffer texture is one-dimensional
break;
default: default:
RecordUnsupportedLevelQueryStorage("GetTexLevelParameteriv_State", pname); RecordUnsupportedLevelQueryStorage("GetTexLevelParameteriv_State", pname);
break; break;
@@ -3061,6 +3174,9 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).z(); *params = textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).z();
break; break;
} }
case TextureStorageType::Buffer:
*params = 1; // a buffer texture is one-dimensional
break;
default: default:
RecordUnsupportedLevelQueryStorage("GetTexLevelParameteriv_State", pname); RecordUnsupportedLevelQueryStorage("GetTexLevelParameteriv_State", pname);
break; break;
@@ -3130,6 +3246,31 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
break; 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: default:
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetTexLevelParameteriv_State", 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(); *params = (GLfloat)textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).x();
break; break;
} }
case TextureStorageType::Buffer:
*params = (GLfloat)GetBufferTextureTexelWidth(textureObject.get());
break;
default: default:
RecordUnsupportedLevelQueryStorage("GetTexLevelParameterfv_State", pname); RecordUnsupportedLevelQueryStorage("GetTexLevelParameterfv_State", pname);
break; break;
@@ -3184,6 +3328,9 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = (GLfloat)textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).y(); *params = (GLfloat)textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).y();
break; break;
} }
case TextureStorageType::Buffer:
*params = 1.0f; // a buffer texture is one-dimensional
break;
default: default:
RecordUnsupportedLevelQueryStorage("GetTexLevelParameterfv_State", pname); RecordUnsupportedLevelQueryStorage("GetTexLevelParameterfv_State", pname);
break; break;
@@ -3199,6 +3346,9 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = (GLfloat)textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).z(); *params = (GLfloat)textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).z();
break; break;
} }
case TextureStorageType::Buffer:
*params = 1.0f; // a buffer texture is one-dimensional
break;
default: default:
RecordUnsupportedLevelQueryStorage("GetTexLevelParameterfv_State", pname); RecordUnsupportedLevelQueryStorage("GetTexLevelParameterfv_State", pname);
break; break;
@@ -3266,6 +3416,27 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
break; 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: default:
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetTexLevelParameterfv_State", 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); 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, 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, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) { GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
auto copyImageSubData = MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData; auto copyImageSubData = MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData;
@@ -3425,7 +3596,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"Backend does not support image-to-image copies.")); "Backend does not support image-to-image copies."));
return; 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); 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 // 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 // in question and the fault is the binding), so this is a local rule rather than a
// change to the helper. // change to the helper.
Bool ValidateCopyImageObjectExists(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, Bool ValidateCopyImageObjectExists(const MG_Backend::CopyImageEndpoint& endpoint,
const char* endpointName) { const char* endpointName) {
if (textureObject) return true; if (endpoint.Exists()) return true;
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue, ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>( MakeUnique<GenericErrorInfo>(
@@ -3498,21 +3669,106 @@ namespace MobileGL::MG_Impl::GLImpl {
MG_Util::ConvertTextureTargetToString(textureObject->GetTarget())))); MG_Util::ConvertTextureTargetToString(textureObject->GetTarget()))));
return false; return false;
} }
} // namespace
Bool ValidateCopyImageSubData_State(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture, // ---- The questions ValidateCopyImageSubData_State asks of one endpoint. ---------------
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, // A renderbuffer answers all of them directly: it has exactly one image, no mip chain and
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture, // no sampler state, and it carries its own internal format and extent.
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) { Int GetCopyImageEndpointSamples(const MG_Backend::CopyImageEndpoint& endpoint) {
if (!ValidateCopyImageObjectExists(srcTexture, "source") || if (endpoint.IsRenderbuffer()) return endpoint.Renderbuffer->GetSamples();
!ValidateCopyImageObjectExists(dstTexture, "destination")) { 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; return false;
} }
const auto srcTextureTarget = MG_Util::ConvertGLEnumToTextureTarget(srcTarget);
const auto dstTextureTarget = MG_Util::ConvertGLEnumToTextureTarget(dstTarget); // Targets with no mip chain have q == level_base by definition (GL 4.6 core 8.17), so no
if (!TextureImpl::ValidateTextureTarget(srcTextureTarget) || // minification filter can make them mipmap incomplete - while the shared predicate derives
!TextureImpl::ValidateTextureTarget(dstTextureTarget)) { // 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; return false;
} }
// GL_TEXTURE_BUFFER and the cube FACE enums convert to a target this frontend knows, but // 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")) { if (!ValidateCopyImageTarget(srcTarget, "source") || !ValidateCopyImageTarget(dstTarget, "destination")) {
return false; return false;
} }
if (!ValidateCopyImageTargetMatchesObject(srcTexture, srcTextureTarget, "source") || if (!ValidateCopyImageTargetMatchesObject(src.Texture, srcTextureTarget, "source") ||
!ValidateCopyImageTargetMatchesObject(dstTexture, dstTextureTarget, "destination")) { !ValidateCopyImageTargetMatchesObject(dst.Texture, dstTextureTarget, "destination")) {
return false; return false;
} }
if (!TextureImpl::ValidateTextureLevelNumber(srcLevel) || 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 // 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 // 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. // the whole glcts process. The answer the spec asks for is GL_INVALID_VALUE.
if (!TextureImpl::ValidateTextureLevelExists(srcTexture, srcLevel, __func__) || if (!ValidateCopyImageEndpointLevelExists(src, srcLevel, __func__) ||
!TextureImpl::ValidateTextureLevelExists(dstTexture, dstLevel, __func__)) { !ValidateCopyImageEndpointLevelExists(dst, dstLevel, __func__)) {
return false; return false;
} }
if (srcWidth < 0 || srcHeight < 0 || srcDepth < 0) { 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 // 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 // single-sample image reports zero - so this one comparison is also what rejects
// copying between a multisample target and a non-multisample one. // 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( MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation, ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>( MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", __func__, "MG_Impl/GLImpl", __func__,
std::format("The two images have different sample counts ({} vs. {}).", std::format("The two images have different sample counts ({} vs. {}).",
srcTexture->GetSamples(), dstTexture->GetSamples()))); srcSamples, dstSamples)));
return false; return false;
} }
// 18.3.2: both images must be complete. An incomplete one has no defined texels to copy // 18.3.2: both images must be complete. An incomplete one has no defined texels to copy
// and no defined storage to copy into. // 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( MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation, ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>( MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", __func__, "MG_Impl/GLImpl", __func__,
std::format("A copied image is incomplete (source complete: {}, destination complete: {}).", std::format("A copied image is incomplete (source complete: {}, destination complete: {}).",
srcTexture->IsComplete(), dstTexture->IsComplete()))); srcComplete, dstComplete)));
return false; return false;
} }
const auto srcUploadTarget = GetPrimaryUploadTarget(srcTexture); const auto srcUploadTarget = GetPrimaryUploadTarget(src.Texture);
const auto dstUploadTarget = GetPrimaryUploadTarget(dstTexture); const auto dstUploadTarget = GetPrimaryUploadTarget(dst.Texture);
const auto srcBlock = TextureImpl::ResolveCopyImageTexelBlock( const auto srcBlock = TextureImpl::ResolveCopyImageTexelBlock(
srcTexture->GetFormat(), GetCompressedLevelFormat(srcTexture, srcUploadTarget, srcLevel)); GetCopyImageEndpointFormat(src), GetCopyImageEndpointCompressedFormat(src, srcUploadTarget, srcLevel));
const auto dstBlock = TextureImpl::ResolveCopyImageTexelBlock( const auto dstBlock = TextureImpl::ResolveCopyImageTexelBlock(
dstTexture->GetFormat(), GetCompressedLevelFormat(dstTexture, dstUploadTarget, dstLevel)); GetCopyImageEndpointFormat(dst), GetCopyImageEndpointCompressedFormat(dst, dstUploadTarget, dstLevel));
if (!TextureImpl::ValidateCopyImageFormatCompatibility(srcBlock, dstBlock)) { if (!TextureImpl::ValidateCopyImageFormatCompatibility(srcBlock, dstBlock)) {
return false; return false;
} }
const IntVec3 srcLevelSize = GetCopyImageLevelSize(srcTexture, srcUploadTarget, srcLevel); const IntVec3 srcLevelSize = GetCopyImageEndpointLevelSize(src, srcUploadTarget, srcLevel);
const IntVec3 dstLevelSize = GetCopyImageLevelSize(dstTexture, dstUploadTarget, dstLevel); const IntVec3 dstLevelSize = GetCopyImageEndpointLevelSize(dst, dstUploadTarget, dstLevel);
if (!TextureImpl::ValidateCopyImageBlockAlignment(srcBlock, srcX, srcY, srcWidth, srcHeight, if (!TextureImpl::ValidateCopyImageBlockAlignment(srcBlock, srcX, srcY, srcWidth, srcHeight,
srcLevelSize.x(), srcLevelSize.y(), "source") || srcLevelSize.x(), srcLevelSize.y(), "source") ||
!TextureImpl::ValidateCopyImageBlockAlignment(dstBlock, dstX, dstY, srcWidth, srcHeight, !TextureImpl::ValidateCopyImageBlockAlignment(dstBlock, dstX, dstY, srcWidth, srcHeight,
@@ -4098,8 +4358,14 @@ namespace MobileGL::MG_Impl::GLImpl {
return false; return false;
} }
// For a cube map this is exactly cube completeness: IsComplete() wants all six faces. // GL 4.6 core 8.11.4 names cube completeness as the only completeness a readback requires,
if (!textureObject->IsComplete()) { // 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( MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation, ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Texture is incomplete")); 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, // 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 // integer-ness). Also rejects a STENCIL_INDEX readback of anything but stencil-only
// (not advertised by MobileGL). // storage, which is the only pairing GL 4.4 / ARB_texture_stencil8 ever made legal.
if (!TextureImpl::ValidateTextureInternalFormatCompatibleWithInput( if (!TextureImpl::ValidateTextureInternalFormatCompatibleWithInput(
textureInputFormat, textureObject->GetFormat(), texturePixelDataType)) { textureInputFormat, textureObject->GetFormat(), texturePixelDataType)) {
return false; return false;
@@ -4158,33 +4424,48 @@ namespace MobileGL::MG_Impl::GLImpl {
const auto* textureMipmapObject = const auto* textureMipmapObject =
static_cast<const MG_State::GLState::TextureObjectMipmap*>(textureObject.get()); static_cast<const MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
const auto& uploadTargets = textureObject->GetUploadTargets(); const auto& uploadTargets = textureObject->GetUploadTargets();
if (!uploadTargets.empty() && static_cast<Uint>(level) < textureMipmapObject->GetMipmapLevelCount()) { // The half of the completeness gate above that GL does keep: the REQUESTED level has
// Tightly packed, and summed over every face because a cube map query returns all // to hold an image. A name that was never given one carries no levels at all (which is
// six. Pack pixel-store state only ever grows this, so a request rejected here // also what an Unknown internal format answers), and a chain grown to reach level N
// could not have fit under any packing. // leaves every level below it at {0, 0, 0}.
const auto texelSize = textureMipmapObject->GetMipmapTexelSize(uploadTargets[0], level); if (uploadTargets.empty() || static_cast<Uint>(level) >= textureMipmapObject->GetMipmapLevelCount()) {
const SizeT required = MG_Util::CalculateInputTextureImageSize(textureInputFormat, MG_State::pGLContext->RecordError(
texturePixelDataType, texelSize) * ErrorCode::InvalidOperation,
uploadTargets.size(); 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( MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation, 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; 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); const SizeT byteSize = ComputeTextureStorageByteSize(textureInternalFormat, levelWidth, 1, 1);
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{levelWidth, 1, 1}, byteSize}); textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{levelWidth, 1, 1}, byteSize});
textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, false); 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 // Immutable storage defines exactly `levels` levels; AllocateStorage only grows, so a
// longer pre-existing chain has to be dropped explicitly. // longer pre-existing chain has to be dropped explicitly.
@@ -4487,6 +4774,12 @@ namespace MobileGL::MG_Impl::GLImpl {
MG_Util::CalculateCompressedTextureImageSize(compressedInfo, MG_Util::CalculateCompressedTextureImageSize(compressedInfo,
{levelWidth, levelHeight, 1})); {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. // See TextureStorage1D.
textureMipmapObject->TruncateMipmapLevels(uploadTarget, static_cast<Uint>(levels)); textureMipmapObject->TruncateMipmapLevels(uploadTarget, static_cast<Uint>(levels));
@@ -4494,32 +4787,6 @@ namespace MobileGL::MG_Impl::GLImpl {
textureObject->SetImmutableLevels(static_cast<Uint>(levels)); 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, void TextureStorage3D(GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height,
GLsizei depth) { GLsizei depth) {
auto textureObject = GetTextureObjectByName(texture, __func__); 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 // Array targets keep their layer count constant across levels; only true 3D
// textures halve depth per level (GL 3.3 §3.9 glTexStorage3D). // textures halve depth per level (GL 3.3 §3.9 glTexStorage3D).
const Bool depthMips = DepthParticipatesInMipmapping(textureObject->GetTarget()); 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) { for (GLsizei level = 0; level < levels; ++level) {
const GLsizei levelWidth = std::max<GLsizei>(1, width >> level); const GLsizei levelWidth = std::max<GLsizei>(1, width >> level);
const GLsizei levelHeight = std::max<GLsizei>(1, height >> level); const GLsizei levelHeight = std::max<GLsizei>(1, height >> level);
@@ -4571,6 +4842,19 @@ namespace MobileGL::MG_Impl::GLImpl {
textureMipmapObject->AllocateStorage(textureUploadTarget, level, textureMipmapObject->AllocateStorage(textureUploadTarget, level,
{{levelWidth, levelHeight, levelDepth}, byteSize}); {{levelWidth, levelHeight, levelDepth}, byteSize});
textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, false); 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. // See TextureStorage1D.
textureMipmapObject->TruncateMipmapLevels(textureUploadTarget, static_cast<Uint>(levels)); 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, GLuint dstName, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) { GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
// A missing name is INVALID_VALUE here, where GetTextureObjectByName's own diagnostic is // 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. // SharedPtr, and let the validator record the error this entry point owes.
const SharedPtr<MG_State::GLState::ITextureObject> srcTexture = //
MG_State::pGLContext->GetTextureObject(srcName); // The TARGET picks the namespace: GL 4.6 core 18.3.2 accepts GL_RENDERBUFFER, and a
const SharedPtr<MG_State::GLState::ITextureObject> dstTexture = // renderbuffer name has nothing to do with a texture name. Resolving both through
MG_State::pGLContext->GetTextureObject(dstName); // GetTextureObject made every renderbuffer endpoint INVALID_VALUE - or, when the number
if (!ValidateCopyImageSubData_State(srcTexture, srcTarget, srcLevel, srcX, srcY, dstTexture, dstTarget, // 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)) { dstLevel, dstX, dstY, srcWidth, srcHeight, srcDepth)) {
return; 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); dstX, dstY, dstZ, srcWidth, srcHeight, srcDepth);
} }
@@ -313,9 +313,13 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
return false; return false;
} }
// TexImage in core 3.3 has no stencil-only upload path (that arrived with GL 4.4). // The stencil-only transfer path arrived with GL 4.4 / ARB_texture_stencil8, and only ever
if (format == TextureInputFormat::StencilIndex) { // pairs with stencil-only storage: against a depth, depth-stencil or colour internal format
return recordInvalidOperation("STENCIL_INDEX is not a valid texture upload 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)) { 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, // 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. // 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, // 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 // and DirectGLES can never have it at all. What that costs is the ARRAY, not the call: GL 4.6
// plus a log line naming the reason - rather than accepting state no draw could honour and // core 10.3.2 defines no error for a well-formed glVertexAttribLFormat, and a GL 4.3 context
// rendering garbage. The matching startup POST row is in MG_Util/SelfTest/DriverPost.cpp. // 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, static void VertexAttribLFormatSeparate_State(const SharedPtr<MG_State::GLState::VertexArrayObject>& vao,
GLuint attribindex, GLint size, GLenum type, GLuint attribindex, GLint size, GLenum type,
GLuint relativeoffset) { GLuint relativeoffset) {
@@ -528,14 +533,11 @@ namespace MobileGL::MG_Impl::GLImpl {
if (!MG_Backend::pActiveBackendObject || if (!MG_Backend::pActiveBackendObject ||
!MG_Backend::pActiveBackendObject->GetDynamicParameters().SupportsFloat64VertexAttributes) { !MG_Backend::pActiveBackendObject->GetDynamicParameters().SupportsFloat64VertexAttributes) {
MGLOG_W_ONCE("VertexAttribLFormat: attribute %u asked for a 64-bit (GL_DOUBLE) format, but this " 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 " "backend has no double-precision vertex attribute support - the format is recorded "
"\"64-bit vertex attributes\" / \"shaderFloat64\" POST row for what that costs", "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); 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), vao->SetAttributeFormatSeparate(attribindex, size, MG_Util::ConvertGLEnumToDataType(type),
@@ -80,6 +80,7 @@ add_executable(MobileGLIntegrationTest
Scenarios/ImageLoadStoreSsoScenario.cpp Scenarios/ImageLoadStoreSsoScenario.cpp
Scenarios/ImageTargetKindScenario.cpp Scenarios/ImageTargetKindScenario.cpp
Scenarios/ImageFormatQualifierScenario.cpp Scenarios/ImageFormatQualifierScenario.cpp
Scenarios/ImageSizeAfterRespecScenario.cpp
Scenarios/SsboDeclarationFormScenario.cpp Scenarios/SsboDeclarationFormScenario.cpp
Scenarios/Glsl420DeclarationScenario.cpp Scenarios/Glsl420DeclarationScenario.cpp
Scenarios/FragmentOutputArrayIndexScenario.cpp Scenarios/FragmentOutputArrayIndexScenario.cpp
@@ -91,6 +92,8 @@ add_executable(MobileGLIntegrationTest
Scenarios/CopyImageLevelRangeScenario.cpp Scenarios/CopyImageLevelRangeScenario.cpp
Scenarios/CopyImageLayeredScenario.cpp Scenarios/CopyImageLayeredScenario.cpp
Scenarios/LayeredAttachmentBarrierScenario.cpp Scenarios/LayeredAttachmentBarrierScenario.cpp
Scenarios/LayeredTextureReadbackScenario.cpp
Scenarios/AtomicCounterScenario.cpp
) )
target_include_directories(MobileGLIntegrationTest PRIVATE target_include_directories(MobileGLIntegrationTest PRIVATE
@@ -247,6 +250,19 @@ endif()
set(MGL_ITEST_VULKAN_ENV ${MGL_ITEST_COMMON_ENV}) set(MGL_ITEST_VULKAN_ENV ${MGL_ITEST_COMMON_ENV})
if (MOBILEGL_ITEST_VK_ICD) if (MOBILEGL_ITEST_VK_ICD)
list(APPEND MGL_ITEST_VULKAN_ENV "VK_ICD_FILENAMES=${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() endif()
# The ENVIRONMENT test property is itself a `;`-list, and gtest_discover_tests # 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); 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 } // namespace MGITest
@@ -697,24 +697,127 @@ void main() {
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)); EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
} }
TEST_F(DoublePrecisionScenario, A64BitVertexFormatIsDeclinedOnEveryBackend) { TEST_F(DoublePrecisionScenario, A64BitVertexFormatIsRecordedAndItsArrayIsDroppedAtDraw) {
if (!Ready()) return; if (!Ready()) return;
// The demotion leaves no 64-bit shader input to feed, so there is nothing a 64-bit // 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 // 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 // ones whose device lacks shaderFloat64.
// drawn as garbage; the matching POST row says the same thing at startup. //
// 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; GLuint vao = 0;
glGenVertexArrays(1, &vao); glGenVertexArrays(1, &vao);
glBindVertexArray(vao); glBindVertexArray(vao);
while (glGetError() != GL_NO_ERROR) {} while (glGetError() != GL_NO_ERROR) {}
glVertexAttribLFormat(0, 3, GL_DOUBLE, 0); glVertexAttribLFormat(1, 3, GL_DOUBLE, 8);
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_INVALID_OPERATION)); 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); glBindVertexArray(0);
glDeleteVertexArrays(1, &vao); glDeleteVertexArrays(1, &vao);
while (glGetError() != GL_NO_ERROR) {} 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
} // namespace MGITest } // 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; 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). // Transform feedback objects (ARB_transform_feedback2 / GL 4.0 core).
// The capture state above and the indexed GL_TRANSFORM_FEEDBACK_BUFFER // The capture state above and the indexed GL_TRANSFORM_FEEDBACK_BUFFER
// binding points are object state, but the context keeps exactly one live // binding points are object state, but the context keeps exactly one live
@@ -466,6 +491,13 @@ namespace MobileGL {
Uint64 m_transformFeedbackAccountedCaptureDraws = 0; Uint64 m_transformFeedbackAccountedCaptureDraws = 0;
Uint64 m_transformFeedbackGeometryCaptureDraws = 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. // Everything a transform feedback object owns while it is NOT the bound one.
struct TransformFeedbackObjectState { struct TransformFeedbackObjectState {
struct SavedBufferBinding { struct SavedBufferBinding {
@@ -129,6 +129,180 @@ namespace {
return element; 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) { static bool IsBuiltInPipelineOutput(const glslang::TObjectReflection& output) {
const auto* type = output.getType(); const auto* type = output.getType();
return type && type->getQualifier().builtIn != glslang::EbvNone; return type && type->getQualifier().builtIn != glslang::EbvNone;
@@ -795,6 +969,22 @@ namespace MobileGL::MG_State::GLState {
return false; 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) ---------- // ---------- GL-facing index spaces (relaxed-parse cleanup) ----------
// Blocks first: global-UBO membership drives the uniform filter below. The // Blocks first: global-UBO membership drives the uniform filter below. The
// synthesized MGL_GLOBAL_UBO is a transpiler artifact - its members are GL // 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"); // MGL_GLOBAL_UBO, so reflection cannot provide them ("source-explicit");
// - glslang's layoutLocation() for opaque uniforms, where the qualifier // - glslang's layoutLocation() for opaque uniforms, where the qualifier
// survives the relaxed parse (and mapIO auto-assigns the rest). // 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<Uint> effectiveLocation(tProgramUniformCount, kNoLocation);
Vector<Bool> locationIsSourceExplicit(tProgramUniformCount, false); Vector<Bool> locationIsSourceExplicit(tProgramUniformCount, false);
UnorderedMap<String, Uint> structExplicitCursor; // declared root -> next member location 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)); 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(); effectiveLocation[i] = uniform.layoutLocation();
} }
if (locationIsSourceExplicit[i] && 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 // 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, artifacts.infoLog = std::format("Uniform '{}' explicit location {} is out of range.", uniform.name,
effectiveLocation[i]); effectiveLocation[i]);
ProgramObject::ResetLinkArtifacts(artifacts); 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) { for (const Int i : artifacts.glUniformIndexToTProgram) {
auto& uniform = artifacts.program->getUniform(i); auto& uniform = artifacts.program->getUniform(i);
const Uint location = effectiveLocation[i]; const Uint location = effectiveLocation[i];
const Int locationSpan = GetUniformLocationSpan(uniform); const Int locationSpan = GetUniformLocationSpan(uniform);
requiredUniformLocations += locationSpan; requiredUniformLocations += locationSpan;
const Bool inNamedBlock = uniform.index >= 0 && !isGlobalUboMember(uniform);
if (!inNamedBlock) defaultBlockLocationDemand += locationSpan;
if (location != kNoLocation) { if (location != kNoLocation) {
artifacts.maxUniformLocation = std::max(artifacts.maxUniformLocation, location + locationSpan - 1); 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", MGLOG_D("ProgramObject %u: Reflection - computed maxUniformLocation=%u uniformNameMaxLength=%d",
in.externalIndex, artifacts.maxUniformLocation, artifacts.uniformNameMaxLength); 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) { if (artifacts.maxUniformLocation + 1 < requiredUniformLocations) {
MGLOG_D("ProgramObject %u: Reflection - maxUniformLocation+1 (%u) < requiredUniformLocations (%d), " MGLOG_D("ProgramObject %u: Reflection - maxUniformLocation+1 (%u) < requiredUniformLocations (%d), "
"adjusting", "adjusting",
@@ -930,6 +1194,27 @@ namespace MobileGL::MG_State::GLState {
glslang::TQualifier::layoutLocationEnd); glslang::TQualifier::layoutLocationEnd);
artifacts.uniformSamplerOrImageUnitIndex.resize(artifacts.maxUniformLocation + 1, -1); 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; Vector<int> unallocatedUniformIndex;
// Pass 1: source-explicit locations. These are API contract // 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; Bool spanIsFree = location + locationSpan - 1 <= artifacts.maxUniformLocation;
for (Int element = 0; spanIsFree && element < locationSpan; ++element) { for (Int element = 0; spanIsFree && element < locationSpan; ++element) {
spanIsFree = spanIsFree =
artifacts.uniformIndexInTProgram[location + element] == glslang::TQualifier::layoutLocationEnd; artifacts.uniformIndexInTProgram[location + element] == glslang::TQualifier::layoutLocationEnd &&
!locationIsReserved(location + element);
} }
if (!spanIsFree) { if (!spanIsFree) {
artifacts.uniformLocations[uniform.name] = kNoLocation; artifacts.uniformLocations[uniform.name] = kNoLocation;
@@ -1006,7 +1292,8 @@ namespace MobileGL::MG_State::GLState {
bool hasRoom = locNeedle + locationSpan - 1 <= artifacts.maxUniformLocation; bool hasRoom = locNeedle + locationSpan - 1 <= artifacts.maxUniformLocation;
for (Int element = 0; hasRoom && element < locationSpan; ++element) { for (Int element = 0; hasRoom && element < locationSpan; ++element) {
hasRoom = artifacts.uniformIndexInTProgram[locNeedle + element] == hasRoom = artifacts.uniformIndexInTProgram[locNeedle + element] ==
glslang::TQualifier::layoutLocationEnd; glslang::TQualifier::layoutLocationEnd &&
!locationIsReserved(locNeedle + element);
} }
if (!hasRoom) continue; if (!hasRoom) continue;
// Found a vacant location at locNeedle // Found a vacant location at locNeedle
@@ -1239,7 +1526,6 @@ namespace MobileGL::MG_State::GLState {
} }
artifacts.lastStageIsFragment = program.getIntermediate(EShLangFragment) != nullptr; artifacts.lastStageIsFragment = program.getIntermediate(EShLangFragment) != nullptr;
artifacts.atomicCounterCount = program.getNumAtomicCounters();
for (Uint dim = 0; dim < 3u; ++dim) { for (Uint dim = 0; dim < 3u; ++dim) {
artifacts.computeLocalSize[dim] = program.getLocalSize(static_cast<Int>(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) { 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 auto explicitLocation = Artifacts().linkedFragDataLocation.find(name);
const Int outputCount = Artifacts().program->getNumPipeOutputs(); for (const PipeOutputReflection& output : Artifacts().pipeOutputReflection) {
for (Int index = 0; index < outputCount; ++index) {
const auto& output = Artifacts().program->getPipeOutput(index);
if (output.name != name) continue; if (output.name != name) continue;
if (explicitLocation != Artifacts().linkedFragDataLocation.end()) return static_cast<Int>(explicitLocation->second); if (explicitLocation != Artifacts().linkedFragDataLocation.end()) {
return static_cast<Int>(output.layoutLocation()); return static_cast<Int>(explicitLocation->second);
}
return output.location;
} }
return -1; return -1;
} }
@@ -24,6 +24,20 @@ namespace MobileGL::MG_State::GLState {
class ProgramObject { class ProgramObject {
public: 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 // 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 // predicates, no recursion: nothing post-link ever walks a struct, a type name or the
// AST, so a POD covers the whole surface exactly. // 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 // SIGSEGV inside glslang::TProgram::getNumPipeInputs - KHR-GL30.api.coverage does exactly
// this after a failed glGetAttribLocation, and reached it as soon as the CopyTexImage2D // this after a failed glGetAttribLocation, and reached it as soon as the CopyTexImage2D
// throw ahead of it stopped killing the run first. // throw ahead of it stopped killing the run first.
Int GetActiveAtomicCounterCount() const {
return Artifacts().atomicCounterCount;
}
Int GetActiveAttributesCount() const { Int GetActiveAttributesCount() const {
return static_cast<Int>(Artifacts().pipeInputReflection.size()); 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. // outputs are varyings and must report -1 (KHR-GL43.program_interface_query.
// separate-programs-tess-control). // separate-programs-tess-control).
Bool lastStageIsFragment = false; Bool lastStageIsFragment = false;
Int atomicCounterCount = 0;
Array<GLuint, 3> computeLocalSize{}; Array<GLuint, 3> computeLocalSize{};
// Replaces program->getUniformIndex(name). Maps the reflected name to its // Replaces program->getUniformIndex(name). Maps the reflected name to its
// TProgram uniform index. // TProgram uniform index.
@@ -8,6 +8,7 @@
#include "ShaderCompileTask.h" #include "ShaderCompileTask.h"
#include <MG_State/GLState/BufferState/BufferState.h>
#include <MG_Util/Converters/MGToGL/ProgramEnumConverter.h> #include <MG_Util/Converters/MGToGL/ProgramEnumConverter.h>
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h> #include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <MG_Util/ShaderTranspiler/ShaderSourceProcessor.h> #include <MG_Util/ShaderTranspiler/ShaderSourceProcessor.h>
@@ -15,6 +16,7 @@
#include <glslang/Include/PoolAlloc.h> #include <glslang/Include/PoolAlloc.h>
#include <algorithm>
#include <charconv> #include <charconv>
namespace { namespace {
@@ -137,8 +139,21 @@ namespace {
return std::nullopt; 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 // 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 // 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. // 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. // Deliberately free of any per-object state so the memo is sound.
@@ -172,6 +187,13 @@ namespace {
return result; 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 // 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 // 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 // what the backends' SPIR-V is generated from - there is no second, GL-client
@@ -26,6 +26,9 @@ namespace MobileGL::MG_State::GLState {
ComputeLocalSizeRejected, ComputeLocalSizeRejected,
// FindReservedIdentifierViolation rejected it. // FindReservedIdentifierViolation rejected it.
ReservedIdentifierRejected, 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. // 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. // Memoizing this saves the parse itself on every later object with that source.
ParseFailed, ParseFailed,
@@ -48,6 +48,7 @@ namespace MobileGL {
m_dirtyRects.resize(requiredLevelCount); m_dirtyRects.resize(requiredLevelCount);
m_compressedData.resize(requiredLevelCount); m_compressedData.resize(requiredLevelCount);
m_compressedFormats.resize(requiredLevelCount, GL_NONE); m_compressedFormats.resize(requiredLevelCount, GL_NONE);
m_requestedCompressedFormats.resize(requiredLevelCount, GL_NONE);
} }
m_texelSizes[level] = input.texelSize; m_texelSizes[level] = input.texelSize;
@@ -79,6 +80,9 @@ namespace MobileGL {
m_compressedFormats[level] = GL_NONE; m_compressedFormats[level] = GL_NONE;
m_compressedData[level].clear(); m_compressedData[level].clear();
m_compressedData[level].shrink_to_fit(); 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) { void MipmapStorage::SetCompressedImage(Uint level, GLenum internalFormat, const void* data, SizeT size) {
@@ -110,6 +114,16 @@ namespace MobileGL {
return m_compressedData[level].data(); 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) { void MipmapStorage::TruncateToLevelCount(SizeT levelCount) {
if (levelCount >= m_data.size()) return; if (levelCount >= m_data.size()) return;
@@ -120,6 +134,7 @@ namespace MobileGL {
m_dirtyRects.resize(levelCount); m_dirtyRects.resize(levelCount);
m_compressedData.resize(levelCount); m_compressedData.resize(levelCount);
m_compressedFormats.resize(levelCount); m_compressedFormats.resize(levelCount);
m_requestedCompressedFormats.resize(levelCount);
} }
void MipmapStorage::UpdateSubData(Uint level, DataPtr input) { void MipmapStorage::UpdateSubData(Uint level, DataPtr input) {
@@ -96,6 +96,18 @@ namespace MobileGL {
SizeT GetCompressedByteSize(Uint level) const; SizeT GetCompressedByteSize(Uint level) const;
const void* MapCompressedData(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: protected:
// Insert one clamped, non-empty write box, keeping the list disjoint // Insert one clamped, non-empty write box, keeping the list disjoint
// and bounded (see kMaxDirtyRects). // and bounded (see kMaxDirtyRects).
@@ -115,6 +127,7 @@ namespace MobileGL {
Vector<Vector<MipmapDirtyRegion>> m_dirtyRects; Vector<Vector<MipmapDirtyRegion>> m_dirtyRects;
Vector<Vector<Uint8>> m_compressedData; Vector<Vector<Uint8>> m_compressedData;
Vector<GLenum> m_compressedFormats; Vector<GLenum> m_compressedFormats;
Vector<GLenum> m_requestedCompressedFormats;
}; };
} // namespace GLState } // namespace GLState
} // namespace MG_State } // namespace MG_State
@@ -111,6 +111,16 @@ namespace MobileGL {
return m_storage[targetIndex].MapCompressedData(level); 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: protected:
Array<MipmapStorage, TargetCount> m_storage; Array<MipmapStorage, TargetCount> m_storage;
}; };
@@ -250,6 +250,10 @@ namespace MobileGL {
return m_contentVersion; return m_contentVersion;
} }
Uint64 TextureObjectBase::GetShapeVersion() const {
return m_shapeVersion;
}
Bool TextureObjectBase::IsMipmapCompleteForFilterCached(Bool mipmapped) const { Bool TextureObjectBase::IsMipmapCompleteForFilterCached(Bool mipmapped) const {
const int slot = mipmapped ? 1 : 0; const int slot = mipmapped ? 1 : 0;
if (m_completeMemoShapeVersion[slot] == m_shapeVersion) { if (m_completeMemoShapeVersion[slot] == m_shapeVersion) {
@@ -373,6 +377,18 @@ namespace MobileGL {
return m_textureStorage.MapCompressedData(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel); 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 { IntVec3 TextureObjectWithOneMipmap::GetBaseSize() const {
if (m_textureStorage.GetLevelCount() == 0) { if (m_textureStorage.GetLevelCount() == 0) {
return {0, 0, 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 // 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). // textures across draws (e.g. the block atlas bound across a whole terrain batch).
virtual Uint64 GetContentVersion() const = 0; 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 // Answers IsMipmapCompleteForFilter() from a memo. Sampling completeness is a
// property of the texture's SHAPE - level sizes, level count, level range, // 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 // 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; void SetImmutableLevels(Uint levels) override;
Uint16 GetTextureParamsVersion() const override; Uint16 GetTextureParamsVersion() const override;
Uint64 GetContentVersion() const override; Uint64 GetContentVersion() const override;
Uint64 GetShapeVersion() const override;
Bool IsMipmapCompleteForFilterCached(Bool mipmapped) const override; Bool IsMipmapCompleteForFilterCached(Bool mipmapped) const override;
// Bumps the content version without touching per-level storage-dirty flags. Used when the // 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 // 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 GLenum GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const = 0;
virtual SizeT GetMipmapCompressedByteSize(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; 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 // 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; GLenum GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
SizeT GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override; SizeT GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
const void* MapMipmapCompressedImage(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; IntVec3 GetBaseSize() const override;
Bool IsComplete() const override; Bool IsComplete() const override;
@@ -96,6 +96,18 @@ namespace MobileGL {
return m_textureStorage.MapCompressedData(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel); 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 { Uint TextureObject2DCube::GetIndexOfTextureUploadTarget(TextureUploadTarget target) const {
MOBILEGL_ASSERT(TextureUploadTarget::CubeMapPositiveX <= target && MOBILEGL_ASSERT(TextureUploadTarget::CubeMapPositiveX <= target &&
target <= TextureUploadTarget::CubeMapNegativeZ, target <= TextureUploadTarget::CubeMapNegativeZ,
@@ -39,6 +39,10 @@ namespace MobileGL {
SizeT GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override; SizeT GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
const void* MapMipmapCompressedImage(TextureUploadTarget uploadTarget, const void* MapMipmapCompressedImage(TextureUploadTarget uploadTarget,
Uint mipmapLevel) const override; 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; IntVec3 GetBaseSize() const override;
Bool IsComplete() 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} ${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) include(GoogleTest)
gtest_discover_tests(EsslShaderPassTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit) 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); const String out = SplitReadWriteImageUniforms(source);
// Both halves: same binding, same format, same type - which is what makes two image // Both halves: same binding, same format, same type - which is what makes two image
// variables on one image unit legal. // variables on one image unit legal - and both `coherent`, which is what makes the store
EXPECT_TRUE(Contains(out, "layout(binding = 2, rgba8) uniform readonly highp image2D goku;")); // through one of them visible to the load through the other.
EXPECT_TRUE(Contains(out, "layout(binding = 2, rgba8) uniform writeonly highp image2D " + WriteAlias("goku") + ";")); 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. // The load keeps the original name, the store moves to the writeonly half.
EXPECT_TRUE(Contains(out, "imageLoad(goku,")); EXPECT_TRUE(Contains(out, "imageLoad(goku,"));
@@ -152,9 +154,9 @@ void main()
} }
)"; )";
const String out = SplitReadWriteImageUniforms(source); 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 coherent readonly highp image2D gohan[3];"));
EXPECT_TRUE(Contains(out, EXPECT_TRUE(Contains(
"layout(binding = 6, rgba8) uniform writeonly highp image2D " + WriteAlias("gohan") + "[3];")); 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, "imageStore(" + WriteAlias("gohan") + "[1],"));
EXPECT_TRUE(Contains(out, "imageLoad(gohan[2],")); EXPECT_TRUE(Contains(out, "imageLoad(gohan[2],"));
} }
@@ -174,9 +176,11 @@ void main()
)"; )";
const String out = SplitReadWriteImageUniforms(source); const String out = SplitReadWriteImageUniforms(source);
// goku is read+write -> split; goku_hd is write-only -> qualified in place, not split. // goku is read+write -> split (and coherent with it); goku_hd is write-only -> qualified in
EXPECT_TRUE(Contains(out, "layout(binding = 1, rgba8) uniform readonly highp image2D goku;")); // place, not split, and left non-coherent because nothing aliases it.
EXPECT_TRUE(Contains(out, "layout(binding = 1, rgba8) uniform writeonly highp image2D " + WriteAlias("goku") + ";")); 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, "layout(binding = 2, rgba8) uniform writeonly highp image2D goku_hd;"));
EXPECT_TRUE(Contains(out, "imageStore(goku_hd,")); EXPECT_TRUE(Contains(out, "imageStore(goku_hd,"));
EXPECT_FALSE(Contains(out, WriteAlias("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 readonly coherent restrict highp image2D goku;"));
EXPECT_TRUE( EXPECT_TRUE(
Contains(out, "uniform writeonly coherent restrict highp image2D " + WriteAlias("goku") + ";")); 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 // imageSize reads no texels and writes none, so it decides nothing; readonly is what keeps
@@ -34,6 +34,17 @@ namespace {
GLint maxFragmentImageUniforms = 4; GLint maxFragmentImageUniforms = 4;
GLint maxComputeImageUniforms = 5; GLint maxComputeImageUniforms = 5;
bool maxGeometryImageUniformsQueried = false; 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 minFragmentInterpolationOffset = -0.75f;
GLfloat maxFragmentInterpolationOffset = 0.625f; GLfloat maxFragmentInterpolationOffset = 0.625f;
GLint fragmentInterpolationOffsetBits = 6; GLint fragmentInterpolationOffsetBits = 6;
@@ -111,7 +122,30 @@ namespace {
funcs.glGetIntegerv = [](GLenum pname, GLint* data) { funcs.glGetIntegerv = [](GLenum pname, GLint* data) {
switch (pname) { switch (pname) {
case GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS: 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; break;
case GL_MAX_VERTEX_IMAGE_UNIFORMS: case GL_MAX_VERTEX_IMAGE_UNIFORMS:
*data = g_fake.maxVertexImageUniforms; *data = g_fake.maxVertexImageUniforms;
@@ -400,6 +434,10 @@ namespace {
MobileGL::MG_External::GLESCapabilities MakeEs31Capabilities() { MobileGL::MG_External::GLESCapabilities MakeEs31Capabilities() {
MobileGL::MG_External::GLESCapabilities caps; MobileGL::MG_External::GLESCapabilities caps;
caps.GLESVersion = {3, 1, 0}; 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; return caps;
} }
@@ -527,6 +565,83 @@ TEST(ImageUniformCapabilities, QueriesRealPerStageLimitsAndConservativelyGatesGe
EXPECT_TRUE(g_fake.maxGeometryImageUniformsQueried); 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) { TEST(FragmentInterpolationCapabilities, QueriesOnlyWhenSupportedAndPreservesDriverLimits) {
const auto funcs = MakeFakeGLESFunctions(); const auto funcs = MakeFakeGLESFunctions();
@@ -716,6 +716,177 @@ void main() {
EXPECT_EQ(TakeError(), GL_INVALID_ENUM); 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 ------ // --------------------------------------------------------- transform-feedback ------
TEST_F(ProgramInterfaceTest, TransformFeedbackVaryingTypes) { TEST_F(ProgramInterfaceTest, TransformFeedbackVaryingTypes) {
const char* vs = R"(#version 430 const char* vs = R"(#version 430
+73
View File
@@ -3239,3 +3239,76 @@ TEST_F(ProgramTest, CreateShaderAndCreateShaderProgramvReportTheRightErrorClasse
EXPECT_NE(program, 0u); EXPECT_NE(program, 0u);
EXPECT_EQ(GetError(), GL_NO_ERROR); 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(0x8051 /*GL_RGB8*/));
EXPECT_FALSE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(0 /*GL_NONE*/)); 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; } 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() { void InstallStubBackendXfbQueries() {
auto& backendGL = MG_Backend::gBackendFunctionsTable.GL; auto& backendGL = MG_Backend::gBackendFunctionsTable.GL;
backendGL.BeginXfbPrimitivesQuery = StubBeginXfbPrimitivesQuery; backendGL.BeginXfbPrimitivesQuery = StubBeginXfbPrimitivesQuery;
@@ -677,6 +700,108 @@ TEST_F(QueryTest, PrimitivesGeneratedKeepsTheBackendResultUnderTheCpuPreference)
// unified truthy rule (set, non-empty, not "0", case-insensitive not "false"). // unified truthy rule (set, non-empty, not "0", case-insensitive not "false").
// Running the binary under MOBILEGL_DISABLE_TIMERQUERY=1 therefore exercises // Running the binary under MOBILEGL_DISABLE_TIMERQUERY=1 therefore exercises
// the real end-to-end path rather than the struct field alone. // 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) { TEST_F(QueryTest, DisableTimerQueryFeatureMatchesEnvironment) {
const char* raw = std::getenv("MOBILEGL_DISABLE_TIMERQUERY"); const char* raw = std::getenv("MOBILEGL_DISABLE_TIMERQUERY");
Bool expected = false; Bool expected = false;
+172
View File
@@ -17,6 +17,7 @@
#include <MG_Backend/DirectGLES/Managers.h> #include <MG_Backend/DirectGLES/Managers.h>
#include <MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h> #include <MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h>
#include <MG_Backend/BackendObjects.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/Getter/GL_Getter.h>
#include <MG_Impl/GLImpl/RenderState/GL_RenderState.h> #include <MG_Impl/GLImpl/RenderState/GL_RenderState.h>
#include <MG_Impl/GLImpl/Texture/GL_Texture.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 // MaxShaderStorageBufferBindings - 1 = 12, so a regression that stops reading the
// probed cap and falls back to the struct default would surface as "binding = 7". // probed cap and falls back to the struct default would surface as "binding = 7".
caps.MaxShaderStorageBufferBindings = 13; 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 const MobileGL::String source = R"(#version 310 es
highp int mg_BaseInstanceLowered; highp int mg_BaseInstanceLowered;
@@ -403,6 +411,9 @@ TEST(DirectGLESSanity, TheIndirectWordIndexIsOneBasedSoItsUnwrittenValueMeansNot
auto& caps = MobileGL::MG_Backend::DirectGLES::g_GLESCapabilities; auto& caps = MobileGL::MG_Backend::DirectGLES::g_GLESCapabilities;
caps.IndirectDrawInstanceIdIncludesBaseInstance = false; caps.IndirectDrawInstanceIdIncludesBaseInstance = false;
caps.MaxShaderStorageBufferBindings = 13; 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 const MobileGL::String source = R"(#version 310 es
highp int mg_BaseInstanceLowered; highp int mg_BaseInstanceLowered;
@@ -428,6 +439,10 @@ TEST(DirectGLESSanity, KeepsInstanceIdWhenIndirectDrawsAreConforming) {
auto& caps = MobileGL::MG_Backend::DirectGLES::g_GLESCapabilities; auto& caps = MobileGL::MG_Backend::DirectGLES::g_GLESCapabilities;
caps.IndirectDrawInstanceIdIncludesBaseInstance = false; caps.IndirectDrawInstanceIdIncludesBaseInstance = false;
caps.MaxShaderStorageBufferBindings = 13; 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 const MobileGL::String source = R"(#version 310 es
highp int mg_BaseInstanceLowered; highp int mg_BaseInstanceLowered;
@@ -442,6 +457,8 @@ void main() {
EXPECT_EQ(rewritten.find("mg_ZeroBasedInstanceID"), MobileGL::String::npos); EXPECT_EQ(rewritten.find("mg_ZeroBasedInstanceID"), MobileGL::String::npos);
EXPECT_NE(rewritten.find("int instance = gl_InstanceID + mg_BaseInstanceLowered;"), 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) { TEST(DirectGLESSanity, LeavesDrawParameterGlobalsAloneOutsideVertexShaders) {
@@ -913,6 +930,161 @@ void main() {
MG_Backend::pActiveBackendObject.reset(); 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) { TEST(GetterSanity, ReportsKhrSubgroupDynamicParameters) {
using namespace MobileGL; using namespace MobileGL;
@@ -544,14 +544,16 @@ namespace {
// (2) the advertised extension vector, including the fp64 flag's own extension // (2) the advertised extension vector, including the fp64 flag's own extension
a.advertisedExtensions = {E_GL_ARB_gpu_shader_fp64, E_GL_KHR_debug}; a.advertisedExtensions = {E_GL_ARB_gpu_shader_fp64, E_GL_KHR_debug};
b.advertisedExtensions = {}; b.advertisedExtensions = {};
// (3) the compute limits (ValidateComputeLocalSizeLimits only) // (3) the compute INVOCATION limit, and deliberately not the work-group size or
a.maxComputeWorkGroupSize[0] = 1024; // count any more. Those two used to sit here on the grounds that
a.maxComputeWorkGroupSize[1] = 1024; // ValidateComputeLocalSizeLimits was their only consumer; wave3 (cb155c5b) made
a.maxComputeWorkGroupSize[2] = 64; // 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; a.maxComputeWorkGroupInvocations = 128;
b.maxComputeWorkGroupSize[0] = 2048;
b.maxComputeWorkGroupSize[1] = 2048;
b.maxComputeWorkGroupSize[2] = 1024;
b.maxComputeWorkGroupInvocations = 2048; b.maxComputeWorkGroupInvocations = 2048;
// (4) a spread of DynamicBackendParameters fields the front end never reads // (4) a spread of DynamicBackendParameters fields the front end never reads
a.params.MaxColorTextureSamples = 1; 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 // 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. // 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) { TEST_F(TranslationCacheTest, TheFrontendFingerprintMovesWithEveryFrontendLimit) {
const CompileEnv base; const CompileEnv base;
const Uint64 baseline = ComputeFrontendCompileEnvFingerprint(base); const Uint64 baseline = ComputeFrontendCompileEnvFingerprint(base);
@@ -613,6 +618,21 @@ TEST_F(TranslationCacheTest, TheFrontendFingerprintMovesWithEveryFrontendLimit)
{"params.MaxComputeImageUniforms", [](CompileEnv& e) { e.params.MaxComputeImageUniforms += 1; }}, {"params.MaxComputeImageUniforms", [](CompileEnv& e) { e.params.MaxComputeImageUniforms += 1; }},
{"params.MaxCombinedImageUniforms", [](CompileEnv& e) { e.params.MaxCombinedImageUniforms += 1; }}, {"params.MaxCombinedImageUniforms", [](CompileEnv& e) { e.params.MaxCombinedImageUniforms += 1; }},
{"params.MaxVertexAttribs", [](CompileEnv& e) { e.params.MaxVertexAttribs += 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 // 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. // storage capacity rather than the driver's number, so the bit is load-bearing.
{"HasBackend", [](CompileEnv& e) { e.backend = BackendType::DirectGLES; }}, {"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 // 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. // produced. Without this the case above would still pass if the cache returned garbage.
TEST_F(TranslationCacheTest, L1HitsAgreeWithACacheDisabledTranslation) { 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)); 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 // 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. // is authoritative and GetIntegerv would otherwise fall through to its INVALID_ENUM default.
TEST_F(TextureTest, MaxTextureMaxAnisotropyIsAnsweredFromTheBackendLimit) { TEST_F(TextureTest, MaxTextureMaxAnisotropyIsAnsweredFromTheBackendLimit) {
@@ -1023,6 +1057,32 @@ TEST_F(TextureTest, TexImage2DAcceptsSpecCompliantFormatCombinations) {
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); 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 // 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 // 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. // 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); 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) { TEST_F(TextureTest, GetTextureSubImageReadsFullNamedLevelWithoutBinding) {
GLuint texture = 0; GLuint texture = 0;
GLuint boundTexture = 0; GLuint boundTexture = 0;
@@ -1651,6 +1761,56 @@ TEST_F(TextureTest, AnUncompressedRespecificationClearsTheCompressedTag) {
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); 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 { namespace {
// A 16x16 RGBA8 texture with exactly `levelCount` levels, defined the way // A 16x16 RGBA8 texture with exactly `levelCount` levels, defined the way
// KHR-GL43.copy_image.non_existent_mipmap defines its textures - glTexImage2D per // KHR-GL43.copy_image.non_existent_mipmap defines its textures - glTexImage2D per
@@ -3320,6 +3480,29 @@ TEST(SharedExponentRGB9E5Test, RawPackedPixelTransferCoversOnlyIdenticalLayouts)
TexturePixelDataType::UnsignedInt5999Rev)); 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) { TEST_F(TextureTest, TexImage2DRGB9E5KeepsNonCanonicalClientWords) {
// Upload direction: GL_RGB / GL_UNSIGNED_INT_5_9_9_9_REV into GL_RGB9_E5 stores the client // 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. // words untouched, including the redundant encodings the CTS generates.
@@ -4011,24 +4194,30 @@ TEST_F(TextureTest, CopyTexImage1DReportsUnsupportedInsteadOfTerminating) {
ExpectSingleGlError(GL_INVALID_OPERATION); ExpectSingleGlError(GL_INVALID_OPERATION);
} }
TEST_F(TextureTest, GetTexLevelParameterOnBufferStorageReportsErrorInsteadOfTerminating) { TEST_F(TextureTest, GetTexLevelParameterAnswersBufferStorageGeometry) {
// TextureStorageType is {Mipmap, Buffer} and the level queries only answer out of a mipmap // TextureStorageType is {Mipmap, Buffer} and the level queries used to answer only out of a
// chain, so every glGetTexLevelParameter* on a GL_TEXTURE_BUFFER texture reached a // mipmap chain, so every glGetTexLevelParameter* on a GL_TEXTURE_BUFFER texture reached a
// THROW_UNIMPL_EXCEPTION default: label and killed the process. // 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; GLuint texture = 0;
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_BUFFER, 1, &texture); MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_BUFFER, 1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_BUFFER, texture); MG_Impl::GLImpl::BindTexture(GL_TEXTURE_BUFFER, texture);
MG_Impl::GLImpl::TexBuffer(GL_TEXTURE_BUFFER, GL_R8, 0); MG_Impl::GLImpl::TexBuffer(GL_TEXTURE_BUFFER, GL_R8, 0);
DrainPendingGlErrors(); 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; GLint intParam = 0x20202020;
MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_BUFFER, 0, pname, &intParam); 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; GLfloat floatParam = 12345.0f;
MG_Impl::GLImpl::GetTexLevelParameterfv(GL_TEXTURE_BUFFER, 0, pname, &floatParam); 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 SrcZ = -1;
GLint DstZ = -1; GLint DstZ = -1;
GLsizei Depth = -1; GLsizei Depth = -1;
Bool SrcIsRenderbuffer = false;
Bool DstIsRenderbuffer = false;
} g_copyImageSubDataCall; } 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, 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, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLsizei srcWidth,
GLsizei srcHeight, GLsizei srcDepth) { GLsizei srcHeight, GLsizei srcDepth) {
(void)srcTexture;
(void)srcLevel; (void)srcLevel;
(void)srcX; (void)srcX;
(void)srcY; (void)srcY;
(void)dstTexture;
(void)dstLevel; (void)dstLevel;
(void)dstX; (void)dstX;
(void)dstY; (void)dstY;
(void)srcWidth; (void)srcWidth;
(void)srcHeight; (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 // 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::GenTextures(1, &reusedSrc);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 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::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::GenTextures(1, &reusedDst);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 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::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); 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, 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(g_copyImageSubDataCall.DstTarget, static_cast<GLenum>(GL_TEXTURE_RECTANGLE));
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); 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) { !f.glUnmapBuffer || !f.glMemoryBarrier || !f.glCreateShader || !f.glCreateProgram) {
return false; return false;
} }
GLint maxVertexSsboBlocks = 0; // Read from caps, not re-queried: the per-stage limits are resolved (and their query
f.glGetIntegerv(GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS, &maxVertexSsboBlocks); // 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) { if (maxVertexSsboBlocks < 1) {
// The native indirect machinery cannot read the command buffer from the vertex // The native indirect machinery cannot read the command buffer from the vertex
// stage on this driver anyway; assume conforming zero-based gl_InstanceID. // stage on this driver anyway; assume conforming zero-based gl_InstanceID.
@@ -1036,6 +1040,15 @@ namespace MobileGL::MG_Util::BackendLoader {
GLint maxVertexAttribs = 16; GLint maxVertexAttribs = 16;
GLint maxComputeShaderStorageBlocks = 8; GLint maxComputeShaderStorageBlocks = 8;
GLint maxCombinedShaderStorageBlocks = 32; 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 maxComputeUniformBlocks = 12;
GLint maxComputeWorkGroupInvocations = 128; GLint maxComputeWorkGroupInvocations = 128;
GLint maxShaderStorageBufferBindings = 8; GLint maxShaderStorageBufferBindings = 8;
@@ -1127,6 +1140,59 @@ namespace MobileGL::MG_Util::BackendLoader {
(caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 2)) { (caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 2)) {
glesFuncs.glGetIntegerv(GL_MAX_GEOMETRY_IMAGE_UNIFORMS, &maxGeometryImageUniforms); 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_DRAW_BUFFERS, &maxDrawBuffers);
glesFuncs.glGetIntegerv(GL_MAX_COLOR_ATTACHMENTS, &maxColorAttachments); glesFuncs.glGetIntegerv(GL_MAX_COLOR_ATTACHMENTS, &maxColorAttachments);
glesFuncs.glGetIntegerv(GL_MAX_CLIP_DISTANCES, &maxClipDistances); glesFuncs.glGetIntegerv(GL_MAX_CLIP_DISTANCES, &maxClipDistances);
@@ -1271,6 +1337,11 @@ namespace MobileGL::MG_Util::BackendLoader {
caps.MaxVertexAttribs = maxVertexAttribs; caps.MaxVertexAttribs = maxVertexAttribs;
caps.MaxComputeShaderStorageBlocks = maxComputeShaderStorageBlocks; caps.MaxComputeShaderStorageBlocks = maxComputeShaderStorageBlocks;
caps.MaxCombinedShaderStorageBlocks = maxCombinedShaderStorageBlocks; caps.MaxCombinedShaderStorageBlocks = maxCombinedShaderStorageBlocks;
caps.MaxVertexShaderStorageBlocks = maxVertexShaderStorageBlocks;
caps.MaxTessControlShaderStorageBlocks = maxTessControlShaderStorageBlocks;
caps.MaxTessEvaluationShaderStorageBlocks = maxTessEvaluationShaderStorageBlocks;
caps.MaxGeometryShaderStorageBlocks = maxGeometryShaderStorageBlocks;
caps.MaxFragmentShaderStorageBlocks = maxFragmentShaderStorageBlocks;
caps.MaxComputeUniformBlocks = maxComputeUniformBlocks; caps.MaxComputeUniformBlocks = maxComputeUniformBlocks;
caps.MaxComputeWorkGroupInvocations = maxComputeWorkGroupInvocations; caps.MaxComputeWorkGroupInvocations = maxComputeWorkGroupInvocations;
caps.MaxShaderStorageBufferBindings = maxShaderStorageBufferBindings; 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_VERTEX_ATTRIBS: %d", caps.MaxVertexAttribs);
MGLOG_I(" GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS: %d", caps.MaxComputeShaderStorageBlocks); MGLOG_I(" GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS: %d", caps.MaxComputeShaderStorageBlocks);
MGLOG_I(" GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS: %d", caps.MaxCombinedShaderStorageBlocks); 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_UNIFORM_BLOCKS: %d", caps.MaxComputeUniformBlocks);
MGLOG_I(" GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS: %d", caps.MaxComputeWorkGroupInvocations); MGLOG_I(" GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS: %d", caps.MaxComputeWorkGroupInvocations);
MGLOG_I(" GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS: %d", caps.MaxShaderStorageBufferBindings); MGLOG_I(" GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS: %d", caps.MaxShaderStorageBufferBindings);
@@ -1248,6 +1248,17 @@ namespace MobileGL {
Int MaxVertexAttribs = 16; Int MaxVertexAttribs = 16;
Int MaxComputeShaderStorageBlocks = 8; Int MaxComputeShaderStorageBlocks = 8;
Int MaxCombinedShaderStorageBlocks = 32; 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 MaxComputeUniformBlocks = 12;
Int MaxComputeWorkGroupInvocations = 128; Int MaxComputeWorkGroupInvocations = 128;
Int MaxShaderStorageBufferBindings = 8; Int MaxShaderStorageBufferBindings = 8;
@@ -253,6 +253,12 @@ namespace MobileGL {
return TextureInternalFormat::Depth32FStencil8; return TextureInternalFormat::Depth32FStencil8;
case GL_STENCIL_INDEX8: case GL_STENCIL_INDEX8:
return TextureInternalFormat::StencilIndex8; 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: case GL_DEPTH_COMPONENT:
return TextureInternalFormat::DepthComponent; return TextureInternalFormat::DepthComponent;
case GL_DEPTH_STENCIL: case GL_DEPTH_STENCIL:
@@ -124,6 +124,9 @@ namespace MobileGL {
case TextureInternalFormat::DepthComponent32F: case TextureInternalFormat::DepthComponent32F:
case TextureInternalFormat::Depth24Stencil8: case TextureInternalFormat::Depth24Stencil8:
case TextureInternalFormat::Depth32FStencil8: 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; return internalformat;
// probably we should assume unorm here? // probably we should assume unorm here?
case TextureInternalFormat::RGBA: { case TextureInternalFormat::RGBA: {
+9 -5
View File
@@ -521,9 +521,11 @@ namespace MobileGL::MG_Util::SelfTest {
builder.Warn("64-bit vertex attributes", builder.Warn("64-bit vertex attributes",
"not supported (ES has no GL_DOUBLE vertex format, and after the fp64 demotion " "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); " "above there is no 64-bit shader input left to feed either); "
"glVertexAttribLFormat / glVertexArrayAttribLFormat report " "glVertexAttribLFormat / glVertexArrayAttribLFormat succeed and their state is "
"GL_INVALID_OPERATION - feed the attribute with glVertexAttribPointer(GL_FLOAT), " "queryable, but an ENABLED 64-bit array is DROPPED at draw and the attribute "
"which a demoted dvec input reads correctly"); "reads its generic current value - feed the attribute with "
"glVertexAttribPointer(GL_FLOAT) instead, which a demoted dvec input reads "
"correctly");
if (glesFuncs.glPatchParameteri != nullptr) { if (glesFuncs.glPatchParameteri != nullptr) {
builder.Pass("Tessellation patch parameters", builder.Pass("Tessellation patch parameters",
"glPatchParameteri present (GL_PATCH_VERTICES reaches the driver)"); "glPatchParameteri present (GL_PATCH_VERTICES reaches the driver)");
@@ -2336,8 +2338,10 @@ namespace MobileGL::MG_Util::SelfTest {
builder.Warn("64-bit vertex attributes", builder.Warn("64-bit vertex attributes",
"not supported; there is no 64-bit shader input left to feed after the fp64 demotion " "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 " "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 " "anyway. glVertexAttribLFormat succeeds and its state is queryable, but an ENABLED "
"glVertexAttribPointer(GL_FLOAT), which a demoted dvec input reads correctly"); "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; Bool shaderDrawParameters = false;
if (vkGetPhysicalDeviceFeatures2Fn != nullptr && properties.apiVersion >= VK_API_VERSION_1_1) { 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[0]);
HashValue(state, env.maxComputeWorkGroupSize[1]); HashValue(state, env.maxComputeWorkGroupSize[1]);
HashValue(state, env.maxComputeWorkGroupSize[2]); 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.maxComputeWorkGroupInvocations);
HashValue(state, env.backend); HashValue(state, env.backend);
// DynamicBackendParameters is a plain aggregate of scalars; hashing its object // 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 ComputeFrontendCompileEnvFingerprint(const CompileEnv& env) {
Uint64 state = 0xff51afd7ed558ccdull; Uint64 state = 0xff51afd7ed558ccdull;
// The seven limits BuildTBuiltInResource copies into TBuiltInResource. Enumerated // The DynamicBackendParameters limits BuildTBuiltInResource copies into
// ONE BY ONE rather than hashed as a struct, deliberately: hashing all of // TBuiltInResource. Enumerated ONE BY ONE rather than hashed as a struct,
// DynamicBackendParameters would drag ~50 backend-only limits into a key that is // deliberately: hashing all of DynamicBackendParameters would drag ~50 backend-only
// supposed to be backend-agnostic, and every one of them would be a false miss. // limits into a key that is supposed to be backend-agnostic, and every one of them
// Keep this list in step with BuildTBuiltInResource. // would be a false miss. Keep this list in step with BuildTBuiltInResource.
HashValue(state, env.params.MaxImageUnits); HashValue(state, env.params.MaxImageUnits);
HashValue(state, env.params.MaxDrawBuffers); HashValue(state, env.params.MaxDrawBuffers);
HashValue(state, env.params.MaxVertexImageUniforms); HashValue(state, env.params.MaxVertexImageUniforms);
@@ -55,6 +58,22 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
HashValue(state, env.params.MaxFragmentImageUniforms); HashValue(state, env.params.MaxFragmentImageUniforms);
HashValue(state, env.params.MaxComputeImageUniforms); HashValue(state, env.params.MaxComputeImageUniforms);
HashValue(state, env.params.MaxCombinedImageUniforms); 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 two inputs to GetReflectionVertexAttribLimit. Hashed as inputs rather than as
// the resolved limit so this stays in one translation unit; that is coarser (two // 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 // 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; env->advertisedExtensions = activeBackend->GetRendererInfo().RendererGLInfo.Extensions;
} }
// GL_MAX_COMPUTE_WORK_GROUP_SIZE. This is a REAL driver call on DirectGLES; it must // GL_MAX_COMPUTE_WORK_GROUP_SIZE / _COUNT. These are REAL driver calls on DirectGLES; they
// happen here, on the context thread, and exactly once per context. The frontend // must happen here, on the context thread, and exactly once per context. The frontend
// minimum is the floor, matching what GL_Getter reports. // minimum is the floor, matching what GL_Getter reports - both sides now floor at the
// TODO: Share these exposed compute limit helpers with GL_Getter.cpp instead of duplicating the frontend minima. // shared MIN_COMPUTE_WORK_GROUP_* constants rather than at their own copy of them.
constexpr Uint kFrontendMinComputeWorkGroupSizes[3] = {1024, 1024, 64};
for (Uint index = 0; index < 3; ++index) { for (Uint index = 0; index < 3; ++index) {
Int backendValue = 0; Int backendSize = 0;
Int backendCount = 0;
if (MG_Backend::gBackendFunctionsTable.GL.GetIntegeri_v) { if (MG_Backend::gBackendFunctionsTable.GL.GetIntegeri_v) {
MG_Backend::gBackendFunctionsTable.GL.GetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_SIZE, index, 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] = 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; constexpr Uint64 kFrontendMaxComputeWorkGroupInvocations = 1024;
+57 -17
View File
@@ -12,6 +12,18 @@
#include <MG_Backend/BackendObject.h> #include <MG_Backend/BackendObject.h>
namespace MobileGL::MG_Util::ShaderTranspiler { 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 - // 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 // so the transformation is a pure function of its three arguments and can run on a worker
// thread. // thread.
@@ -34,7 +46,13 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
struct CompileEnv { struct CompileEnv {
// --- compute limits: the ONLY former real-driver read in the pipeline --- // --- compute limits: the ONLY former real-driver read in the pipeline ---
// GL_MAX_COMPUTE_WORK_GROUP_SIZE, already max()'d with the frontend minimum. // 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. // GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS, likewise.
Uint64 maxComputeWorkGroupInvocations = 1024; 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 // 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. // 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): // WHAT IS IN IT (audited; re-audit whenever a new env read appears in the front end):
// * the seven DynamicBackendParameters fields BuildTBuiltInResource actually copies // * the DynamicBackendParameters fields BuildTBuiltInResource copies into
// into TBuiltInResource - MaxImageUnits, MaxDrawBuffers, MaxVertexImageUniforms, // TBuiltInResource - MaxImageUnits, MaxDrawBuffers, MaxVertexImageUniforms,
// MaxGeometryImageUniforms, MaxFragmentImageUniforms, MaxComputeImageUniforms, // MaxGeometryImageUniforms, MaxFragmentImageUniforms, MaxComputeImageUniforms,
// MaxCombinedImageUniforms. glslang enforces those at parse, so they decide // MaxCombinedImageUniforms, MaxComputeTextureImageUnits. glslang enforces those at
// whether a shader compiles at all and can change the link result. // 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 // * MaxVertexAttribs and the HasBackend() bit: the two inputs to ProgramLinkTask's
// GetReflectionVertexAttribLimit, which bounds how many vertex input locations // GetReflectionVertexAttribLimit, which bounds how many vertex input locations
// reflection records - so they change the REFLECTION the memo carries. // 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 // The sharding this costs is nil in practice and worth naming so nobody re-litigates
// genuinely reflect the same GLSL differently. // 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: // WHAT IS DELIBERATELY OUT:
// * `backend` beyond the HasBackend() bit. Nothing in the parse, the link or // * `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.) // fp64 GLSL translates identically with the flag on or off.)
// * the other ~50 DynamicBackendParameters fields: read by the GL getters and by // * the other ~50 DynamicBackendParameters fields: read by the GL getters and by
// the backends, never by the parse, the link or reflection. // the backends, never by the parse, the link or reflection.
// * maxComputeWorkGroupSize / maxComputeWorkGroupInvocations. Consumed ONLY by // * maxComputeWorkGroupInvocations - and ONLY this one; its two former companions
// ValidateComputeLocalSizeLimits, a pre-parse ACCEPT/REJECT gate. A rejected // moved into the list above at the wave3 merge. glslang has no
// shader fails its compile, so its program never reaches the tail of the link and // gl_MaxComputeWorkGroupInvocations built-in and BuildTBuiltInResource does not
// no L1 entry is ever created under a rejecting environment; an accepted one // read this field, so its sole consumer is still ValidateComputeLocalSizeLimits, a
// produces the same SPIR-V under any limits, because BuildTBuiltInResource // pre-parse ACCEPT/REJECT gate. A rejected shader fails its compile, so its
// HARDCODES the compute maxima instead of reading these. // program never reaches the tail of the link and no L1 entry is ever created under
// THIS ONE IS A REACHABILITY ARGUMENT, NOT AN INDEPENDENCE ONE. If the TODO in // a rejecting environment; an accepted one parses identically at any value.
// BuildTBuiltInResource ("Drive glslang compute resource limits from the active // THIS ONE IS A REACHABILITY ARGUMENT, NOT AN INDEPENDENCE ONE, and it is now the
// backend") is ever done, these MUST move into this fingerprint. // 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() Uint64 frontendFingerprint = 0; // set by CaptureCompileEnv()
Bool HasBackend() const { return backend != BackendType::Unknown; } Bool HasBackend() const { return backend != BackendType::Unknown; }
@@ -83,23 +83,11 @@ namespace MobileGL {
Resources.minProgramTexelOffset = -8; Resources.minProgramTexelOffset = -8;
Resources.maxProgramTexelOffset = 7; Resources.maxProgramTexelOffset = 7;
Resources.maxClipDistances = 8; Resources.maxClipDistances = 8;
Resources.maxComputeWorkGroupCountX = 65535; Resources.maxComputeUniformComponents = MAX_COMPUTE_UNIFORM_COMPONENTS;
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.maxComputeTextureImageUnits = 16; Resources.maxComputeTextureImageUnits = 16;
Resources.maxComputeImageUniforms = 8; Resources.maxComputeImageUniforms = 8;
Resources.maxComputeAtomicCounters = 8; Resources.maxComputeAtomicCounters = MAX_ATOMIC_COUNTERS_PER_STAGE;
Resources.maxComputeAtomicCounterBuffers = 1; Resources.maxComputeAtomicCounterBuffers = MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE;
Resources.maxVaryingComponents = 60; Resources.maxVaryingComponents = 60;
Resources.maxVertexOutputComponents = 64; Resources.maxVertexOutputComponents = 64;
Resources.maxGeometryInputComponents = 64; Resources.maxGeometryInputComponents = 64;
@@ -137,16 +125,22 @@ namespace MobileGL {
Resources.maxTessControlAtomicCounters = 0; Resources.maxTessControlAtomicCounters = 0;
Resources.maxTessEvaluationAtomicCounters = 0; Resources.maxTessEvaluationAtomicCounters = 0;
Resources.maxGeometryAtomicCounters = 0; Resources.maxGeometryAtomicCounters = 0;
Resources.maxFragmentAtomicCounters = 8; Resources.maxFragmentAtomicCounters = MAX_ATOMIC_COUNTERS_PER_STAGE;
Resources.maxCombinedAtomicCounters = 8; Resources.maxCombinedAtomicCounters = MAX_ATOMIC_COUNTERS_PER_STAGE;
Resources.maxAtomicCounterBindings = 1; // 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.maxVertexAtomicCounterBuffers = 0;
Resources.maxTessControlAtomicCounterBuffers = 0; Resources.maxTessControlAtomicCounterBuffers = 0;
Resources.maxTessEvaluationAtomicCounterBuffers = 0; Resources.maxTessEvaluationAtomicCounterBuffers = 0;
Resources.maxGeometryAtomicCounterBuffers = 0; Resources.maxGeometryAtomicCounterBuffers = 0;
Resources.maxFragmentAtomicCounterBuffers = 1; Resources.maxFragmentAtomicCounterBuffers = MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE;
Resources.maxCombinedAtomicCounterBuffers = 1; Resources.maxCombinedAtomicCounterBuffers = MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE;
Resources.maxAtomicCounterBufferSize = 16384; Resources.maxAtomicCounterBufferSize = MAX_ATOMIC_COUNTER_BUFFER_SIZE;
Resources.maxTransformFeedbackBuffers = 4; Resources.maxTransformFeedbackBuffers = 4;
Resources.maxTransformFeedbackInterleavedComponents = 64; Resources.maxTransformFeedbackInterleavedComponents = 64;
Resources.maxCullDistances = 8; Resources.maxCullDistances = 8;
@@ -165,6 +159,14 @@ namespace MobileGL {
// Resource checking must describe the same backend contract exposed through // Resource checking must describe the same backend contract exposed through
// glGetIntegerv. Keeping this copy local also avoids racing on a process-global // glGetIntegerv. Keeping this copy local also avoids racing on a process-global
// TBuiltInResource when Iris compiles shaders concurrently. // 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 MG_Backend::DynamicBackendParameters fallbackParameters{};
const auto& activeBackend = MG_Backend::pActiveBackendObject; const auto& activeBackend = MG_Backend::pActiveBackendObject;
const auto& dynamicParameters = const auto& dynamicParameters =
@@ -178,6 +180,25 @@ namespace MobileGL {
Resources.maxFragmentImageUniforms = dynamicParameters.MaxFragmentImageUniforms; Resources.maxFragmentImageUniforms = dynamicParameters.MaxFragmentImageUniforms;
Resources.maxComputeImageUniforms = dynamicParameters.MaxComputeImageUniforms; Resources.maxComputeImageUniforms = dynamicParameters.MaxComputeImageUniforms;
Resources.maxCombinedImageUniforms = dynamicParameters.MaxCombinedImageUniforms; 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.nonInductiveForLoops = true;
Resources.limits.whileLoops = true; Resources.limits.whileLoops = true;
@@ -10,6 +10,7 @@
#include <algorithm> #include <algorithm>
#include <cctype> #include <cctype>
#include <cerrno>
#include <climits> #include <climits>
#include <cstdlib> #include <cstdlib>
#include <initializer_list> #include <initializer_list>
@@ -818,6 +819,116 @@ namespace {
ReplaceIdentifier(source, "GL_ARB_gpu_shader_int64", "MG_DISABLED_GL_ARB_gpu_shader_int64"); 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 // 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 // `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 // 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); FilterUnsupportedGpuShaderInt64(env, source);
CoerceUniformBlockPackingToStd140(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); RenameBuiltinShadowingFunctions(source);
@@ -1113,10 +1229,65 @@ namespace MobileGL {
return false; return false;
} }
bool IsDecimalIntegerToken(const String& text) { // One GLSL integer literal, spelled the C way: "0x"/"0X" is hexadecimal, a leading
if (text.empty()) return false; // '0' is OCTAL, everything else decimal, and a single trailing 'u'/'U' is legal.
return std::all_of(text.begin(), text.end(), // strtoll with base 0 already implements exactly that detection, so the only work
[](char ch) { return ch >= '0' && ch <= '9'; }); // 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 // Parses one brace-free depth-0 statement [begin, end) and records its
@@ -1129,6 +1300,7 @@ namespace MobileGL {
MobileGL::UnorderedMap<String, MobileGL::Int>& locations) { MobileGL::UnorderedMap<String, MobileGL::Int>& locations) {
using MobileGL::Int; using MobileGL::Int;
long long location = -1; long long location = -1;
long long literal = 0;
bool sawUniform = false; bool sawUniform = false;
SizeT declaratorBegin = end; SizeT declaratorBegin = end;
@@ -1144,9 +1316,9 @@ namespace MobileGL {
} else if (layoutToken == ")") { } else if (layoutToken == ")") {
--parenDepth; --parenDepth;
} else if (parenDepth == 1 && layoutToken == "location" && j + 2 < end && } else if (parenDepth == 1 && layoutToken == "location" && j + 2 < end &&
tokens[j + 1].text == "=" && IsDecimalIntegerToken(tokens[j + 2].text)) { tokens[j + 1].text == "=" &&
location = std::min(std::strtoll(tokens[j + 2].text.c_str(), nullptr, 10), ParseGlslIntegerLiteral(tokens[j + 2].text, literal)) {
static_cast<long long>(INT_MAX / 2)); location = std::min(literal, static_cast<long long>(INT_MAX / 2));
j += 2; j += 2;
} }
++j; ++j;
@@ -1175,21 +1347,25 @@ namespace MobileGL {
const String& name = tokens[k].text; const String& name = tokens[k].text;
++k; ++k;
long long span = 1; long long span = 1;
Vector<long long> dimensions;
while (k < end && tokens[k].text == "[") { while (k < end && tokens[k].text == "[") {
++k; ++k;
long long dimension = 1; long long dimension = 1;
if (k < end && IsDecimalIntegerToken(tokens[k].text)) { if (k < end && ParseGlslIntegerLiteral(tokens[k].text, literal)) {
dimension = std::strtoll(tokens[k].text.c_str(), nullptr, 10); dimension = literal;
++k; ++k;
} }
if (k >= end || tokens[k].text != "]") return; // sized by expression; bail out if (k >= end || tokens[k].text != "]") return; // sized by expression; bail out
++k; ++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 // Keep the first sighting: a duplicate can only come from alternative
// preprocessor branches declaring the same name. // preprocessor branches declaring the same name.
locations.emplace(name, static_cast<Int>(std::min( locations.emplace(name, static_cast<Int>(std::min(
nextLocation, static_cast<long long>(INT_MAX / 2)))); nextLocation, static_cast<long long>(INT_MAX / 2))));
RecordArrayOfArraysElementLocations(name, dimensions, nextLocation, locations);
nextLocation += span; nextLocation += span;
if (k >= end) break; if (k >= end) break;
if (tokens[k].text == "=") { // skip an initializer up to the declarator comma if (tokens[k].text == "=") { // skip an initializer up to the declarator comma
@@ -1225,6 +1401,7 @@ namespace MobileGL {
MobileGL::UnorderedMap<String, MobileGL::Uint>& bindings) { MobileGL::UnorderedMap<String, MobileGL::Uint>& bindings) {
using MobileGL::Int; using MobileGL::Int;
long long binding = -1; long long binding = -1;
long long literal = 0;
bool sawUniform = false; bool sawUniform = false;
SizeT declaratorBegin = end; SizeT declaratorBegin = end;
@@ -1240,9 +1417,9 @@ namespace MobileGL {
} else if (layoutToken == ")") { } else if (layoutToken == ")") {
--parenDepth; --parenDepth;
} else if (parenDepth == 1 && layoutToken == "binding" && j + 2 < end && } else if (parenDepth == 1 && layoutToken == "binding" && j + 2 < end &&
tokens[j + 1].text == "=" && IsDecimalIntegerToken(tokens[j + 2].text)) { tokens[j + 1].text == "=" &&
binding = std::min(std::strtoll(tokens[j + 2].text.c_str(), nullptr, 10), ParseGlslIntegerLiteral(tokens[j + 2].text, literal)) {
static_cast<long long>(INT_MAX / 2)); binding = std::min(literal, static_cast<long long>(INT_MAX / 2));
j += 2; j += 2;
} }
++j; ++j;
@@ -1275,7 +1452,7 @@ namespace MobileGL {
++k; ++k;
while (k < end && tokens[k].text == "[") { while (k < end && tokens[k].text == "[") {
++k; ++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 if (k >= end || tokens[k].text != "]") return; // sized by expression; bail out
++k; ++k;
} }
@@ -1332,6 +1509,100 @@ namespace MobileGL {
return bindings; 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> ExtractExplicitUniformLocations(const String& source) {
UnorderedMap<String, Int> locations; UnorderedMap<String, Int> locations;
// Fast path: without the qualifier keyword there is nothing to extract. // 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 // mapIO can capture them, so they are recovered lexically (same narrow
// grammar discipline as ExtractExplicitUniformLocations). // grammar discipline as ExtractExplicitUniformLocations).
UnorderedMap<String, Uint> ExtractExplicitOpaqueBindings(const String& source); 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 ShaderTranspiler
} // namespace MG_Util } // namespace MG_Util
} // namespace MobileGL } // namespace MobileGL
@@ -326,6 +326,55 @@ namespace MobileGL {
SPVC_CHK_RETURN 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) { spvc_result SpvcSession::Compile(const char** result) {
if (!(usage & SessionUsageBit::Transpile)) return SPVC_ERROR_INVALID_ARGUMENT; if (!(usage & SessionUsageBit::Transpile)) return SPVC_ERROR_INVALID_ARGUMENT;
SPVC_CHK_INIT SPVC_CHK_INIT
@@ -105,6 +105,21 @@ namespace MobileGL {
// arrayed block's elements are separate GL resources spelled "B[0]", "B[1]"). // arrayed block's elements are separate GL resources spelled "B[0]", "B[1]").
// Entries with a negative value mean "never rebound" and are skipped. // Entries with a negative value mean "never rebound" and are skipped.
spvc_result SetShaderStorageBlockBinding(const UnorderedMap<String, Int>& bindings); 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); spvc_result Compile(const char** result);
const SpvcMetadata& GetMetadata() const; const SpvcMetadata& GetMetadata() const;
const char* GetLastErrorString() const; const char* GetLastErrorString() const;
@@ -119,6 +119,7 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
builder.Value(inputs.maxDepthTextureSamples); builder.Value(inputs.maxDepthTextureSamples);
builder.Value(inputs.advertisedMaxSamples); builder.Value(inputs.advertisedMaxSamples);
builder.Value(static_cast<Uint32>(inputs.esslVersion)); builder.Value(static_cast<Uint32>(inputs.esslVersion));
builder.Value(inputs.atomicCounterEsslBindingTop);
builder.Value(static_cast<Uint8>(inputs.enableSpirvValidation)); builder.Value(static_cast<Uint8>(inputs.enableSpirvValidation));
static const std::set<String> kEmptySet; static const std::set<String> kEmptySet;
builder.NameSet(inputs.xfbCaptureBlockNames ? *inputs.xfbCaptureBlockNames : kEmptySet); builder.NameSet(inputs.xfbCaptureBlockNames ? *inputs.xfbCaptureBlockNames : kEmptySet);
@@ -135,6 +136,7 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
SizeT EsslTranslationResultBytes(const EsslTranslationResult& result) { SizeT EsslTranslationResultBytes(const EsslTranslationResult& result) {
SizeT bytes = result.essl.size(); SizeT bytes = result.essl.size();
for (const String& name : result.flattenedXfbBlockNames) bytes += name.size(); for (const String& name : result.flattenedXfbBlockNames) bytes += name.size();
bytes += result.atomicCounterGlBindings.size() * sizeof(Int);
return bytes; return bytes;
} }
@@ -428,6 +428,8 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
// derived from LIVE glBindImageTexture state and is the one genuinely // derived from LIVE glBindImageTexture state and is the one genuinely
// per-draw-state input in here; // per-draw-state input in here;
// * the storage-block binding overrides handed to SPIRV-Cross; // * 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 // * the ESSL version SPIRV-Cross targets (ResolveBackendEsslVersion, i.e. the
// driver's GLES version) - the remaining two SPIRV-Cross options are // driver's GLES version) - the remaining two SPIRV-Cross options are
// compile-time constants (GLSL_ES true, VULKAN_SEMANTICS false); // 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 // feedback capture list follows them, so a payload that dropped them would
// silently un-rename every capture on a cache hit. // silently un-rename every capture on a cache hit.
std::set<String> flattenedXfbBlockNames; 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>; using EsslTranslationResultPtr = SharedPtr<const EsslTranslationResult>;
@@ -462,6 +471,13 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
const UnorderedMap<String, Uint>* glFormatByUniformName = nullptr; const UnorderedMap<String, Uint>* glFormatByUniformName = nullptr;
const UnorderedMap<String, Int>* storageBlockBindingOverrides = 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 --- // --- SPIRV-Cross options ---
Uint esslVersion = 300; Uint esslVersion = 300;
+29
View File
@@ -14,6 +14,35 @@ namespace MobileGL {
namespace MG_Util { namespace MG_Util {
namespace ShaderTranspiler { namespace ShaderTranspiler {
inline const char* GLOBAL_UBO_NAME = "MGL_GLOBAL_UBO"; 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 {}; struct EmptyType {};
@@ -138,6 +138,11 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
case TextureInternalFormat::DepthComponent32F: case TextureInternalFormat::DepthComponent32F:
out = {1, ShadowComponent::Float32, false}; out = {1, ShadowComponent::Float32, false};
return true; 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::R8:
case TextureInternalFormat::Red: out = {1, ShadowComponent::UNorm8, false}; return true; 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; case TextureInputFormat::BGRAInteger: out = {{2, 1, 0, 3}, 4, true}; return true;
// A depth value converts like a single normalized/float channel. // A depth value converts like a single normalized/float channel.
case TextureInputFormat::DepthComponent: out = {{0, -1, -1, -1}, 1, false}; return true; 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: 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); 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 // assume 8 bit per channel
// swizzle.size() == channel count // swizzle.size() == channel count
void ProcessColorSwizzle(void* data, SizeT pixelCount, const Vector<TextureSwizzleParam>& swizzle) { void ProcessColorSwizzle(void* data, SizeT pixelCount, const Vector<TextureSwizzleParam>& swizzle) {
@@ -990,6 +1008,11 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
const void* inputPixel, const void* inputPixel,
Vector<Uint8>& outputPixel) { Vector<Uint8>& outputPixel) {
outputPixel.clear(); 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; if (inputPixel == nullptr || !IsValidUnpackPixelPair(textureInputFormat, inputDataType)) return false;
PixelStoreParameters params{}; PixelStoreParameters params{};
@@ -44,6 +44,17 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
Bool IsRawPackedPixelTransfer(TextureInternalFormat internalFormat, TextureInputFormat clientFormat, Bool IsRawPackedPixelTransfer(TextureInternalFormat internalFormat, TextureInputFormat clientFormat,
TexturePixelDataType clientType); 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 // 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 // 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 // 4 Floats per texel; integer formats fill it with 4 Uint32/Int32 per texel and set