diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 7a8155f9..4b82ca40 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -395,6 +395,50 @@ namespace MobileGL::MG_Backend::DirectGLES { } } + void SyncAtomicCounterBuffers(const Vector& 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(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(glBinding)); + auto& obj = point.GetBoundObject(); + if (!obj) { + BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, static_cast(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(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(esslBinding), + backendResource->id, static_cast(start), + static_cast(end - start)); + } + // The whole point of a counter is that the shader INCREMENTS it, and every + // conformance case reads the result back with glMapBufferRange or + // glGetBufferSubData - which serve the frontend's CPU shadow until the buffer is + // flagged (BufferObject::SyncGpuWrites), exactly as for a storage buffer. + obj->MarkGpuWritten(); + } + } + void SyncBoundBuffer(BufferTarget target, GLenum glTarget) { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); @@ -1394,12 +1438,22 @@ namespace MobileGL::MG_Backend::DirectGLES { } } + // Highest image unit that has ever been given a texture, plus one. Maintained by the + // single funnel below, so it is a sound "no draw in this context can be reading an image" + // test: nothing reaches an image unit without going through SyncImageTextureBinding. + // Almost every program (every Minecraft draw) leaves it at zero, which is what keeps the + // draw-path staleness check below at one integer test. + static Uint g_imageUnitHighWaterMark = 0; + void SyncImageTextureBinding(Uint unit) { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast(unit)); TrackWritableImageBufferUnit(unit, IsWritableImageBufferTexture(imageBinding)); + if (imageBinding.Texture && unit + 1 > g_imageUnitHighWaterMark) { + g_imageUnitHighWaterMark = unit + 1; + } if (!imageBinding.Texture) { g_GLESFuncs.glBindImageTexture(unit, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8); return; @@ -1453,6 +1507,40 @@ namespace MobileGL::MG_Backend::DirectGLES { SyncImageTextureBinding(unit); } } + + // What the draw path last swept the image units against. A draw never swept them at all: + // an image unit was established once, eagerly, by glBindImageTexture and never revisited. + // That is stale the moment the texture behind it is re-specified with a new size or + // format, because ES 3.1 only allows IMMUTABLE storage on an image unit + // (SyncTextureObjectToBackend's imageBindableStorageRequired), immutable storage cannot be + // redefined, and so the re-spec MINTS A NEW ES TEXTURE NAME - leaving the unit pointing at + // the deleted one and imageSize() reporting the old dimensions + // (KHR-GL43.shader_image_size.advanced-changeSize). + static Uint64 g_imageSweepContextId = 0; + static Uint64 g_imageSweepSamplingGeneration = 0; + static Uint g_imageSweepBackendContextGeneration = 0; + static Bool g_imageSweepValid = false; + + // The sweep is a glBindImageTexture per unit, so it must not run per draw: the gate is the + // frontend's sampling-resolution generation, which TextureObjectBase::BumpShapeVersion + // moves on exactly the shape and format changes that can force the re-mint. Deliberately + // NOT the backend-side re-mint counter (g_attachmentBackendIdGeneration's sibling would be + // the obvious choice): a texture that is bound ONLY to an image unit is re-minted inside + // this very sweep, so a backend-side trigger would be bumped after the gate had already + // declined to run it. + void SyncImageTextureBindingsForDraw(const DrawTextureSyncKeys& keys) { + if (g_imageUnitHighWaterMark == 0) return; + if (g_imageSweepValid && g_imageSweepContextId == keys.contextId && + g_imageSweepSamplingGeneration == keys.samplingGeneration && + g_imageSweepBackendContextGeneration == g_backendContextGeneration) { + return; + } + SyncImageTextureBindings(); + g_imageSweepContextId = keys.contextId; + g_imageSweepSamplingGeneration = keys.samplingGeneration; + g_imageSweepBackendContextGeneration = g_backendContextGeneration; + g_imageSweepValid = true; + } } // namespace TextureImpl namespace FramebufferImpl { @@ -2396,6 +2484,11 @@ namespace MobileGL::MG_Backend::DirectGLES { syncBit & DrawSyncBit::IndirectBuffer); VertexArrayImpl::SyncCurrentVAO(currentVAO, vaoTwin); TextureImpl::SyncNeccessaryTextures(textureKeys); + // A draw reads and writes through its image units too, so the unit bindings have to be + // as current as the sampled ones. Gated (see the sweep): a program with no image binding + // pays one integer test, and one with images re-issues them only when a texture shape + // moved under them. + TextureImpl::SyncImageTextureBindingsForDraw(textureKeys); // A draw writes through its image units too - the conformance case that found this // stores into a buffer texture from the FRAGMENT stage, not from a dispatch. TextureImpl::MarkWritableImageBufferTexturesGpuWritten(); @@ -2915,6 +3008,16 @@ namespace MobileGL::MG_Backend::DirectGLES { } } + // Atomic counter buffers. Bound here rather than beside the storage-buffer sync + // in SyncNeccessaryBuffers because the reserved slot the transpiled ESSL reads + // them at is PROGRAM state: it is `top - GL binding` for the counter blocks THIS + // program declares, and no other program's blocks live there. Both the draw and + // the dispatch path reach this, which is what a compute-shader counter needs. + if (!backendProgram.GetAtomicCounterBindings().empty()) { + BufferImpl::SyncAtomicCounterBuffers(backendProgram.GetAtomicCounterBindings(), + backendProgram.GetAtomicCounterEsslBindingTop()); + } + { #ifdef TRACY_ENABLE ZoneScopedNC("BindSamplerUnit", TRACY_ZONECOLOR_BACKEND); @@ -5683,15 +5786,27 @@ namespace MobileGL::MG_Backend::DirectGLES { g_GLESFuncs.glDispatchComputeIndirect(indirect); } + // An atomic counter is a shader storage block by the time it reaches the ES driver (glslang + // lowers every atomic_uint onto one), so an application that asks only for the counter + // barrier is asking about memory the driver knows as storage-buffer memory. Ordering one + // does not oblige a driver to order the other, so the counter bit implies the storage bit + // here - which is what the lowering costs and the only place it can be paid. + static GLbitfield LowerAtomicCounterBarrierBits(GLbitfield barriers) { + if ((barriers & GL_ATOMIC_COUNTER_BARRIER_BIT) != 0) { + barriers |= GL_SHADER_STORAGE_BARRIER_BIT; + } + return barriers; + } + void MemoryBarrier(GLbitfield barriers) { - g_GLESFuncs.glMemoryBarrier(barriers); + g_GLESFuncs.glMemoryBarrier(LowerAtomicCounterBarrierBits(barriers)); if (g_GLESCapabilities.IsAngleRenderer) { g_GLESFuncs.glFlush(); } } void MemoryBarrierByRegion(GLbitfield barriers) { - g_GLESFuncs.glMemoryBarrierByRegion(barriers); + g_GLESFuncs.glMemoryBarrierByRegion(LowerAtomicCounterBarrierBits(barriers)); } // One endpoint of a glCopyImageSubData, expressed the way the ES driver stores it. @@ -7842,9 +7957,14 @@ namespace MobileGL::MG_Backend::DirectGLES { tempFB, GL_READ_FRAMEBUFFER, backendTexId, backendAttachTarget == GL_UNKNOWN_MGL ? target : backendAttachTarget, level, /*withStencil=*/format == GL_DEPTH_STENCIL); - } else if (backendAttachTarget == GL_TEXTURE_3D || backendAttachTarget == GL_TEXTURE_2D_ARRAY) { - // ES cannot attach 3D/array textures through glFramebufferTexture2D; read layer 0. Reads - // of deeper slices are served from the CPU shadow instead (see the shadow-first branch). + } else if (backendAttachTarget == GL_TEXTURE_3D || backendAttachTarget == GL_TEXTURE_2D_ARRAY || + backendAttachTarget == GL_TEXTURE_CUBE_MAP_ARRAY) { + // ES cannot attach 3D/array textures through glFramebufferTexture2D; layer 0 here, and + // the deeper slices one at a time in the per-layer loop below. A CUBE MAP ARRAY is in + // this list for the same reason its layer-faces are addressed as array layers: + // glFramebufferTexture2D has no target token for it, so the 2D attach it used to take + // left the scratch FBO incomplete and every read fell through to the (stale) CPU + // shadow - which is exactly the all-zero result the conformance suite saw. ScratchFBOImpl::EnsureColorAttachmentLayer(tempFB, GL_READ_FRAMEBUFFER, backendTexId, level, 0); } else { ScratchFBOImpl::EnsureColorAttachment2D( @@ -7897,6 +8017,18 @@ namespace MobileGL::MG_Backend::DirectGLES { auto size = textureMipmapObject->GetMipmapTexelSize(MG_Util::ConvertGLEnumToTextureUploadTarget(target), level); + // GL_TEXTURE_1D_ARRAY keeps its LAYERS in the state-side height (that is what + // glTexImage2D(GL_TEXTURE_1D_ARRAY, w, layers) means), while the ES texture behind it is a + // 2D array of height 1 with the layers in depth - GetBackendUploadSize performs exactly + // that swap on the way in. Everything below addresses the ES image, so the same swap has + // to happen here: without it the readback asked layer 0 for a `layers`-row rectangle it + // does not have, and every layer but the first came back undefined (all zeroes on Adreno, + // KHR-GL4x.shader_image_load_store.basic-allTargets-*). + const Bool oneDimensionalArray = textureObject->GetTarget() == TextureTarget::Texture1DArray; + if (oneDimensionalArray) { + size = TextureImpl::GetBackendUploadSize(TextureTarget::Texture1DArray, size); + } + MGLOG_D("GetTexImage: mip level %d size = %dx%d", level, size.x(), size.y()); // Prefer the client-format conversion for every convertible combination: the "native" ES pairs @@ -7910,10 +8042,14 @@ namespace MobileGL::MG_Backend::DirectGLES { TextureImpl::BackendTextureFormatAddsAlpha(textureObject->GetFormat(), textureObject->GetTarget()); // GL_PACK_IMAGE_HEIGHT/GL_PACK_SKIP_IMAGES only apply to 3D/array image // readbacks (cube-map arrays address as arrays); 2D targets must ignore - // them (GL 3.3 section 6.1.4). - const Bool applyPackImageParams = backendAttachTarget == GL_TEXTURE_3D || - backendAttachTarget == GL_TEXTURE_2D_ARRAY || - backendAttachTarget == GL_TEXTURE_CUBE_MAP_ARRAY; + // them (GL 3.3 section 6.1.4). A 1D ARRAY is one of those 2D targets: GL hands it back + // as a single two-dimensional image whose ROWS are the layers, so the layer stride is + // one packed row and the image parameters do not enter into it - even though the ES + // texture underneath is an array and is read one layer at a time. + const Bool applyPackImageParams = !oneDimensionalArray && + (backendAttachTarget == GL_TEXTURE_3D || + backendAttachTarget == GL_TEXTURE_2D_ARRAY || + backendAttachTarget == GL_TEXTURE_CUBE_MAP_ARRAY); const GLsizei sliceCount = std::max(size.z(), 1); const Bool multiSlice = size.z() > 1; // glGetTexImage answers with the STORED texels, and for a packed format whose encoding @@ -7947,7 +8083,8 @@ namespace MobileGL::MG_Backend::DirectGLES { // Attach the layers one at a time instead and read each off the GPU, keeping the shadow // for the formats the FBO cannot represent at all. if (multiSlice && tempFBOComplete && - (backendAttachTarget == GL_TEXTURE_3D || backendAttachTarget == GL_TEXTURE_2D_ARRAY)) { + (backendAttachTarget == GL_TEXTURE_3D || backendAttachTarget == GL_TEXTURE_2D_ARRAY || + backendAttachTarget == GL_TEXTURE_CUBE_MAP_ARRAY)) { // Each slice is packed as its own 2D image, so the per-slice call must not apply // GL_PACK_SKIP_IMAGES / GL_PACK_IMAGE_HEIGHT itself - this walks the destination // over them, using the same layout StoreWideRowsToClient computes. diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index 444aadcd..a71dcc35 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -45,6 +45,16 @@ namespace MobileGL::MG_Backend::DirectGLES { constexpr const char* INDIRECT_PARAMS_BLOCK_NAME = "mg_IndirectParams"; constexpr const char* ZERO_BASED_INSTANCE_ID_NAME = "mg_ZeroBasedInstanceID"; + // ES has no atomic-counter buffers: glslang lowers every atomic_uint onto a synthesized + // storage block, so one GL counter BUFFER costs one of the driver's shader-storage binding + // points. Those slots are taken from the TOP of the range downwards - below the one + // mg_IndirectParams already reserves - so an application binding its own SSBOs from 0 upwards + // never meets them, and the slot for GL binding N is `this - N` in every stage of the + // program without any shared state. Negative when the driver has no room left at all. + static Int AtomicCounterEsslBindingTop() { + return g_GLESCapabilities.MaxShaderStorageBufferBindings - 2; + } + static Bool IsAngleLlvmpipeRenderer() { return g_GLESCapabilities.IsAngleLlvmpipeRenderer; } @@ -1758,14 +1768,16 @@ namespace MobileGL::MG_Backend::DirectGLES { m_syncedAttributeVersions[attribIndex].BufferVersion; if (!needsSyncFormat && !needsSyncBuffer && !needsSyncBaseInstance) continue; - // Defence in depth. The frontend already declines glVertexAttribLFormat on this - // backend (SupportsFloat64VertexAttributes is false - ES has no GL_DOUBLE vertex - // format and ESSL has no fp64 type), so IsLong should never arrive here; if it ever - // did, passing GL_DOUBLE to glVertexAttribPointer would only raise GL_INVALID_ENUM on - // the real driver. Disabling rather than merely skipping matters: becoming long bumps - // FormatVersion, not SwitchVersion, so the enable/disable block above will not run - // again and an already-enabled array would stay enabled with no pointer and no - // ARRAY_BUFFER binding - which ES 3.1+ makes an INVALID_OPERATION at draw. + // This is where a 64-bit array actually stops. glVertexAttribLFormat is a legal call + // in a GL 4.3 context and the frontend RECORDS its format (the state queries have to + // answer), so IsLong does arrive here - what this backend cannot do is FEED it: + // SupportsFloat64VertexAttributes is false because ES has no GL_DOUBLE vertex format + // and ESSL has no fp64 type, and passing GL_DOUBLE to glVertexAttribPointer would + // only raise GL_INVALID_ENUM on the real driver. Disabling rather than merely + // skipping matters: becoming long bumps FormatVersion, not SwitchVersion, so the + // enable/disable block above will not run again and an already-enabled array would + // stay enabled with no pointer and no ARRAY_BUFFER binding - which ES 3.1+ makes an + // INVALID_OPERATION at draw. // // IsLong is not the only way a 64-bit array gets here: glVertexAttribFormat // with GL_DOUBLE asks for doubles in memory CONVERTED to float, so it is not @@ -4745,6 +4757,22 @@ namespace MobileGL::MG_Backend::DirectGLES { ImageFormatBakeInputs CollectImageFormatBakeInputs( const MG_State::GLState::ProgramObject& stateProgramObject) { ImageFormatBakeInputs inputs; + // A format GLSL ES cannot spell on a driver with no GL_NV_image_formats to spell it + // with. There is no legal ESSL for such a shader at all, so the stage will not + // compile and the program is lost - a failure that used to leave nothing behind but + // a draw that rendered nothing. Recorded and reported ONCE per program build rather + // than per uniform: an image array reaches this decision once per element. + String unspellableUniform; + String unspellableFormat; + Uint unspellableCount = 0; + const auto recordUnspellableFormat = [&](const String& uniformName, String formatSpelling) { + if (unspellableCount == 0) { + unspellableUniform = uniformName; + unspellableFormat = Move(formatSpelling); + } + ++unspellableCount; + }; + const Uint maxUniformLoc = stateProgramObject.GetMaxUniformLocation(); for (Uint loc = 0; loc <= maxUniformLoc; ++loc) { const auto& name = stateProgramObject.GetUniformName(loc); @@ -4757,6 +4785,10 @@ namespace MobileGL::MG_Backend::DirectGLES { // still needs the extension directive to survive the ES compiler. if (!IsCoreEsslLayoutFormat(type->getQualifier().getFormat())) { inputs.needsExtendedImageFormats = true; + if (!g_GLESCapabilities.SupportsExtendedImageFormats) { + recordUnspellableFormat(name, glslang::TQualifier::getLayoutFormatString( + type->getQualifier().getFormat())); + } } continue; } @@ -4782,6 +4814,8 @@ namespace MobileGL::MG_Backend::DirectGLES { MGLOG_D("Image uniform '%s' has no declared format and its unit %d holds 0x%x, which GLSL ES " "core cannot spell and this driver has no GL_NV_image_formats for.", name.c_str(), unit, boundFormat); + recordUnspellableFormat( + name, MG_Util::ShaderTranspiler::ShaderCompiler::EsslImageFormatSpelling(boundFormat)); continue; } inputs.needsExtendedImageFormats = true; @@ -4824,6 +4858,18 @@ namespace MobileGL::MG_Backend::DirectGLES { for (const auto& name : textCompleted) { inputs.glFormatByUniformName.erase(name); } + // Unlatched MGLOG_E, like the transpile- and link-failure diagnostics in SyncToBackend: + // one line per failing program build, and naming the uniform and the format is the + // whole diagnostic value. Left as a log rather than a link failure on purpose - the + // frontend has already reported LINK_STATUS = true and GL cannot retract it, and the + // program stays queryable exactly as the "linked but not drawable" exit leaves it. + if (unspellableCount != 0) { + MGLOG_E("Image format '%s' on uniform '%s' has no GLSL ES spelling and this driver does not expose " + "GL_NV_image_formats%s; the stage using it cannot compile and the program will draw " + "nothing.", + unspellableFormat.empty() ? "(none)" : unspellableFormat.c_str(), unspellableUniform.c_str(), + unspellableCount > 1 ? " (and it is not the only image uniform affected)" : ""); + } return inputs; } @@ -4881,6 +4927,11 @@ namespace MobileGL::MG_Backend::DirectGLES { // this build current - the draw path compares the signature and rebuilds on a change. const auto& storageBlockBindingOverrides = stateProgramObject->GetShaderStorageBlockBindingOverrides(); m_shaderStorageBlockBindingSignature = ComputeShaderStorageBlockBindingSignature(*stateProgramObject); + // Rebuilt by the transpile loop below, one entry per atomic-counter block it finds. + // The top is snapshotted here so every stage of this program - and the draw path + // reading it afterwards - resolves the same slot for the same GL binding. + m_atomicCounterGlBindings.clear(); + m_atomicCounterEsslBindingTop = AtomicCounterEsslBindingTop(); // The same shape again for image FORMATS: what a format-less image declaration // compiles to depends on live glBindImageTexture state, so the pairs it was built // against are recorded here and compared per draw (ImageUnitFormatsStillMatch). @@ -5206,6 +5257,16 @@ namespace MobileGL::MG_Backend::DirectGLES { spvcSession.SetShaderStorageBlockBinding(storageBlockBindingOverrides); } + // Atomic counters, same mechanism for the same reason. glslang already turned + // every atomic_uint into a member of gl_AtomicCounterBlock_ 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. + spvcSession.SetAtomicCounterBlockBindings(m_atomicCounterEsslBindingTop, + m_atomicCounterGlBindings); + const char* result = nullptr; spvcSession.Compile(&result); @@ -5353,6 +5414,16 @@ namespace MobileGL::MG_Backend::DirectGLES { MGLOG_D("Processed shader source length: %zu", source.length()); } + // A counter buffer declared by several stages was recorded once per stage; the draw + // path binds per GL binding point, so collapse the duplicates here rather than + // re-issuing the same glBindBufferBase two or three times every draw. + if (!m_atomicCounterGlBindings.empty()) { + std::sort(m_atomicCounterGlBindings.begin(), m_atomicCounterGlBindings.end()); + m_atomicCounterGlBindings.erase( + std::unique(m_atomicCounterGlBindings.begin(), m_atomicCounterGlBindings.end()), + m_atomicCounterGlBindings.end()); + } + // Transform feedback capture runs on the real driver (see XfbImpl in // DirectGLES.cpp), so the capture set has to be declared on the backend // program before it links. SPIRV-Cross keeps user output names verbatim in diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index 56ec8aad..0f9b92b5 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -396,6 +396,13 @@ namespace MobileGL::MG_Backend::DirectGLES { void BindBufferBaseCached(GLenum glTarget, Uint index, Uint id); void BindBufferRangeCached(GLenum glTarget, Uint index, Uint id, GLintptr offset, GLsizeiptr size); void InvalidateIndexedBufferBindingCache(); + // Re-issues the GL_ATOMIC_COUNTER_BUFFER binding points a program's shaders declare as + // GL_SHADER_STORAGE_BUFFER bindings at the reserved slots the transpiled ESSL was built + // against (BackendProgramObjectImpl::GetAtomicCounterBindings / + // GetAtomicCounterEsslBindingTop). ES has no counter-buffer target at all, so without + // this the shader reads a storage block nobody ever bound a buffer to and the buffer the + // application bound never reaches the driver. + void SyncAtomicCounterBuffers(const Vector& glBindings, Int esslBindingTop); // Buffer-storage pool maintenance. TrimBufferPool evicts over-budget entries // (called once per frame from Present); ClearBufferPool drops all pooled ids // without glDeleteBuffers (called when the ES context is going away). @@ -1171,6 +1178,13 @@ namespace MobileGL::MG_Backend::DirectGLES { // qualifier, so the overrides are baked into the source). A mismatch means the // program is stale exactly like the clamp masks above. Uint64 GetShaderStorageBlockBindingSignature() const { return m_shaderStorageBlockBindingSignature; } + // GL atomic-counter binding points the transpiled stages declare (sorted, unique), + // and the top of the reserved shader-storage range their counter blocks were + // transpiled against - the slot for GL binding N is `top - N`. Empty for every + // program that uses no atomic counter, which is what keeps the per-draw cost of the + // counter sync at one empty-vector test. + const Vector& GetAtomicCounterBindings() const { return m_atomicCounterGlBindings; } + Int GetAtomicCounterEsslBindingTop() const { return m_atomicCounterEsslBindingTop; } Bool HasGlobalUboBlock() const { return m_globalUboBackendBlockIndex >= 0; } const Vector& GetUniformBlockBackendIndices() const { return m_uniformBlockBackendIndices; } @@ -1237,6 +1251,8 @@ namespace MobileGL::MG_Backend::DirectGLES { Uint m_fragColorBroadcastCount = 1; // 0 is the signature of an empty override set, i.e. what almost every program has. Uint64 m_shaderStorageBlockBindingSignature = 0; + Vector m_atomicCounterGlBindings; + Int m_atomicCounterEsslBindingTop = -1; Bool m_isInitialized = false; Bool m_backendProgramUsable = false; diff --git a/MobileGL/MG_Backend/DirectGLES/Utils.cpp b/MobileGL/MG_Backend/DirectGLES/Utils.cpp index c74014ee..3abb7640 100644 --- a/MobileGL/MG_Backend/DirectGLES/Utils.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Utils.cpp @@ -822,9 +822,19 @@ namespace MobileGL::MG_Backend::DirectGLES { // A rebuilt declaration. Keeps SPIRV-Cross's own word order (`uniform readonly // highp image2D`) so the image-rebinding regex in Managers.cpp still matches what // comes out of here, whichever order the two passes end up running in. + // + // `forceCoherent` is for the SPLIT pair only. GLSL guarantees that a write through + // one image variable is visible to a read through a DIFFERENT one only when both are + // declared coherent, and the split turns a same-variable read-after-write - which + // desktop GLSL orders by construction, so the source almost never says `coherent` - + // into exactly that cross-variable shape. Without it the driver may serve the load + // from a cache that never saw the store through the writeonly half. String BuildImageDeclaration(const ImageUniformDecl& decl, const char* memoryQualifier, - const String& variableName) { + const String& variableName, Bool forceCoherent = false) { String out = "layout(" + decl.layout + ") uniform "; + if (forceCoherent && !ContainsIdentifier(decl.qualifiers, "coherent")) { + out += "coherent "; + } out += memoryQualifier; out += ' '; if (!decl.qualifiers.empty()) { @@ -1008,9 +1018,15 @@ namespace MobileGL::MG_Backend::DirectGLES { decl.writeName = MakeImageWriteAliasName(decl.name, glslCode, takenAliases); takenAliases.push_back(decl.writeName); decl.split = true; + // Both halves carry `coherent`; see BuildImageDeclaration. The + // single-declaration cases below stay as they were - nothing aliases them, so + // there is no visibility to restore and no reason to pay for the cache + // behaviour. edits.push_back({decl.declStart, decl.declLength, - BuildImageDeclaration(decl, "readonly", decl.name) + "\n" + - BuildImageDeclaration(decl, "writeonly", decl.writeName)}); + BuildImageDeclaration(decl, "readonly", decl.name, /*forceCoherent=*/true) + + "\n" + + BuildImageDeclaration(decl, "writeonly", decl.writeName, + /*forceCoherent=*/true)}); } else if (decl.stored) { edits.push_back({decl.declStart, decl.declLength, BuildImageDeclaration(decl, "writeonly", decl.name)}); diff --git a/MobileGL/MG_Backend/DirectGLES/Utils.h b/MobileGL/MG_Backend/DirectGLES/Utils.h index 33f04bd5..063651d2 100644 --- a/MobileGL/MG_Backend/DirectGLES/Utils.h +++ b/MobileGL/MG_Backend/DirectGLES/Utils.h @@ -196,11 +196,18 @@ namespace MobileGL::MG_Backend::DirectGLES { // * loaded only -> add `readonly` // * stored only -> add `writeonly` // * both -> emit TWO declarations on the same binding and of the - // same type, `readonly ` and `writeonly - // `, and point every - // imageStore at the second one. Several image variables - // may share an image unit as long as they have the same - // type and format, which is exactly what the pair is. + // same type, `coherent readonly ` and `coherent + // writeonly `, and point + // every imageStore at the second one. Several image + // variables may share an image unit as long as they have + // the same type and format, which is exactly what the pair + // is. + // + // The `coherent` on both halves of the pair is load-bearing, not decoration: GLSL only + // guarantees a write through one image variable is visible to a read through a DIFFERENT + // one when both are coherent, and the split is what makes a same-variable + // read-after-write cross-variable. The single-declaration repairs above do not get it - + // nothing aliases them. // // Budget note: the split DOUBLES the image-uniform count of the stage it fires in, so // a driver advertising a tight GL_MAX_{FRAGMENT,VERTEX,...}_IMAGE_UNIFORMS can turn a diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.cpp index caf17a7a..c69a994a 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.cpp @@ -8,6 +8,7 @@ #include "VertexInputStateFactory.h" #include "MG_Util/Converters/MGToStr/DataTypeConverter.h" +#include #include namespace MobileGL::MG_Backend::DirectVulkan { @@ -330,6 +331,20 @@ namespace MobileGL::MG_Backend::DirectVulkan { // for every R64 float format, so a native 64-bit vertex fetch is simply unavailable there // while shaderFloat64 is not. Both halves key off nothing but the attribute being long, // so they always agree without extra plumbing. + // + // ... as long as the shader half still runs. It does not when the backend has declared + // no 64-bit vertex attribute support: DemoteFloat64Pass has already narrowed every + // `dvec` input to a `vec` by then, so PackDoubleVertexInputsPass finds nothing to pack + // and a UINT-formatted attribute would be fed to a float input - garbage with no + // diagnostic anywhere. Declining here drops the array instead (the caller skips + // UNDEFINED attributes and reports them through unsupportedAttribMask), which is what + // DirectGLES does for the same state. The frontend RECORDS the format either way, so + // this gate is the only thing standing between a legal glVertexAttribLFormat and a + // mismatched pipeline. + if (MG_Backend::pActiveBackendObject == nullptr || + !MG_Backend::pActiveBackendObject->GetDynamicParameters().SupportsFloat64VertexAttributes) { + return VK_FORMAT_UNDEFINED; + } if (!isLong || isInteger || normalized) return VK_FORMAT_UNDEFINED; switch (size) { case 1: return VK_FORMAT_R32G32_UINT; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp index 4c5f7ac7..ce259330 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp @@ -1494,6 +1494,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { const auto* mipTexture = MG_State::GLState::AsMipmapTexture(&texture); const Uint32 mipLevelCount = mipTexture != nullptr ? mipTexture->GetMipmapLevelCount() : 0u; return resource.syncedContentVersion != texture.GetContentVersion() || + resource.syncedShapeVersion != texture.GetShapeVersion() || resource.syncedTextureParamsVersion != texture.GetTextureParamsVersion() || resource.syncedMipLevelCount != mipLevelCount; } @@ -1593,11 +1594,16 @@ namespace MobileGL::MG_Backend::DirectVulkan { Bool VkTextureManager::SyncTexture(MG_State::GLState::ITextureObject &texture, TextureResource &outResource) { // Cross-draw fast path: if the resource is already built and neither the texture's - // pixel content (bumped in MarkStorageDirty) nor its params changed since the last - // sync, there is nothing to re-check or re-upload - skip CheckMipmapCompleteness, - // SyncTextureResource, SyncTextureViews and the per-level dirty scan. Layout is - // maintained separately by the transition path, so the resource still reflects truth. + // pixel content (bumped in MarkStorageDirty), its SHAPE (bumped in BumpShapeVersion) + // nor its params changed since the last sync, there is nothing to re-check or + // re-upload - skip CheckMipmapCompleteness, SyncTextureResource, SyncTextureViews and + // the per-level dirty scan. Layout is maintained separately by the transition path, so + // the resource still reflects truth. The shape version is NOT redundant with the + // content one: glTexImage2D(..., nullptr) re-specifies a level's size or format + // without dirtying a texel, which is exactly how a re-specified image-unit texture used + // to keep reporting its old imageSize(). const Uint64 syncingContentVersion = texture.GetContentVersion(); + const Uint64 syncingShapeVersion = texture.GetShapeVersion(); const auto* syncingMipTexture = MG_State::GLState::AsMipmapTexture(&texture); const Uint32 syncingMipLevelCount = syncingMipTexture != nullptr ? syncingMipTexture->GetMipmapLevelCount() : 0u; @@ -1609,6 +1615,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { m_storageImageTextures.find(MakeTextureIdentity(&texture)) != m_storageImageTextures.end(); if (outResource.image != VK_NULL_HANDLE && !storageUpgradePending && outResource.syncedContentVersion == syncingContentVersion && + outResource.syncedShapeVersion == syncingShapeVersion && outResource.syncedTextureParamsVersion == texture.GetTextureParamsVersion() && outResource.syncedMipLevelCount == syncingMipLevelCount) { return true; @@ -1629,6 +1636,12 @@ namespace MobileGL::MG_Backend::DirectVulkan { return false; } + // From here down the size is VULKAN geometry, not GL's: a 1D array's layer count moves + // out of the height it occupies GL-side and into z, which is the slot + // TryResolveTextureShapeInfo reads arrayLayers from and the only one that leaves + // extent.height at the 1 a VK_IMAGE_TYPE_1D image is required to have. + texelSize = ToVulkanLevelExtent(texture.GetTarget(), texelSize); + if (!SyncTextureResource(texture, uploadTarget, texelSize, byteSize, mipLevelCount, outResource)) { MGLOG_D("%s: SyncTextureResource failed", __func__); return false; @@ -1660,6 +1673,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { if (!hasDirtyMipLevel) { outResource.syncedContentVersion = syncingContentVersion; outResource.syncedMipLevelCount = syncingMipLevelCount; + outResource.syncedShapeVersion = syncingShapeVersion; return true; } @@ -1669,6 +1683,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { } outResource.syncedContentVersion = syncingContentVersion; outResource.syncedMipLevelCount = syncingMipLevelCount; + outResource.syncedShapeVersion = syncingShapeVersion; return true; } @@ -2536,7 +2551,13 @@ namespace MobileGL::MG_Backend::DirectVulkan { uploadItem.target = target; uploadItem.level = level; uploadItem.baseArrayLayer = ResolveUploadArrayLayer(target); - uploadItem.texelSize = texelSize; + // Vulkan geometry, like the image this stages into (see SyncTexture): a 1D + // array's layers move from y to z, where the copy loop's depthSelectsArrayLayer + // branch turns them into layerCount. The shadow needs no repacking to follow - + // one layer of a 1D array IS one row of `width` texels, so the tight-packed + // per-layer copy the swapped size describes reads the same bytes in the same + // order as the row-major level it replaces. + uploadItem.texelSize = ToVulkanLevelExtent(mipmapTexture.GetTarget(), texelSize); uploadItem.source = source; uploadItem.offset = stagingSize; uploadItem.uploadByteSize = byteSize; @@ -2574,6 +2595,23 @@ namespace MobileGL::MG_Backend::DirectVulkan { } uploadItem.uploadByteSize = rectTexels * uploadItem.texelBytes; } + // The boxes came out of the shadow in GL coordinates, where a 1D + // array's layer is the y. They have to follow texelSize across to z or + // they would address rows of an image that now has exactly one, and + // the staging walk would read the wrong bytes for them. Every byte + // count computed above is a product of the three extents, so moving + // the axes leaves all of them alone - and an OFFSET lands on a zero y, + // not on the extent's one, which is why this is spelled out rather than + // handed to ToVulkanLevelExtent. + if (mipmapTexture.GetTarget() == TextureTarget::Texture1DArray) { + uploadItem.regionLo = {uploadItem.regionLo.x(), 0, uploadItem.regionLo.y()}; + uploadItem.regionSize = {uploadItem.regionSize.x(), 1, + uploadItem.regionSize.y()}; + for (auto& rect : uploadItem.rects) { + rect.lo = {rect.lo.x(), 0, rect.lo.y()}; + rect.hi = {rect.hi.x(), 1, rect.hi.y()}; + } + } } } if (formatInfo.expandRgbToRgba) { diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h index 40f23d0d..423da9da 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h @@ -22,6 +22,25 @@ class ITextureObject; namespace MobileGL::MG_Backend::DirectVulkan { enum class SamplerNumericDomain : Uint8; +// A GL 1D-ARRAY level keeps its LAYER COUNT in the state-side HEIGHT: that is what +// glTexImage2D(GL_TEXTURE_1D_ARRAY, width, layers) means, and the frontend records the level +// as {width, layers, 1} (see GL_Texture.cpp's AllocateStorage and the completeness walk in +// TextureObject.cpp, which shrinks only x down the chain). Vulkan packs it the other way: a +// 1D array is a VK_IMAGE_TYPE_1D image whose extent.height MUST be 1 and whose layers live in +// arrayLayers - i.e. in the slot this backend reads out of z. So every place that turns a GL +// level size into Vulkan image geometry has to move the count across first, and every GL-space +// sub-box that rides along with it has to move its y the same way. DirectGLES performs the +// identical remap onto the ES 2D array it maps 1D arrays to (GetBackendUploadSize). +// +// Applied to nothing else: a 2D array, a cube array and a 3D texture all already carry their +// depth/layer count in z, which is where the Vulkan side expects it. +inline IntVec3 ToVulkanLevelExtent(TextureTarget stateTarget, const IntVec3& glTexelSize) { + if (stateTarget == TextureTarget::Texture1DArray) { + return {glTexelSize.x(), 1, glTexelSize.y()}; + } + return glTexelSize; +} + class VkTextureManager { public: // Monotonic epoch bumped whenever a texture VkImage is (re)created. The render-pass @@ -206,6 +225,12 @@ public: // as defense-in-depth: any path that grows the level set (which resizes the sampled view) // busts the skip even if it failed to bump the content version. Uint32 syncedMipLevelCount = 0; + // Snapshot of ITextureObject::GetShapeVersion() at the last successful sync. The content + // version alone does NOT cover a re-specification: glTexImage2D(..., nullptr) on an + // already-defined level changes its size or format and dirties no texel, so it moves the + // shape version and nothing else. Without this in the early-out key the image, its views + // and therefore imageSize() all keep answering with the texture's PREVIOUS shape. + Uint64 syncedShapeVersion = 0; TextureResource() = default; TextureResource(const TextureResource&) = delete; @@ -237,6 +262,7 @@ public: std::swap(this->lastRecordingGeneration, that.lastRecordingGeneration); std::swap(this->syncedContentVersion, that.syncedContentVersion); std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount); + std::swap(this->syncedShapeVersion, that.syncedShapeVersion); } void Reset() { @@ -300,6 +326,7 @@ public: syncedTextureParamsVersion = 0; syncedContentVersion = 0; syncedMipLevelCount = 0; + syncedShapeVersion = 0; } ~TextureResource() { diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 6d2dbf7d..2a1b0541 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -9813,7 +9813,7 @@ void main() { VkImageAspectFlags imageAspect, Uint32 mipLevel, Uint32 baseArrayLayer, GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels, - Bool defaultFramebufferOrientation) { + Bool defaultFramebufferOrientation, Uint32 sourceLayerCount) { const Bool wantDepth = format != GL_STENCIL_INDEX; const Bool wantStencil = format != GL_DEPTH_COMPONENT; auto& frame = m_frameContext.GetCurrent(); @@ -9892,6 +9892,10 @@ void main() { if (!mapped) return; } + // See the header: a stack of one-row layers and a single multi-row layer copy out to the + // same tightly-packed bytes, so only the region's shape splits the two cases. + const Uint32 copyLayerCount = std::max(sourceLayerCount, 1u); + const Uint32 copyRowCount = copyLayerCount > 1u ? 1u : copyExtent.height; VkBufferImageCopy regions[2]{}; Uint32 regionCount = 0; if (wantDepth) { @@ -9900,9 +9904,9 @@ void main() { region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; region.imageSubresource.mipLevel = mipLevel; region.imageSubresource.baseArrayLayer = baseArrayLayer; - region.imageSubresource.layerCount = 1; + region.imageSubresource.layerCount = copyLayerCount; region.imageOffset = {copyOffset.x, copyOffset.y, 0}; - region.imageExtent = {copyExtent.width, copyExtent.height, 1}; + region.imageExtent = {copyExtent.width, copyRowCount, 1}; } if (wantStencil) { auto& region = regions[regionCount++]; @@ -9910,9 +9914,9 @@ void main() { region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_STENCIL_BIT; region.imageSubresource.mipLevel = mipLevel; region.imageSubresource.baseArrayLayer = baseArrayLayer; - region.imageSubresource.layerCount = 1; + region.imageSubresource.layerCount = copyLayerCount; region.imageOffset = {copyOffset.x, copyOffset.y, 0}; - region.imageExtent = {copyExtent.width, copyExtent.height, 1}; + region.imageExtent = {copyExtent.width, copyRowCount, 1}; } vkCmdCopyImageToBuffer(frame.commandBuffer, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, readback.GetHandle(), regionCount, regions); @@ -10147,9 +10151,17 @@ void main() { ? static_cast(textureUploadTarget) - static_cast(TextureUploadTarget::CubeMapPositiveX) : 0; + // A 1D array's levelSize.y() is its LAYER count, and those layers are the rows + // GL wants back - but in Vulkan they are array layers of a one-row image, not + // rows of layer 0, so the read has to be told which of the two it is looking at. + const Uint32 sourceLayers = + textureObject->GetTarget() == TextureTarget::Texture1DArray + ? static_cast(std::max(levelSize.y(), 1)) + : 1u; ReadDepthStencilImageToClient(resource->image, resource->format, &resource->layout, resource->aspect, static_cast(level), arrayLayer, 0, 0, levelSize.x(), - levelSize.y(), format, type, pixels); + levelSize.y(), format, type, pixels, + /*defaultFramebufferOrientation=*/false, sourceLayers); } else { MGLOG_E_ONCE("DirectVulkan::GetTexImage skipped: color query of a non-color texture"); } @@ -10167,12 +10179,19 @@ void main() { // destination layout (GL 3.3 section 6.1.4). const auto imageTextureTarget = textureObject->GetTarget(); const Bool is3dImage = imageTextureTarget == TextureTarget::Texture3D; - const Bool isArrayImage = imageTextureTarget == TextureTarget::Texture1DArray || + const Bool is1dArrayImage = imageTextureTarget == TextureTarget::Texture1DArray; + const Bool isArrayImage = is1dArrayImage || imageTextureTarget == TextureTarget::Texture2DArray || imageTextureTarget == TextureTarget::TextureCubeMapArray; const GLsizei depthSlices = is3dImage ? std::max(texelSize.z(), 1) : 1; const GLsizei arrayLayers = isArrayImage ? static_cast(resource->arrayLayers) : 1; - const GLsizei sliceCount = std::max(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(depthSlices * (is1dArrayImage ? 1 : arrayLayers), 1); if (bufSize >= 0) { const Int dstChannels = GetReadbackChannelCount(format); if ((type == GL_UNSIGNED_BYTE || type == GL_FLOAT) && dstChannels > 0) { @@ -10225,7 +10244,8 @@ void main() { copyRegion.imageSubresource.mipLevel = static_cast(level); copyRegion.imageSubresource.baseArrayLayer = 0; copyRegion.imageSubresource.layerCount = static_cast(arrayLayers); - copyRegion.imageExtent = {static_cast(width), static_cast(height), + copyRegion.imageExtent = {static_cast(width), + is1dArrayImage ? 1u : static_cast(height), static_cast(depthSlices)}; vkCmdCopyImageToBuffer(frame.commandBuffer, resource->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, readback.GetHandle(), 1, ©Region); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index fff16041..d03c82ec 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -217,10 +217,15 @@ namespace MobileGL::MG_Backend::DirectVulkan { // depth/stencil image, which this renderer stores display-side-up: the copy rect then // has to be mapped out of GL's bottom-origin space and the copied rows re-oriented on // the way back, exactly as the colour ReadPixels path does. + // `sourceLayerCount` above 1 says the `height` rows the client is owed are stored as that + // many ARRAY LAYERS of a one-row image rather than as rows of one layer - the shape a GL + // 1D array has in Vulkan. The two produce byte-identical tightly-packed readbacks, so + // only the copy region differs; everything after it is written against `height`. void ReadDepthStencilImageToClient(VkImage image, VkFormat vkFormat, VkImageLayout* trackedLayout, VkImageAspectFlags imageAspect, Uint32 mipLevel, Uint32 baseArrayLayer, GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, - void* pixels, Bool defaultFramebufferOrientation = false); + void* pixels, Bool defaultFramebufferOrientation = false, + Uint32 sourceLayerCount = 1); // Same-extent depth blit between images of different depth formats: host // round-trip with a per-texel re-encode (see BlitNamedFramebuffer). Bool BlitDepthAcrossFormats(FrameContext::FrameData& frame, VkImage srcImage, VkFormat srcFormat, diff --git a/MobileGL/MG_Impl/GLImpl/Buffer/Validators.cpp b/MobileGL/MG_Impl/GLImpl/Buffer/Validators.cpp index 50df5b1e..a93da564 100644 --- a/MobileGL/MG_Impl/GLImpl/Buffer/Validators.cpp +++ b/MobileGL/MG_Impl/GLImpl/Buffer/Validators.cpp @@ -13,6 +13,7 @@ #include #include #include +#include namespace MobileGL::MG_Impl::GLImpl::BufferImpl { Bool ValidateBufferTarget(BufferTarget target) { @@ -67,6 +68,13 @@ namespace MobileGL::MG_Impl::GLImpl::BufferImpl { // binding points in GL 3.3 (no ARB_transform_feedback3). pointCount = std::min(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( + pointCount, static_cast(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTER_BUFFER_BINDINGS)); + } return pointCount; } } // namespace diff --git a/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp b/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp index 408e08fb..ceaf42fb 100644 --- a/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp +++ b/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp @@ -326,10 +326,23 @@ namespace MobileGL::MG_Impl::GLImpl { } } + // GL 4.6 core 10.9: inside a conditional block whose predicate did not pass, the drawing + // commands, Clear, ClearBuffer* and the compute dispatches are DISCARDED. The gate sits on the + // wrappers that ISSUE the backend call rather than at the top of each entry point, so that + // everything a real driver would still do inside the block - argument validation and the + // errors it raises - happens exactly as it does outside one, and only the command itself is + // dropped. It is deliberately not on the frontend's transform-feedback accounting either: + // that mirrors what the capture stage would have written, and a conditional block around a + // capturing draw has no test coverage in either direction. + static Bool ConditionalRenderDiscardsCommand() { + return MG_State::pGLContext->ConditionalRenderDiscardsCommands(); + } + void Clear_Backend(GLbitfield mask) { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif + if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.Clear(mask); } @@ -337,6 +350,7 @@ namespace MobileGL::MG_Impl::GLImpl { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif + if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.DrawElements(mode, count, type, indices); } @@ -345,6 +359,7 @@ namespace MobileGL::MG_Impl::GLImpl { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif + if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.MultiDrawElements(mode, count, type, indices, drawcount); } @@ -353,6 +368,7 @@ namespace MobileGL::MG_Impl::GLImpl { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif + if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsBaseVertex(mode, count, type, indices, drawcount, basevertex); } @@ -361,6 +377,7 @@ namespace MobileGL::MG_Impl::GLImpl { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif + if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.DrawArrays(mode, first, count); } @@ -368,6 +385,7 @@ namespace MobileGL::MG_Impl::GLImpl { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif + if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.MultiDrawArrays(mode, first, count, drawcount); } @@ -376,6 +394,7 @@ namespace MobileGL::MG_Impl::GLImpl { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif + if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.DrawElementsBaseVertex(mode, count, type, indices, basevertex); } @@ -384,6 +403,7 @@ namespace MobileGL::MG_Impl::GLImpl { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif + if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirect(mode, type, indirect, drawcount, stride); } @@ -391,6 +411,7 @@ namespace MobileGL::MG_Impl::GLImpl { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif + if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirect(mode, indirect, drawcount, stride); } @@ -399,6 +420,7 @@ namespace MobileGL::MG_Impl::GLImpl { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif + if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirectCount(mode, type, indirect, drawcount, maxdrawcount, stride); } @@ -408,6 +430,7 @@ namespace MobileGL::MG_Impl::GLImpl { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif + if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirectCount(mode, indirect, drawcount, maxdrawcount, stride); } @@ -417,6 +440,7 @@ namespace MobileGL::MG_Impl::GLImpl { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif + if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.DrawRangeElementsBaseVertex(mode, start, end, count, type, indices, basevertex); } @@ -426,6 +450,7 @@ namespace MobileGL::MG_Impl::GLImpl { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif + if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.DrawRangeElements(mode, start, end, count, type, indices); } @@ -435,6 +460,7 @@ namespace MobileGL::MG_Impl::GLImpl { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif + if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseVertexBaseInstance( mode, count, type, indices, instancecount, basevertex, baseinstance); } @@ -444,6 +470,7 @@ namespace MobileGL::MG_Impl::GLImpl { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif + if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseVertex(mode, count, type, indices, instancecount, basevertex); } @@ -453,6 +480,7 @@ namespace MobileGL::MG_Impl::GLImpl { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif + if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseInstance(mode, count, type, indices, instancecount, baseinstance); } @@ -462,6 +490,7 @@ namespace MobileGL::MG_Impl::GLImpl { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif + if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstanced(mode, count, type, indices, instancecount); } @@ -469,6 +498,7 @@ namespace MobileGL::MG_Impl::GLImpl { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif + if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.DrawElementsIndirect(mode, type, indirect); } void DrawArraysInstancedBaseInstance_Backend(GLenum mode, GLint first, GLsizei count, GLsizei instancecount, @@ -476,6 +506,7 @@ namespace MobileGL::MG_Impl::GLImpl { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif + if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.DrawArraysInstancedBaseInstance(mode, first, count, instancecount, baseinstance); } @@ -484,6 +515,7 @@ namespace MobileGL::MG_Impl::GLImpl { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif + if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.DrawArraysInstanced(mode, first, count, instancecount); } @@ -491,6 +523,7 @@ namespace MobileGL::MG_Impl::GLImpl { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif + if (ConditionalRenderDiscardsCommand()) return; MG_Backend::gBackendFunctionsTable.GL.DrawArraysIndirect(mode, indirect); } @@ -519,6 +552,9 @@ namespace MobileGL::MG_Impl::GLImpl { return; } } + // GL 4.3 added both dispatches to the conditional-render set (GL 4.6 core 10.9), which is + // exactly what KHR-GL43.compute_shader.conditional-dispatching checks. + if (ConditionalRenderDiscardsCommand()) return; dispatchCompute(numGroupsX, numGroupsY, numGroupsZ); } @@ -570,6 +606,7 @@ namespace MobileGL::MG_Impl::GLImpl { return; } if (!ValidateCurrentProgramForCompute(__func__)) return; + if (ConditionalRenderDiscardsCommand()) return; dispatchComputeIndirect(indirect); } diff --git a/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp b/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp index 1be2e4b2..c23a9f3e 100644 --- a/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp +++ b/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp @@ -725,8 +725,8 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, LoadName, GLuint name) DECLARE_GL_FUNCTION_S DECLARE_GL_FUNCTION_STUB_HEAD(void, PushName, GLuint name) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PushName, name) DECLARE_GL_FUNCTION_STUB_HEAD(void, PopName) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PopName) DECLARE_GL_FUNCTION_HEAD(void, ClampColor, GLenum target, GLenum clamp) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClampColor, target, clamp) -DECLARE_GL_FUNCTION_STUB_HEAD(void, BeginConditionalRender, GLuint id, GLenum mode) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BeginConditionalRender, id, mode) -DECLARE_GL_FUNCTION_STUB_HEAD(void, EndConditionalRender, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, EndConditionalRender) +DECLARE_GL_FUNCTION_HEAD(void, BeginConditionalRender, GLuint id, GLenum mode) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BeginConditionalRender, id, mode) +DECLARE_GL_FUNCTION_HEAD(void, EndConditionalRender) DECLARE_GL_FUNCTION_END_NO_RETURN(void, EndConditionalRender) DECLARE_GL_FUNCTION_HEAD(void, VertexAttribI1i, GLuint index, GLint x) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribI1i, index, x) DECLARE_GL_FUNCTION_HEAD(void, VertexAttribI2i, GLuint index, GLint x, GLint y) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribI2i, index, x, y) DECLARE_GL_FUNCTION_HEAD(void, VertexAttribI3i, GLuint index, GLint x, GLint y, GLint z) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribI3i, index, x, y, z) @@ -982,7 +982,7 @@ DECLARE_GL_FUNCTION_HEAD(void, GetDoublei_v, GLenum target, GLuint index, GLdoub DECLARE_GL_FUNCTION_HEAD(void, DrawArraysInstancedBaseInstance, GLenum mode, GLint first, GLsizei count, GLsizei instancecount, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawArraysInstancedBaseInstance, mode, first, count, instancecount, baseinstance) DECLARE_GL_FUNCTION_HEAD(void, DrawElementsInstancedBaseInstance, GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawElementsInstancedBaseInstance, mode, count, type, indices, instancecount, baseinstance) DECLARE_GL_FUNCTION_HEAD(void, DrawElementsInstancedBaseVertexBaseInstance, GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLint basevertex, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawElementsInstancedBaseVertexBaseInstance, mode, count, type, indices, instancecount, basevertex, baseinstance) -DECLARE_GL_FUNCTION_STUB_HEAD(void, GetActiveAtomicCounterBufferiv, GLuint program, GLuint bufferIndex, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetActiveAtomicCounterBufferiv, program, bufferIndex, pname, params) +DECLARE_GL_FUNCTION_HEAD(void, GetActiveAtomicCounterBufferiv, GLuint program, GLuint bufferIndex, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetActiveAtomicCounterBufferiv, program, bufferIndex, pname, params) DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedbackInstanced, GLenum mode, GLuint id, GLsizei instancecount) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedbackInstanced, mode, id, instancecount) DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedbackStreamInstanced, GLenum mode, GLuint id, GLuint stream, GLsizei instancecount) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedbackStreamInstanced, mode, id, stream, instancecount) DECLARE_GL_FUNCTION_HEAD(void, ClearBufferData, GLenum target, GLenum internalformat, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearBufferData, target, internalformat, format, type, data) diff --git a/MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp b/MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp index 60292eea..8bd69f80 100644 --- a/MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp +++ b/MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp @@ -2613,18 +2613,26 @@ namespace MobileGL::MG_Impl::GLImpl { } void ClearBufferfi_Backend(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) { + // GL 4.6 core 10.9 makes ClearBuffer* conditional alongside the drawing commands. + if (MG_State::pGLContext->ConditionalRenderDiscardsCommands()) return; MG_Backend::gBackendFunctionsTable.GL.ClearBufferfi(buffer, drawbuffer, depth, stencil); } void ClearBufferfv_Backend(GLenum buffer, GLint drawbuffer, const GLfloat* value) { + // GL 4.6 core 10.9 makes ClearBuffer* conditional alongside the drawing commands. + if (MG_State::pGLContext->ConditionalRenderDiscardsCommands()) return; MG_Backend::gBackendFunctionsTable.GL.ClearBufferfv(buffer, drawbuffer, value); } void ClearBufferuiv_Backend(GLenum buffer, GLint drawbuffer, const GLuint* value) { + // GL 4.6 core 10.9 makes ClearBuffer* conditional alongside the drawing commands. + if (MG_State::pGLContext->ConditionalRenderDiscardsCommands()) return; MG_Backend::gBackendFunctionsTable.GL.ClearBufferuiv(buffer, drawbuffer, value); } void ClearBufferiv_Backend(GLenum buffer, GLint drawbuffer, const GLint* value) { + // GL 4.6 core 10.9 makes ClearBuffer* conditional alongside the drawing commands. + if (MG_State::pGLContext->ConditionalRenderDiscardsCommands()) return; MG_Backend::gBackendFunctionsTable.GL.ClearBufferiv(buffer, drawbuffer, value); } diff --git a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp index 64675804..8e7a03eb 100644 --- a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp +++ b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include namespace MobileGL::MG_Impl::GLImpl { @@ -46,13 +47,29 @@ namespace MobileGL::MG_Impl::GLImpl { } } - constexpr GLint kFrontendMaxComputeUniformComponents = 1024; - constexpr GLint kFrontendMaxComputeAtomicCounters = 8; - constexpr GLint kFrontendMaxComputeAtomicCounterBuffers = 8; + // Shared with the glslang resource table for the same reason as the atomic-counter + // limits below: gl_MaxComputeUniformComponents expands from BuildTBuiltInResource. + constexpr GLint kFrontendMaxComputeUniformComponents = + static_cast(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(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTERS_PER_STAGE); + constexpr GLint kFrontendMaxComputeAtomicCounterBuffers = + static_cast(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE); constexpr GLint kFrontendMaxComputeSharedMemorySize = 32768; constexpr GLint kFrontendMaxComputeWorkGroupInvocations = 1024; - constexpr GLint kFrontendMaxCombinedAtomicCounters = 8; - constexpr GLint kFrontendMaxFragmentAtomicCounters = 8; + constexpr GLint kFrontendMaxCombinedAtomicCounters = + static_cast(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTERS_PER_STAGE); + constexpr GLint kFrontendMaxCombinedAtomicCounterBuffers = + static_cast(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE); + constexpr GLint kFrontendMaxFragmentAtomicCounters = + static_cast(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTERS_PER_STAGE); + constexpr GLint kFrontendMaxFragmentAtomicCounterBuffers = + static_cast(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE); constexpr GLint kFrontendMaxGeometryAtomicCounters = 0; constexpr GLint kFrontendMaxTessControlAtomicCounters = 0; constexpr GLint kFrontendMaxTessEvaluationAtomicCounters = 0; @@ -66,10 +83,11 @@ namespace MobileGL::MG_Impl::GLImpl { constexpr GLint kFrontendMaxTessControlAtomicCounterBuffers = 0; constexpr GLint kFrontendMaxTessEvaluationAtomicCounterBuffers = 0; constexpr GLint kFrontendMaxVertexAtomicCounterBuffers = 0; - // One atomic counter is a uint, and a buffer never has to hold more counters than the - // combined limit the frontend advertises. GL 4.6 table 23.63 floors this at 32 bytes. + // GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE: the byte offset ceiling a counter may be declared + // at. The matching binding count is applied in GetIndexedBufferQueryPointCount, so that + // the getter, the indexed queries and glBindBufferBase all share one ceiling. constexpr GLint kFrontendMaxAtomicCounterBufferSize = - kFrontendMaxCombinedAtomicCounters * static_cast(sizeof(GLuint)); + static_cast(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTER_BUFFER_SIZE); // KHR_debug minima (GL 4.6 table 23.66); the debug entry points are stubs, but the // limits they advertise still have to be legal. constexpr GLint kFrontendMaxDebugGroupStackDepth = 64; @@ -103,12 +121,16 @@ namespace MobileGL::MG_Impl::GLImpl { constexpr GLint kFrontendSubpixelBits = 4; constexpr GLint kFrontendMaxSamples = 4; + // The floors under GL_MAX_COMPUTE_WORK_GROUP_COUNT / _SIZE. Shared with the compile + // pipeline (CaptureCompileEnv floors the same driver answers at them, and + // BuildTBuiltInResource expands gl_MaxComputeWorkGroup* from the result), because a + // shader is allowed to compare the built-in constant against this query. constexpr GLint GetMinComputeWorkGroupCount(GLuint index) { - return index < 3 ? 65535 : 0; + return index < 3 ? static_cast(MG_Util::ShaderTranspiler::MIN_COMPUTE_WORK_GROUP_COUNT[index]) : 0; } constexpr GLint GetMinComputeWorkGroupSize(GLuint index) { - return index < 2 ? 1024 : (index == 2 ? 64 : 0); + return index < 3 ? static_cast(MG_Util::ShaderTranspiler::MIN_COMPUTE_WORK_GROUP_SIZE[index]) : 0; } GLint GetMaxCombinedUniformComponents(GLint maxDefaultUniformComponents, GLint maxUniformBlocks, @@ -186,6 +208,16 @@ namespace MobileGL::MG_Impl::GLImpl { MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxShaderStorageBufferBindings; return std::min(frontendCount, static_cast(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(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTER_BUFFER_BINDINGS)); + } return frontendCount; } @@ -1549,6 +1581,9 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_MAX_COMBINED_ATOMIC_COUNTERS: *params = kFrontendMaxCombinedAtomicCounters; return; + case GL_MAX_COMBINED_ATOMIC_COUNTER_BUFFERS: + *params = kFrontendMaxCombinedAtomicCounterBuffers; + return; case GL_MAX_COMBINED_UNIFORM_BLOCKS: *params = ClampUniformBlockCount(kFrontendMaxCombinedUniformBlocks); return; @@ -1564,6 +1599,9 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_MAX_FRAGMENT_ATOMIC_COUNTERS: *params = kFrontendMaxFragmentAtomicCounters; return; + case GL_MAX_FRAGMENT_ATOMIC_COUNTER_BUFFERS: + *params = kFrontendMaxFragmentAtomicCounterBuffers; + return; case GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS: *params = StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxFragmentShaderStorageBlocks); return; @@ -1672,7 +1710,8 @@ namespace MobileGL::MG_Impl::GLImpl { *params = 15; // TODO return; case GL_MAX_UNIFORM_LOCATIONS: - *params = 1024 * 4; // TODO + // The same constant the link's location allocator enforces - see ProgramObject. + *params = MG_State::GLState::ProgramObject::MAX_UNIFORM_LOCATIONS; return; case GL_MAX_VARYING_COMPONENTS: *params = kFrontendMaxVaryingComponents; @@ -2002,6 +2041,24 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_UNIFORM_BUFFER_START: RecordIndexedOnlyGetterError(__func__, pname); return; + // glBindBufferBase/Range set the GENERIC binding point too (GL 4.6 core 6.1.1), and this + // is the one indexed-buffer family whose non-indexed query was never answered - so it + // fell through to INVALID_ENUM and left the caller's variable holding whatever was in its + // stack slot. _START/_SIZE stay indexed-only, exactly like their uniform-buffer siblings. + case GL_ATOMIC_COUNTER_BUFFER_BINDING: + if (const auto& obj = + MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::AtomicCounter).GetBoundObject()) { + *params = static_cast(obj->GetExternalIndex()); + } else { + *params = 0; + } + return; + case GL_ATOMIC_COUNTER_BUFFER_START: + RecordIndexedOnlyGetterError(__func__, pname); + return; + case GL_ATOMIC_COUNTER_BUFFER_SIZE: + RecordIndexedOnlyGetterError(__func__, pname); + return; case GL_UNPACK_ALIGNMENT: *params = MG_State::pGLContext->GetPixelStoreParam(PixelStoreParam::UnpackAlignment); return; @@ -2237,18 +2294,19 @@ namespace MobileGL::MG_Impl::GLImpl { static_cast(INT32_MAX))); break; case GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS: + // NOT the frontend's binding-point array size: GetIndexedBufferQueryPointCount + // clamps this family to the range a lowered counter block can actually be served + // from, which is the same number glslang compiles a layout(binding = N) atomic_uint + // against and the same one glBindBufferBase validates an index against. *params = static_cast(GetIndexedBufferQueryPointCount(BufferTarget::AtomicCounter)); break; case GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE: // The conformance suite splits this evenly across every advertised binding point and // binds all of them in one glBindBuffersRange - // (KHR-GL44.multi_bind.functional_bind_buffers_range), so the pair has to divide: - // 32 bytes over 36 binding points is a zero-sized range, which BindBufferRange - // rejects with INVALID_VALUE before it binds anything. Floor the advertised size at - // one counter per binding point. - *params = std::max( - kFrontendMaxAtomicCounterBufferSize, - static_cast(GetIndexedBufferQueryPointCount(BufferTarget::AtomicCounter) * sizeof(GLuint))); + // (KHR-GL44.multi_bind.functional_bind_buffers_range), so the pair has to divide - + // a zero-sized range is INVALID_VALUE before BindBufferRange binds anything. The + // shared constant is 16384 over 8 binding points, which divides. + *params = kFrontendMaxAtomicCounterBufferSize; break; case GL_MAX_TEXTURE_BUFFER_SIZE: *params = dynamicParameters.MaxTextureBufferSize; diff --git a/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp b/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp index 29090d8c..da1f5732 100644 --- a/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp +++ b/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp @@ -642,7 +642,13 @@ namespace MobileGL::MG_Impl::GLImpl { break; } case GL_ACTIVE_ATOMIC_COUNTER_BUFFERS: - *params = programObject->GetActiveAtomicCounterCount(); + // Counter BUFFERS, not counters, and glslang's own getNumAtomicCounters() answers + // neither: the relaxed parse has already turned every atomic_uint into a plain uint + // member of a synthesized storage block by the time it builds its reflection, so it + // reports zero. The interface-query model recovers the buffers from those blocks and + // is what glGetProgramInterfaceiv(GL_ATOMIC_COUNTER_BUFFER, GL_ACTIVE_RESOURCES) + // already answers - the two queries are required to agree. + *params = ProgramInterface::GetActiveResourceCount(*programObject, GL_ATOMIC_COUNTER_BUFFER); MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params); break; case GL_ACTIVE_ATTRIBUTES: @@ -2835,6 +2841,73 @@ namespace MobileGL::MG_Impl::GLImpl { return ProgramInterface::GetResourceLocationIndex(*programObject, programInterface, name); } + // GL 4.6 §7.7. Every property this reports is one the GL_ATOMIC_COUNTER_BUFFER interface + // already carries, so this is a rename of glGetProgramResourceiv's props onto the older + // entry point's - and the two are required to agree, which is only true while both read the + // same model. It was a silent stub: it wrote nothing, raised nothing, and left every probe + // reading its own uninitialised output. + static Bool TryMapActiveAtomicCounterBufferProp(GLenum pname, GLenum& outProp) { + switch (pname) { + case GL_ATOMIC_COUNTER_BUFFER_BINDING: + outProp = GL_BUFFER_BINDING; + return true; + case GL_ATOMIC_COUNTER_BUFFER_DATA_SIZE: + outProp = GL_BUFFER_DATA_SIZE; + return true; + case GL_ATOMIC_COUNTER_BUFFER_ACTIVE_ATOMIC_COUNTERS: + outProp = GL_NUM_ACTIVE_VARIABLES; + return true; + case GL_ATOMIC_COUNTER_BUFFER_ACTIVE_ATOMIC_COUNTER_INDICES: + outProp = GL_ACTIVE_VARIABLES; + return true; + case GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_VERTEX_SHADER: + outProp = GL_REFERENCED_BY_VERTEX_SHADER; + return true; + case GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_TESS_CONTROL_SHADER: + outProp = GL_REFERENCED_BY_TESS_CONTROL_SHADER; + return true; + case GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_TESS_EVALUATION_SHADER: + outProp = GL_REFERENCED_BY_TESS_EVALUATION_SHADER; + return true; + case GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_GEOMETRY_SHADER: + outProp = GL_REFERENCED_BY_GEOMETRY_SHADER; + return true; + case GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_FRAGMENT_SHADER: + outProp = GL_REFERENCED_BY_FRAGMENT_SHADER; + return true; + case GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_COMPUTE_SHADER: + outProp = GL_REFERENCED_BY_COMPUTE_SHADER; + return true; + default: + return false; + } + } + + void GetActiveAtomicCounterBufferiv(GLuint program, GLuint bufferIndex, GLenum pname, GLint* params) { + auto& programObject = TryToGetProgramForInterfaceQuery(program, __func__); + if (!programObject) return; + GLenum prop = GL_NONE; + if (!TryMapActiveAtomicCounterBufferProp(pname, prop)) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidEnum, + MakeUnique("MG_Impl/GLImpl", __func__, + "pname is not an active atomic counter buffer property.")); + return; + } + Vector values; + if (!ProgramInterface::GetResourceProp(*programObject, GL_ATOMIC_COUNTER_BUFFER, bufferIndex, prop, values)) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique("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: is an active shader storage block index of // - 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 diff --git a/MobileGL/MG_Impl/GLImpl/Program/GL_Program.h b/MobileGL/MG_Impl/GLImpl/Program/GL_Program.h index 437db49c..cdebc445 100644 --- a/MobileGL/MG_Impl/GLImpl/Program/GL_Program.h +++ b/MobileGL/MG_Impl/GLImpl/Program/GL_Program.h @@ -140,6 +140,7 @@ namespace MobileGL::MG_Impl::GLImpl { const GLenum* props, GLsizei bufSize, GLsizei* length, GLint* params); GLint GetProgramResourceLocation(GLuint program, GLenum programInterface, const GLchar* name); GLint GetProgramResourceLocationIndex(GLuint program, GLenum programInterface, const GLchar* name); + void GetActiveAtomicCounterBufferiv(GLuint program, GLuint bufferIndex, GLenum pname, GLint* params); void ShaderStorageBlockBinding(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding); void Uniform1d(GLint location, GLdouble v0); void Uniform1dv(GLint location, GLsizei count, const GLdouble* value); diff --git a/MobileGL/MG_Impl/GLImpl/Program/ProgramInterface.cpp b/MobileGL/MG_Impl/GLImpl/Program/ProgramInterface.cpp index 1137ad5f..6883e733 100644 --- a/MobileGL/MG_Impl/GLImpl/Program/ProgramInterface.cpp +++ b/MobileGL/MG_Impl/GLImpl/Program/ProgramInterface.cpp @@ -19,7 +19,7 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface { // "_" (ParseContextBase.cpp), one per GL // atomic-counter binding point. That block IS the GL_ATOMIC_COUNTER_BUFFER resource // and its trailing number IS GL_BUFFER_BINDING; its members stay GL_UNIFORMs. - constexpr const char* kAtomicCounterBlockPrefix = "gl_AtomicCounterBlock"; + constexpr const char* kAtomicCounterBlockPrefix = MG_Util::ShaderTranspiler::ATOMIC_COUNTER_BLOCK_PREFIX; enum class BlockKind { Uniform, // a real GL uniform block diff --git a/MobileGL/MG_Impl/GLImpl/Query/GL_Query.cpp b/MobileGL/MG_Impl/GLImpl/Query/GL_Query.cpp index c8b95895..dcc470f5 100644 --- a/MobileGL/MG_Impl/GLImpl/Query/GL_Query.cpp +++ b/MobileGL/MG_Impl/GLImpl/Query/GL_Query.cpp @@ -565,6 +565,75 @@ namespace MobileGL::MG_Impl::GLImpl { queryObject->ended = true; } + void BeginConditionalRender(GLuint id, GLenum mode) { + // GL 4.6 core 10.9's eight modes. The _INVERTED half flips the sense of the predicate; + // the BY_REGION half only narrows WHERE an implementation is permitted to discard, so + // treating it as its whole-framebuffer sibling is what an implementation without region + // granularity does. The _NO_WAIT half is a permission to render rather than stall, not an + // obligation - see the resolve below. + Bool inverted = false; + switch (mode) { + case GL_QUERY_WAIT: + case GL_QUERY_NO_WAIT: + case GL_QUERY_BY_REGION_WAIT: + case GL_QUERY_BY_REGION_NO_WAIT: + inverted = false; + break; + case GL_QUERY_WAIT_INVERTED: + case GL_QUERY_NO_WAIT_INVERTED: + case GL_QUERY_BY_REGION_WAIT_INVERTED: + case GL_QUERY_BY_REGION_NO_WAIT_INVERTED: + inverted = true; + break; + default: + RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "mode is not a conditional render mode."); + return; + } + + if (MG_State::pGLContext->IsConditionalRenderActive()) { + RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "Conditional rendering is already active."); + return; + } + + { + const std::lock_guard lock(g_queryObjectsMutex); + const auto* queryObject = FindQueryObjectLocked(id); + // A generated NAME is not yet a query object; it becomes one at its first use with a + // target (the same rule glIsQuery answers by). + if (!queryObject || (!queryObject->created && queryObject->target == 0)) { + RecordQueryError(ErrorCode::InvalidValue, __FUNCTION__, "id is not the name of a query object."); + return; + } + if (queryObject->active) { + RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "The query object is still active."); + return; + } + if (queryObject->target != GL_SAMPLES_PASSED && queryObject->target != GL_ANY_SAMPLES_PASSED && + queryObject->target != GL_ANY_SAMPLES_PASSED_CONSERVATIVE) { + RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, + "Conditional rendering requires an occlusion query object."); + return; + } + } + + // Resolved ONCE, here, and by WAITING even for the _NO_WAIT modes: the spec lets those + // render instead of stalling, so always waiting is conforming and is the only choice that + // gives the whole block one deterministic verdict. Reading it per command instead would + // let a result that lands mid-block change the answer half way through. + Uint64 samplesPassed = 0; + if (!GetQueryObjectValue(id, GL_QUERY_RESULT, __FUNCTION__, samplesPassed)) return; + const Bool passed = samplesPassed != 0; + MG_State::pGLContext->BeginConditionalRender(id, mode, inverted ? passed : !passed); + } + + void EndConditionalRender() { + if (!MG_State::pGLContext->IsConditionalRenderActive()) { + RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "Conditional rendering is not active."); + return; + } + MG_State::pGLContext->EndConditionalRender(); + } + void GetQueryiv(GLenum target, GLenum pname, GLint* params) { if (!params) { return; diff --git a/MobileGL/MG_Impl/GLImpl/Query/GL_Query.h b/MobileGL/MG_Impl/GLImpl/Query/GL_Query.h index 5921e7d2..51998693 100644 --- a/MobileGL/MG_Impl/GLImpl/Query/GL_Query.h +++ b/MobileGL/MG_Impl/GLImpl/Query/GL_Query.h @@ -29,6 +29,11 @@ namespace MobileGL::MG_Impl::GLImpl { void GetQueryBufferObjecti64v(GLuint id, GLuint buffer, GLenum pname, GLintptr offset); void GetQueryBufferObjectui64v(GLuint id, GLuint buffer, GLenum pname, GLintptr offset); void QueryCounter(GLuint id, GLenum target); + // Conditional rendering (GL 4.6 core 10.9). Implemented here rather than beside the drawing + // entry points because the predicate is a QUERY OBJECT's result, and the object registry - + // with the lock that guards it - lives in this file. + void BeginConditionalRender(GLuint id, GLenum mode); + void EndConditionalRender(); // Destroys every still-registered query object exactly as DeleteQueries would. // GL requires queries to die with their context; called only from full library // teardown (DestroyImpl), where no context survives on any thread, so the diff --git a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp index 9009930b..16a459be 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp @@ -661,21 +661,42 @@ namespace MobileGL::MG_Impl::GLImpl { "Compressed texture formats are not supported.")); } - // glGetTexLevelParameter{i,f}v answers WIDTH/HEIGHT/DEPTH out of the mipmap chain. The only - // other storage type the state layer knows is GL_TEXTURE_BUFFER (TextureStorageType is - // {Mipmap, Buffer}), whose level geometry this stack does not track yet. Report that instead - // of throwing: THROW_UNIMPL_EXCEPTION unwinds a C++ exception through the C GL ABI and takes - // the process down, which is never an acceptable answer to a query - see the same reasoning - // above for the compressed-format path. + // GL_TEXTURE_WIDTH of a buffer texture: how many texels of the texture's internal format fit + // in the buffer range it addresses, CLAMPED to GL_MAX_TEXTURE_BUFFER_SIZE. Attaching a larger + // buffer is legal (GL 4.6 core 8.9) - the texture simply addresses the first + // MAX_TEXTURE_BUFFER_SIZE texels of it, and that clamped count is what WIDTH reports. + // + // GL_TEXTURE_BUFFER_SIZE is deliberately NOT clamped the same way: it reports the range in + // basic machine units exactly as glTexBuffer/glTexBufferRange were given it. Swapping the two + // fails KHR-GL43.texture_buffer.texture_buffer_max_size in the opposite direction. + GLint GetBufferTextureTexelWidth(const MG_State::GLState::ITextureObject* textureObject) { + const SizeT texelByteSize = MG_Util::GetSizedInternalFormatSizeInBytes(textureObject->GetFormat()); + // A format with no known footprint has no texel count to report; answering 0 beats + // dividing by it. + if (texelByteSize == 0) return 0; + const auto* bufferTextureObject = + static_cast(textureObject); + const SizeT texelCount = bufferTextureObject->GetBufferRangeSizeInBytes() / texelByteSize; + const SizeT maxTexelCount = static_cast( + std::max(0, MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxTextureBufferSize)); + return static_cast(std::min(texelCount, maxTexelCount)); + } + + // glGetTexLevelParameter{i,f}v answers WIDTH/HEIGHT/DEPTH out of the mipmap chain, and (since + // the buffer-texture arms above) out of the attached buffer range for GL_TEXTURE_BUFFER. This + // is what is left: a storage class with no level geometry at all. Report it instead of + // throwing - THROW_UNIMPL_EXCEPTION unwinds a C++ exception through the C GL ABI and takes the + // process down, which is never an acceptable answer to a query - see the same reasoning above + // for the compressed-format path. void RecordUnsupportedLevelQueryStorage(const char* caller, GLenum pname) { - MGLOG_W_ONCE("%s: glGetTexLevelParameter(pname=%s) is not implemented for texture-buffer " - "storage; recording GL_INVALID_OPERATION instead of terminating", + MGLOG_W_ONCE("%s: glGetTexLevelParameter(pname=%s) is not implemented for this texture's " + "storage class; recording GL_INVALID_OPERATION instead of terminating", caller, MG_Util::ConvertGLEnumToString(pname).c_str()); MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique( "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 @@ -3059,6 +3080,15 @@ namespace MobileGL::MG_Impl::GLImpl { *params = textureObject->GetSamplerObject()->GetMaxAnisotropy(); } break; + // GL 4.6 core 8.11 lists this among the parameters EVERY GetTexParameter form answers. + // It was handled by the iv/Iiv/Iuiv getters and missed by this one, so the float query + // raised GL_INVALID_ENUM and left the caller's float untouched - which is what + // KHR-GL4x.shader_image_load_store.basic-api-texParam reads back. + case GL_IMAGE_FORMAT_COMPATIBILITY_TYPE: + if (params) { + *params = static_cast(GL_IMAGE_FORMAT_COMPATIBILITY_BY_SIZE); + } + break; case GL_DEPTH_STENCIL_TEXTURE_MODE: if (params) { *params = static_cast(textureObject->GetDepthStencilTextureMode()); @@ -3108,6 +3138,9 @@ namespace MobileGL::MG_Impl::GLImpl { *params = textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).x(); break; } + case TextureStorageType::Buffer: + *params = GetBufferTextureTexelWidth(textureObject.get()); + break; default: RecordUnsupportedLevelQueryStorage("GetTexLevelParameteriv_State", pname); break; @@ -3123,6 +3156,9 @@ namespace MobileGL::MG_Impl::GLImpl { *params = textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).y(); break; } + case TextureStorageType::Buffer: + *params = 1; // a buffer texture is one-dimensional + break; default: RecordUnsupportedLevelQueryStorage("GetTexLevelParameteriv_State", pname); break; @@ -3138,6 +3174,9 @@ namespace MobileGL::MG_Impl::GLImpl { *params = textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).z(); break; } + case TextureStorageType::Buffer: + *params = 1; // a buffer texture is one-dimensional + break; default: RecordUnsupportedLevelQueryStorage("GetTexLevelParameteriv_State", pname); break; @@ -3207,6 +3246,31 @@ namespace MobileGL::MG_Impl::GLImpl { } break; } + case GL_TEXTURE_BUFFER_SIZE: + case GL_TEXTURE_BUFFER_OFFSET: { + // GL 4.6 core 8.9: both describe the window of the attached buffer a GL_TEXTURE_BUFFER + // texture addresses, so there is nothing to report for any other storage - which is + // INVALID_OPERATION, the same shape GL_TEXTURE_COMPRESSED_IMAGE_SIZE guards itself with + // above. + if (textureObject->GetStorageType() != TextureStorageType::Buffer) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique( + "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(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(pname == GL_TEXTURE_BUFFER_SIZE + ? bufferTextureObject->GetBufferRangeSizeInBytes() + : bufferTextureObject->GetBufferRangeOffset()); + } + break; + } default: MG_State::pGLContext->RecordError( ErrorCode::InvalidEnum, MakeUnique("MG_Impl/GLImpl", "GetTexLevelParameteriv_State", @@ -3246,6 +3310,9 @@ namespace MobileGL::MG_Impl::GLImpl { *params = (GLfloat)textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).x(); break; } + case TextureStorageType::Buffer: + *params = (GLfloat)GetBufferTextureTexelWidth(textureObject.get()); + break; default: RecordUnsupportedLevelQueryStorage("GetTexLevelParameterfv_State", pname); break; @@ -3261,6 +3328,9 @@ namespace MobileGL::MG_Impl::GLImpl { *params = (GLfloat)textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).y(); break; } + case TextureStorageType::Buffer: + *params = 1.0f; // a buffer texture is one-dimensional + break; default: RecordUnsupportedLevelQueryStorage("GetTexLevelParameterfv_State", pname); break; @@ -3276,6 +3346,9 @@ namespace MobileGL::MG_Impl::GLImpl { *params = (GLfloat)textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).z(); break; } + case TextureStorageType::Buffer: + *params = 1.0f; // a buffer texture is one-dimensional + break; default: RecordUnsupportedLevelQueryStorage("GetTexLevelParameterfv_State", pname); break; @@ -3343,6 +3416,27 @@ namespace MobileGL::MG_Impl::GLImpl { } break; } + case GL_TEXTURE_BUFFER_SIZE: + case GL_TEXTURE_BUFFER_OFFSET: { + // See GetTexLevelParameteriv_State: both describe the attached buffer range of a + // GL_TEXTURE_BUFFER texture, so any other storage makes the query INVALID_OPERATION. + if (textureObject->GetStorageType() != TextureStorageType::Buffer) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique( + "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(textureObject.get()); + *params = static_cast(pname == GL_TEXTURE_BUFFER_SIZE + ? bufferTextureObject->GetBufferRangeSizeInBytes() + : bufferTextureObject->GetBufferRangeOffset()); + } + break; + } default: MG_State::pGLContext->RecordError( ErrorCode::InvalidEnum, MakeUnique("MG_Impl/GLImpl", "GetTexLevelParameterfv_State", diff --git a/MobileGL/MG_Impl/GLImpl/VertexArray/GL_VertexArray.cpp b/MobileGL/MG_Impl/GLImpl/VertexArray/GL_VertexArray.cpp index 6fd7ad63..cfffb533 100644 --- a/MobileGL/MG_Impl/GLImpl/VertexArray/GL_VertexArray.cpp +++ b/MobileGL/MG_Impl/GLImpl/VertexArray/GL_VertexArray.cpp @@ -514,10 +514,15 @@ namespace MobileGL::MG_Impl::GLImpl { // recorded DataType is always Float64 - what IsLong adds is that this is the *unconverted* form, // as opposed to VertexAttribFormat(GL_DOUBLE), which asks for a float conversion. // - // Whether the backend can feed it is detected, not assumed: DirectVulkan needs shaderFloat64, - // and DirectGLES can never have it at all. A backend without it declines here, loudly - GL error - // plus a log line naming the reason - rather than accepting state no draw could honour and - // rendering garbage. The matching startup POST row is in MG_Util/SelfTest/DriverPost.cpp. + // Whether the backend can FEED it is detected, not assumed: DirectVulkan needs shaderFloat64, + // and DirectGLES can never have it at all. What that costs is the ARRAY, not the call: GL 4.6 + // core 10.3.2 defines no error for a well-formed glVertexAttribLFormat, and a GL 4.3 context + // has 64-bit attributes in core, so declining the call would be non-conformant and would make + // the four pure state queries (VERTEX_ATTRIB_ARRAY_SIZE / _TYPE / _LONG / _RELATIVE_OFFSET) + // unanswerable (KHR-GL43.vertex_attrib_binding.basic-state1/3). The format is therefore + // RECORDED here and the enabled array is dropped at draw instead - loudly, once, naming the + // reason. The matching startup POST row is in MG_Util/SelfTest/DriverPost.cpp; the draw-side + // drop is DirectGLES/Managers.cpp and, on DirectVulkan, VertexInputStateFactory's Float64 case. static void VertexAttribLFormatSeparate_State(const SharedPtr& vao, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) { @@ -528,14 +533,11 @@ namespace MobileGL::MG_Impl::GLImpl { if (!MG_Backend::pActiveBackendObject || !MG_Backend::pActiveBackendObject->GetDynamicParameters().SupportsFloat64VertexAttributes) { MGLOG_W_ONCE("VertexAttribLFormat: attribute %u asked for a 64-bit (GL_DOUBLE) format, but this " - "backend has no double-precision vertex attribute support - see the " - "\"64-bit vertex attributes\" / \"shaderFloat64\" POST row for what that costs", + "backend has no double-precision vertex attribute support - the format is recorded " + "and queryable, but the array will be DROPPED at draw and the attribute will read " + "its generic current value; see the \"64-bit vertex attributes\" / \"shaderFloat64\" " + "POST row for what that costs", attribindex); - MG_State::pGLContext->RecordError( - ErrorCode::InvalidOperation, - MakeUnique("MG_Impl/GLImpl", "VertexAttribLFormat", - "64-bit vertex attributes are not supported by this backend.")); - return; } vao->SetAttributeFormatSeparate(attribindex, size, MG_Util::ConvertGLEnumToDataType(type), diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index 4b8b5c61..e5412602 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -80,6 +80,7 @@ add_executable(MobileGLIntegrationTest Scenarios/ImageLoadStoreSsoScenario.cpp Scenarios/ImageTargetKindScenario.cpp Scenarios/ImageFormatQualifierScenario.cpp + Scenarios/ImageSizeAfterRespecScenario.cpp Scenarios/SsboDeclarationFormScenario.cpp Scenarios/Glsl420DeclarationScenario.cpp Scenarios/FragmentOutputArrayIndexScenario.cpp @@ -91,6 +92,8 @@ add_executable(MobileGLIntegrationTest Scenarios/CopyImageLevelRangeScenario.cpp Scenarios/CopyImageLayeredScenario.cpp Scenarios/LayeredAttachmentBarrierScenario.cpp + Scenarios/LayeredTextureReadbackScenario.cpp + Scenarios/AtomicCounterScenario.cpp ) target_include_directories(MobileGLIntegrationTest PRIVATE diff --git a/MobileGL/MG_IntegrationTest/Scenarios/AtomicCounterScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/AtomicCounterScenario.cpp new file mode 100644 index 00000000..55884664 --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Scenarios/AtomicCounterScenario.cpp @@ -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_ 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 +#include + +#include "../Harness/HeadlessGL.h" +#include "../Harness/ScenarioFixture.h" + +#ifdef GLAPI +#undef GLAPI +#endif +#define GL_GLEXT_PROTOTYPES +#include +#include +#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(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& seed) { + GLuint buffer = 0; + glGenBuffers(1, &buffer); + glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, buffer); + glBufferData(GL_ATOMIC_COUNTER_BUFFER, + static_cast(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 ReadCounters(GLuint buffer, int count) { + std::vector values(static_cast(count), 0xDEADBEEFu); + glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, buffer); + glGetBufferSubData(GL_ATOMIC_COUNTER_BUFFER, 0, + static_cast(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 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 zeroValues = ReadCounters(zero, 2); + const std::vector 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 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 diff --git a/MobileGL/MG_IntegrationTest/Scenarios/BufferTextureScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/BufferTextureScenario.cpp index 83c62bc5..392ebad8 100644 --- a/MobileGL/MG_IntegrationTest/Scenarios/BufferTextureScenario.cpp +++ b/MobileGL/MG_IntegrationTest/Scenarios/BufferTextureScenario.cpp @@ -299,4 +299,99 @@ void main() { EXPECT_EQ(FirstGLError(), 0u); } + // glGetTexLevelParameter used to refuse EVERY pname on a buffer texture: WIDTH/HEIGHT/DEPTH + // fell out of a mipmap-only switch as GL_INVALID_OPERATION, and GL_TEXTURE_BUFFER_SIZE / + // GL_TEXTURE_BUFFER_OFFSET were not in the switch at all, so they came back GL_INVALID_ENUM. + // KHR-GL43.texture_buffer wraps both queries in GLU_EXPECT_NO_ERROR, so the error alone fails + // the case before any value is compared. + // + // The two halves report DIFFERENT units and only one of them is clamped, which is the thing + // easiest to get backwards: WIDTH is a TEXEL count clamped to GL_MAX_TEXTURE_BUFFER_SIZE, + // BUFFER_SIZE is the range in basic machine units exactly as it was given. + TEST_F(BufferTextureScenario, LevelQueriesDescribeTheAttachedBufferRange) { + if (!Ready()) return; + FirstGLError(); + + GLint offsetAlignment = 1; + glGetIntegerv(GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT, &offsetAlignment); + if (offsetAlignment < 1) offsetAlignment = 1; + GLint maxTexels = 0; + glGetIntegerv(GL_MAX_TEXTURE_BUFFER_SIZE, &maxTexels); + ASSERT_EQ(FirstGLError(), 0u); + ASSERT_GT(maxTexels, 0) << "an OpenGL 4.x context may not advertise a zero buffer-texture limit"; + + constexpr GLint kTexelBytes = 4; // GL_RGBA8 + const GLsizeiptr rangeOffset = static_cast(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 zeros(static_cast(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(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(rangeBytes)) + << "GL_TEXTURE_BUFFER_SIZE reports basic machine units, not texels"; + EXPECT_EQ(levelQuery(GL_TEXTURE_BUFFER_OFFSET), static_cast(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(rangeBytes / kTexelBytes)); + EXPECT_FLOAT_EQ(levelQueryF(GL_TEXTURE_HEIGHT), 1.0f); + EXPECT_FLOAT_EQ(levelQueryF(GL_TEXTURE_BUFFER_SIZE), static_cast(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(bufferBytes)); + EXPECT_EQ(levelQuery(GL_TEXTURE_WIDTH), static_cast(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(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 diff --git a/MobileGL/MG_IntegrationTest/Scenarios/DoublePrecisionScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/DoublePrecisionScenario.cpp index a59e3d69..682719e2 100644 --- a/MobileGL/MG_IntegrationTest/Scenarios/DoublePrecisionScenario.cpp +++ b/MobileGL/MG_IntegrationTest/Scenarios/DoublePrecisionScenario.cpp @@ -697,24 +697,127 @@ void main() { EXPECT_EQ(glGetError(), static_cast(GL_NO_ERROR)); } - TEST_F(DoublePrecisionScenario, A64BitVertexFormatIsDeclinedOnEveryBackend) { + TEST_F(DoublePrecisionScenario, A64BitVertexFormatIsRecordedAndItsArrayIsDroppedAtDraw) { if (!Ready()) return; // The demotion leaves no 64-bit shader input to feed, so there is nothing a 64-bit // vertex FETCH could be fetched into - on either backend, and no longer only on the - // ones whose device lacks shaderFloat64. Declined loudly rather than accepted and - // drawn as garbage; the matching POST row says the same thing at startup. + // ones whose device lacks shaderFloat64. + // + // What that costs is the ARRAY, not the CALL. GL 4.6 core 10.3.2 defines no error for + // a well-formed glVertexAttribLFormat and 64-bit attributes are core in the GL 4.3 + // context MobileGL advertises, so refusing the call would be non-conformant and would + // leave four pure state queries unanswerable + // (KHR-GL43.vertex_attrib_binding.basic-state1/3). The format is therefore recorded and + // queryable; the enabled array is what gets dropped, and the attribute then reads its + // generic current value. The matching POST row says exactly that at startup. GLuint vao = 0; glGenVertexArrays(1, &vao); glBindVertexArray(vao); while (glGetError() != GL_NO_ERROR) {} - glVertexAttribLFormat(0, 3, GL_DOUBLE, 0); - EXPECT_EQ(glGetError(), static_cast(GL_INVALID_OPERATION)); + glVertexAttribLFormat(1, 3, GL_DOUBLE, 8); + EXPECT_EQ(glGetError(), static_cast(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(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(GL_NO_ERROR)); glBindVertexArray(0); glDeleteVertexArrays(1, &vao); while (glGetError() != GL_NO_ERROR) {} } + // The consequence of recording the state rather than refusing the call: a 64-bit array can + // now be ENABLED in a VAO that a draw uses, which it never could before. That must not + // take the draw down. Leaving such an array enabled with no pointer behind it is exactly + // the documented Adreno null-deref (SIGSEGV inside the next glDraw*), so DirectGLES + // disables it before glVertexAttribPointer can ever see GL_DOUBLE, and DirectVulkan maps + // the format to VK_FORMAT_UNDEFINED so it never enters the pipeline's vertex input state. + // + // The shader deliberately does NOT read location 1: that keeps the two backends on the + // same path (DirectVulkan declines a draw whose SHADER reads an unsupported enabled array, + // by design and loudly, which is a different assertion from this one) and it is the shape + // the crash needed - an enabled array nothing set a pointer for. + TEST_F(DoublePrecisionScenario, AnEnabledLongArrayDoesNotBreakADrawThatIgnoresIt) { + if (!Ready()) return; + + constexpr const char* kVs = R"(#version 430 core +layout(location = 0) in vec2 aPos; +void main() { gl_Position = vec4(aPos, 0.0, 1.0); } +)"; + constexpr const char* kFs = R"(#version 430 core +out vec4 o_color; +void main() { o_color = vec4(0.0, 1.0, 0.0, 1.0); } +)"; + std::string error; + const unsigned int program = CompileProgram(kVs, kFs, &error); + ASSERT_NE(program, 0u) << error; + + ColorFbo target = MakeColorFbo(32, 32); + ASSERT_NE(target.fbo, 0u) << "could not create the render target"; + BindFbo(target); + + const float positions[8] = {-1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f}; + const double doubles[4] = {1.0, 2.0, 3.0, 4.0}; + + GLuint vao = 0; + GLuint positionBuffer = 0; + GLuint doubleBuffer = 0; + glGenVertexArrays(1, &vao); + glBindVertexArray(vao); + glGenBuffers(1, &positionBuffer); + glBindBuffer(GL_ARRAY_BUFFER, positionBuffer); + glBufferData(GL_ARRAY_BUFFER, sizeof(positions), positions, GL_STATIC_DRAW); + glGenBuffers(1, &doubleBuffer); + glBindBuffer(GL_ARRAY_BUFFER, doubleBuffer); + glBufferData(GL_ARRAY_BUFFER, sizeof(doubles), doubles, GL_STATIC_DRAW); + glBindBuffer(GL_ARRAY_BUFFER, 0); + + glVertexAttribFormat(0, 2, GL_FLOAT, GL_FALSE, 0); + glVertexAttribBinding(0, 0); + glBindVertexBuffer(0, positionBuffer, 0, static_cast(2 * sizeof(float))); + glEnableVertexAttribArray(0); + + glVertexAttribLFormat(1, 1, GL_DOUBLE, 0); + glVertexAttribBinding(1, 1); + glBindVertexBuffer(1, doubleBuffer, 0, static_cast(sizeof(double))); + glEnableVertexAttribArray(1); + EXPECT_EQ(FirstGLError(), 0u) << "setting up the 64-bit array was refused"; + + ClearTo(0.0f, 0.0f, 0.0f, 1.0f); + glUseProgram(program); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + EXPECT_EQ(FirstGLError(), 0u) << "a draw with an enabled 64-bit array must not raise an error"; + + const Image image = ReadPixels(target.width, target.height); + ASSERT_FALSE(image.Empty()); + EXPECT_GT(image.At(target.width / 2, target.height / 2).g, 200) + << "the draw did not happen; the enabled 64-bit array must be dropped, not fatal"; + + glDisableVertexAttribArray(0); + glDisableVertexAttribArray(1); + glBindVertexArray(0); + glDeleteVertexArrays(1, &vao); + glDeleteBuffers(1, &positionBuffer); + glDeleteBuffers(1, &doubleBuffer); + BindDefaultFramebuffer(); + DestroyColorFbo(target); + glUseProgram(0); + glDeleteProgram(program); + EXPECT_EQ(FirstGLError(), 0u); + } + } // namespace } // namespace MGITest diff --git a/MobileGL/MG_IntegrationTest/Scenarios/ImageSizeAfterRespecScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/ImageSizeAfterRespecScenario.cpp new file mode 100644 index 00000000..77ca8922 --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Scenarios/ImageSizeAfterRespecScenario.cpp @@ -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 +#include + +#include "../Harness/HeadlessGL.h" +#include "../Harness/ScenarioFixture.h" + +#ifdef GLAPI +#undef GLAPI +#endif +#define GL_GLEXT_PROTOTYPES +#include +#include +#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 (¢re)[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 pixels(static_cast(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(kTargetSize / 2) * kTargetSize + kTargetSize / 2) * 4; + for (int i = 0; i < 4; ++i) { + centre[i] = pixels[offset + static_cast(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(centre[0]), 0) << "the FIRST draw already disagrees about imageSize(): got (" + << static_cast(centre[0]) << ", " + << static_cast(centre[1]) << ", " + << static_cast(centre[2]) << ")"; + EXPECT_EQ(static_cast(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(centre[0]), 0) + << "after the re-spec the draw still sees the OLD image size; centre pixel was (" + << static_cast(centre[0]) << ", " << static_cast(centre[1]) << ", " + << static_cast(centre[2]) << ")"; + EXPECT_EQ(static_cast(centre[1]), 255); + } + +} // namespace MGITest diff --git a/MobileGL/MG_IntegrationTest/Scenarios/LayeredTextureReadbackScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/LayeredTextureReadbackScenario.cpp new file mode 100644 index 00000000..51dd9405 --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Scenarios/LayeredTextureReadbackScenario.cpp @@ -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 +#include +#include + +#include "../Harness/HeadlessGL.h" +#include "../Harness/ScenarioFixture.h" + +#ifdef GLAPI +#undef GLAPI +#endif +#define GL_GLEXT_PROTOTYPES +#include +#include +#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(layer) * 100u + static_cast(x); + } + + GLuint ExpectedCubeArrayTexel(int x, int y, int layerFace) { + return 1000u + static_cast(layerFace) * 100u + static_cast(y) * 10u + + static_cast(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(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(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 m_programs; + std::vector 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 seed(static_cast(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 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(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 seed(static_cast(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 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(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 diff --git a/MobileGL/MG_State/GLState/Core.h b/MobileGL/MG_State/GLState/Core.h index b72c899d..70c2a5e9 100644 --- a/MobileGL/MG_State/GLState/Core.h +++ b/MobileGL/MG_State/GLState/Core.h @@ -368,6 +368,31 @@ namespace MobileGL { return m_transformFeedbackGeometryCaptureDraws; } + // Conditional rendering (GL 4.6 core 10.9). `discard` is the verdict already + // resolved from the query object at glBeginConditionalRender - the predicate is + // read ONCE there, not per command, because GL specifies the block against the + // result available at Begin and re-reading it would let a query that is still + // being written change the answer mid-block. + void BeginConditionalRender(GLuint queryId, GLenum mode, Bool discard) { + m_conditionalRenderActive = true; + m_conditionalRenderQuery = queryId; + m_conditionalRenderMode = mode; + m_conditionalRenderDiscards = discard; + } + void EndConditionalRender() { + m_conditionalRenderActive = false; + m_conditionalRenderQuery = 0; + m_conditionalRenderMode = GL_NONE; + m_conditionalRenderDiscards = false; + } + Bool IsConditionalRenderActive() const { return m_conditionalRenderActive; } + GLuint GetConditionalRenderQuery() const { return m_conditionalRenderQuery; } + // Whether the commands GL 4.6 core 10.9 makes conditional are being discarded + // right now. False whenever no block is open, so a caller needs no second test. + Bool ConditionalRenderDiscardsCommands() const { + return m_conditionalRenderActive && m_conditionalRenderDiscards; + } + // Transform feedback objects (ARB_transform_feedback2 / GL 4.0 core). // The capture state above and the indexed GL_TRANSFORM_FEEDBACK_BUFFER // binding points are object state, but the context keeps exactly one live @@ -466,6 +491,13 @@ namespace MobileGL { Uint64 m_transformFeedbackAccountedCaptureDraws = 0; Uint64 m_transformFeedbackGeometryCaptureDraws = 0; + // Conditional rendering. Context state, not object state: GL 4.6 core 10.9 allows + // exactly one block open at a time and no object owns it. + Bool m_conditionalRenderActive = false; + Bool m_conditionalRenderDiscards = false; + GLuint m_conditionalRenderQuery = 0; + GLenum m_conditionalRenderMode = GL_NONE; + // Everything a transform feedback object owns while it is NOT the bound one. struct TransformFeedbackObjectState { struct SavedBufferBinding { diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp b/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp index 9194d3ea..071a4846 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp @@ -63,6 +63,180 @@ namespace { return element; } + // GL 4.6 core 7.7 / ARB_shader_atomic_counters: within one binding no two atomic counters + // may occupy the same bytes, every offset is a multiple of 4, and no counter may reach past + // GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE. glslang enforces all three in fixOffset(), which the + // Vulkan-relaxed parse never reaches - vkRelaxedRemapUniformVariable folds the atomic_uint + // into a synthesized storage block and returns from declareVariable() before fixOffset() + // runs, clearing explicitOffset on the way ("xxTODO: use logic from fixOffset()"). Two + // counters declared at the same binding AND the same offset therefore linked cleanly. + // + // The offsets themselves survive that lowering (reflection and the SPIR-V generator both + // honour layoutOffset), so the check belongs here, over the same model the GL queries answer + // from. Returns the info-log line for an illegal layout, empty for a legal one. + static MobileGL::String ValidateAtomicCounterLayout(glslang::TProgram& reflection) { + using MobileGL::Bool; + using MobileGL::Int; + using MobileGL::SizeT; + using MobileGL::String; + using MobileGL::Vector; + namespace Transpiler = MobileGL::MG_Util::ShaderTranspiler; + + const Int blockCount = reflection.getNumUniformBlocks(); + if (blockCount <= 0) return {}; + const SizeT prefixLength = std::strlen(Transpiler::ATOMIC_COUNTER_BLOCK_PREFIX); + Vector isCounterBlock(static_cast(blockCount), false); + Bool anyCounterBlock = false; + for (Int i = 0; i < blockCount; ++i) { + const auto& block = reflection.getUniformBlock(i); + isCounterBlock[static_cast(i)] = + block.name.compare(0, prefixLength, Transpiler::ATOMIC_COUNTER_BLOCK_PREFIX) == 0; + anyCounterBlock = anyCounterBlock || isCounterBlock[static_cast(i)]; + } + if (!anyCounterBlock) return {}; // every program that declares no atomic counter + + struct CounterSpan { + Int offset = 0; + Int size = 0; + String name; + }; + Vector> spansByBlock(static_cast(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(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(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(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> 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* use = nullptr; + for (SizeT stage = 0; stage < kStageCount; ++stage) { + if ((static_cast(uniform.stages) & (1u << static_cast(kStages[stage]))) == 0) { + continue; + } + // The one insert this uniform performs, so the reference survives the rest of the + // stage loop - a flat hash map relocates on insert, never on read. + if (use == nullptr) { + use = &useByName[StripArrayElementSuffix(uniform.name)]; + } + ++(*use)[stage].entries; + (*use)[stage].declared = std::max((*use)[stage].declared, elements); + } + } + + Int counts[kStageCount] = {}; + for (const auto& entry : useByName) { + for (SizeT stage = 0; stage < kStageCount; ++stage) { + counts[stage] += std::max(entry.second[stage].entries, entry.second[stage].declared); + } + } + + Int combined = 0; + for (SizeT stage = 0; stage < kStageCount; ++stage) { + combined += counts[stage]; + if (counts[stage] > limits[stage]) { + return std::format("This program uses {} active image uniforms in one stage, more than the {} " + "{} allows.", + counts[stage], limits[stage], kLimitNames[stage]); + } + } + if (combined > env.params.MaxCombinedImageUniforms) { + return std::format("This program uses {} active image uniforms across its stages, more than the {} " + "GL_MAX_COMBINED_IMAGE_UNIFORMS allows.", + combined, env.params.MaxCombinedImageUniforms); + } + return {}; + } + static bool IsBuiltInPipelineOutput(const glslang::TObjectReflection& output) { const auto* type = output.getType(); return type && type->getQualifier().builtIn != glslang::EbvNone; @@ -606,6 +780,22 @@ namespace MobileGL::MG_State::GLState { return false; } + if (String atomicCounterError = ValidateAtomicCounterLayout(*artifacts.program); + !atomicCounterError.empty()) { + artifacts.infoLog = Move(atomicCounterError); + DeferLog(std::format("ProgramObject {}: Link failed - {}", in.externalIndex, artifacts.infoLog)); + ProgramObject::ResetLinkArtifacts(artifacts); + return false; + } + + if (String imageUniformError = ValidateImageUniformLimits(*artifacts.program, env); + !imageUniformError.empty()) { + artifacts.infoLog = Move(imageUniformError); + DeferLog(std::format("ProgramObject {}: Link failed - {}", in.externalIndex, artifacts.infoLog)); + ProgramObject::ResetLinkArtifacts(artifacts); + return false; + } + // ---------- GL-facing index spaces (relaxed-parse cleanup) ---------- // Blocks first: global-UBO membership drives the uniform filter below. The // synthesized MGL_GLOBAL_UBO is a transpiler artifact - its members are GL @@ -658,7 +848,16 @@ namespace MobileGL::MG_State::GLState { // MGL_GLOBAL_UBO, so reflection cannot provide them ("source-explicit"); // - glslang's layoutLocation() for opaque uniforms, where the qualifier // survives the relaxed parse (and mapIO auto-assigns the rest). - constexpr Uint kNoLocation = glslang::TQualifier::layoutLocationEnd; + // + // "no effective location yet". Deliberately OUTSIDE the location space rather than + // glslang::TQualifier::layoutLocationEnd, which is the first location past the pool and + // therefore only one off a legal one - a sentinel that sits at the boundary it guards has + // to be re-proved safe every time the ceiling moves, and glslang uses that same value for + // "this opaque uniform has no location" as well. + constexpr Uint kNoLocation = ~static_cast(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(ProgramObject::MAX_UNIFORM_LOCATIONS); Vector effectiveLocation(tProgramUniformCount, kNoLocation); Vector locationIsSourceExplicit(tProgramUniformCount, false); UnorderedMap structExplicitCursor; // declared root -> next member location @@ -695,13 +894,19 @@ namespace MobileGL::MG_State::GLState { cursor->second += static_cast(GetUniformLocationSpan(uniform)); } } - if (effectiveLocation[i] == kNoLocation && type != nullptr && type->isOpaque()) { + // glslang parks "no location" at layoutLocationEnd, which is a real location in this + // table's numbering - test for it explicitly rather than letting it through as one. + if (effectiveLocation[i] == kNoLocation && type != nullptr && type->isOpaque() && + uniform.layoutLocation() != glslang::TQualifier::layoutLocationEnd) { effectiveLocation[i] = uniform.layoutLocation(); } if (locationIsSourceExplicit[i] && - effectiveLocation[i] + static_cast(GetUniformLocationSpan(uniform)) > kNoLocation) { + effectiveLocation[i] + static_cast(GetUniformLocationSpan(uniform)) > kMaxUniformLocations) { // Config A rejected out-of-range explicit locations at parse; keep them - // from growing the location table unboundedly. + // from growing the location table unboundedly. Stated against the advertised + // GL_MAX_UNIFORM_LOCATIONS, because that is the rule being enforced (GL 4.6 core + // 7.6.1): an array whose LAST element passes the ceiling is a link error even + // though its base compiled fine. artifacts.infoLog = std::format("Uniform '{}' explicit location {} is out of range.", uniform.name, effectiveLocation[i]); ProgramObject::ResetLinkArtifacts(artifacts); @@ -709,12 +914,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> 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(*explicitLocation); + const Int locationSpan = GetUniformLocationSpan(uniform); + if (location + static_cast(locationSpan) > kMaxUniformLocations) { + artifacts.infoLog = std::format("Uniform '{}' explicit location {} is out of range.", uniform.name, + location); + ProgramObject::ResetLinkArtifacts(artifacts); + return false; + } + deadExplicitReservations.emplace_back(location, locationSpan); + deadReservedLocationCount += locationSpan; + artifacts.maxUniformLocation = std::max(artifacts.maxUniformLocation, location + locationSpan - 1); + MGLOG_D("ProgramObject %u: Reflection - inactive uniform '%s' reserves locations %u..%u without " + "becoming GL-visible", + in.externalIndex, uniform.name.c_str(), location, location + locationSpan - 1); + } + + Int requiredUniformLocations = deadReservedLocationCount; + // The same count restricted to DEFAULT-BLOCK uniforms, which is the only thing + // GL_MAX_UNIFORM_LOCATIONS bounds. requiredUniformLocations cannot serve: it also carries + // named-block members, which take a slot in this allocator's table (an implementation + // detail) but consume no GL uniform location at all, so a big UBO array would otherwise + // fail a link the spec allows. + Int defaultBlockLocationDemand = deadReservedLocationCount; for (const Int i : artifacts.glUniformIndexToTProgram) { auto& uniform = artifacts.program->getUniform(i); const Uint location = effectiveLocation[i]; const Int locationSpan = GetUniformLocationSpan(uniform); requiredUniformLocations += locationSpan; + const Bool inNamedBlock = uniform.index >= 0 && !isGlobalUboMember(uniform); + if (!inNamedBlock) defaultBlockLocationDemand += locationSpan; if (location != kNoLocation) { artifacts.maxUniformLocation = std::max(artifacts.maxUniformLocation, location + locationSpan - 1); } @@ -727,6 +975,22 @@ namespace MobileGL::MG_State::GLState { MGLOG_D("ProgramObject %u: Reflection - computed maxUniformLocation=%u uniformNameMaxLength=%d", in.externalIndex, artifacts.maxUniformLocation, artifacts.uniformNameMaxLength); + // GL 4.6 core 7.6.1: explicit, implicit and reserved-but-inactive default-block uniforms + // all draw from the one GL_MAX_UNIFORM_LOCATIONS pool, and a program asking for more than + // the implementation advertises FAILS TO LINK + // (KHR-GL43.explicit_uniform_location.uniform-loc-negative-link-max-num-of-locations). + // A single uniform whose own span passes the ceiling was already rejected above; this is + // the aggregate half of the same rule. + if (defaultBlockLocationDemand > static_cast(kMaxUniformLocations)) { + artifacts.infoLog = + std::format("Uniform locations exhausted: the default-block uniforms need {} locations but " + "GL_MAX_UNIFORM_LOCATIONS is {}.", + defaultBlockLocationDemand, kMaxUniformLocations); + DeferLog(std::format("ProgramObject {}: Link failed - {}", in.externalIndex, artifacts.infoLog)); + ProgramObject::ResetLinkArtifacts(artifacts); + return false; + } + if (artifacts.maxUniformLocation + 1 < requiredUniformLocations) { MGLOG_D("ProgramObject %u: Reflection - maxUniformLocation+1 (%u) < requiredUniformLocations (%d), " "adjusting", @@ -741,6 +1005,27 @@ namespace MobileGL::MG_State::GLState { glslang::TQualifier::layoutLocationEnd); artifacts.uniformSamplerOrImageUnitIndex.resize(artifacts.maxUniformLocation + 1, -1); + // Occupancy for the inactive explicit uniforms collected above: a set bit means "the + // source claimed this location", which is enough to keep the two implicit passes off it + // without making the location reachable through any GL entry point. A location the + // fallback grow path mints later is past this bitset by construction (every reservation + // was folded into maxUniformLocation before the table was sized), so the lookup treats + // out-of-range as free rather than resizing in lockstep. + // Left empty - and unallocated - when nothing reserved anything, which is every program in + // the shader-pack corpus; the lookup below reads an empty bitset as "nothing is reserved". + Vector 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 unallocatedUniformIndex; // Pass 1: source-explicit locations. These are API contract @@ -785,7 +1070,8 @@ namespace MobileGL::MG_State::GLState { Bool spanIsFree = location + locationSpan - 1 <= artifacts.maxUniformLocation; for (Int element = 0; spanIsFree && element < locationSpan; ++element) { spanIsFree = - artifacts.uniformIndexInTProgram[location + element] == glslang::TQualifier::layoutLocationEnd; + artifacts.uniformIndexInTProgram[location + element] == glslang::TQualifier::layoutLocationEnd && + !locationIsReserved(location + element); } if (!spanIsFree) { artifacts.uniformLocations[uniform.name] = kNoLocation; @@ -817,7 +1103,8 @@ namespace MobileGL::MG_State::GLState { bool hasRoom = locNeedle + locationSpan - 1 <= artifacts.maxUniformLocation; for (Int element = 0; hasRoom && element < locationSpan; ++element) { hasRoom = artifacts.uniformIndexInTProgram[locNeedle + element] == - glslang::TQualifier::layoutLocationEnd; + glslang::TQualifier::layoutLocationEnd && + !locationIsReserved(locNeedle + element); } if (!hasRoom) continue; // Found a vacant location at locNeedle diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h index a6cec261..2271b8b4 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h @@ -24,6 +24,20 @@ namespace MobileGL::MG_State::GLState { class ProgramObject { public: + // GL_MAX_UNIFORM_LOCATIONS: locations 0 .. MAX_UNIFORM_LOCATIONS-1 are the whole legal + // range (GL 4.6 core 7.6.1 / ARB_explicit_uniform_location). Shared with GL_Getter rather + // than spelled twice, because the link and the query must agree exactly - the CTS declares + // a uniform at the advertised value minus one and expects it to link + // (KHR-GL43.explicit_uniform_location.uniform-loc-max). + // + // Tied to glslang's own ceiling and NOT raisable past it: ParseHelper rejects + // `layout(location = N)` for N >= TQualifier::layoutLocationEnd at COMPILE time, so + // layoutLocationEnd - 1 is the largest location any shader in this stack can declare - + // which makes exactly layoutLocationEnd locations, 0 .. layoutLocationEnd - 1, the pool. + // Advertising more would promise a location no shader could name. Comfortably above the + // 1024 GL 4.3 requires. + static constexpr Int MAX_UNIFORM_LOCATIONS = static_cast(glslang::TQualifier::layoutLocationEnd); + ProgramObject(Uint externalIndex) : m_externalIndex(externalIndex), m_lifetimeId(AllocateLifetimeId()) {} // Cancel-not-join, exactly like ~ShaderObject: the link job owns its inputs, so an // in-flight link whose program just went away is safe to abandon where it stands. @@ -702,10 +716,6 @@ namespace MobileGL::MG_State::GLState { // SIGSEGV inside glslang::TProgram::getNumPipeInputs - KHR-GL30.api.coverage does exactly // this after a failed glGetAttribLocation, and reached it as soon as the CopyTexImage2D // throw ahead of it stopped killing the run first. - Int GetActiveAtomicCounterCount() const { - const auto& program = Artifacts().program; - return program ? program->getNumAtomicCounters() : 0; - } Int GetActiveAttributesCount() const { const auto& program = Artifacts().program; return program ? program->getNumPipeInputs() : 0; diff --git a/MobileGL/MG_State/GLState/ProgramState/ShaderCompileTask.cpp b/MobileGL/MG_State/GLState/ProgramState/ShaderCompileTask.cpp index 70165f11..41fdeb37 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ShaderCompileTask.cpp +++ b/MobileGL/MG_State/GLState/ProgramState/ShaderCompileTask.cpp @@ -8,6 +8,7 @@ #include "ShaderCompileTask.h" +#include #include #include #include @@ -15,6 +16,7 @@ #include +#include #include namespace { @@ -137,8 +139,21 @@ namespace { return std::nullopt; } + // What glGetIntegerv(GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS) answers, recomputed rather than + // queried: the compile runs on a worker with no context, and the pname is not a plain backend + // parameter - the getter caps the backend's count by the state layer's fixed binding-point + // array (GL_Getter's GetIndexedBufferQueryPointCount). A shader must be judged against the + // number the application was told, not against either half of it. + static MobileGL::Int MaxShaderStorageBufferBindings( + const MobileGL::MG_Util::ShaderTranspiler::CompileEnv& env) { + const MobileGL::Int frontendPoints = + static_cast(MobileGL::MG_State::GLState::BufferBindingPointCount); + if (!env.HasBackend()) return frontendPoints; + return std::min(frontendPoints, std::max(env.params.MaxShaderStorageBufferBindings, 0)); + } + // The half of a compile that depends on nothing but the source text, the stage and the - // environment snapshot: preprocessing, the two lexical rejections, and the two lexical + // environment snapshot: preprocessing, the three lexical rejections, and the two lexical // side-channel extractions. Split out so P0b layer 2 can memoize exactly this and // nothing else - the glslang parse stays per-object because its TShader is consume-once. // Deliberately free of any per-object state so the memo is sound. @@ -172,6 +187,13 @@ namespace { return result; } + if (const std::optional bindingError = FindShaderStorageBindingViolation( + result.preprocessedSource, MaxShaderStorageBufferBindings(env))) { + result.outcome = ShaderPreprocessOutcome::ResourceBindingRejected; + result.infoLog = *bindingError; + return result; + } + // The parse this feeds runs in the link-compatible configuration (Vulkan-client // env with relaxed rules): the TShader it produces is what glLinkProgram links and // what the backends' SPIR-V is generated from - there is no second, GL-client diff --git a/MobileGL/MG_State/GLState/ProgramState/ShaderPreprocessCache.h b/MobileGL/MG_State/GLState/ProgramState/ShaderPreprocessCache.h index 6a5c8416..238ff7ba 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ShaderPreprocessCache.h +++ b/MobileGL/MG_State/GLState/ProgramState/ShaderPreprocessCache.h @@ -26,6 +26,9 @@ namespace MobileGL::MG_State::GLState { ComputeLocalSizeRejected, // FindReservedIdentifierViolation rejected it. ReservedIdentifierRejected, + // FindShaderStorageBindingViolation rejected it: a storage block declared a binding at or + // past GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS. + ResourceBindingRejected, // The source-only half was clean but glslang rejected the preprocessed source. // Memoizing this saves the parse itself on every later object with that source. ParseFailed, diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp b/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp index 86c1a4a2..55f0695a 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp @@ -250,6 +250,10 @@ namespace MobileGL { return m_contentVersion; } + Uint64 TextureObjectBase::GetShapeVersion() const { + return m_shapeVersion; + } + Bool TextureObjectBase::IsMipmapCompleteForFilterCached(Bool mipmapped) const { const int slot = mipmapped ? 1 : 0; if (m_completeMemoShapeVersion[slot] == m_shapeVersion) { diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObject.h b/MobileGL/MG_State/GLState/TextureState/TextureObject.h index 742e5e78..1f214450 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject.h +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject.h @@ -55,6 +55,12 @@ namespace MobileGL::MG_State::GLState { // Backends compare it against a per-resource snapshot to skip re-syncing unchanged // textures across draws (e.g. the block atlas bound across a whole terrain batch). virtual Uint64 GetContentVersion() const = 0; + // Monotonic counter bumped on every SHAPE mutation - level sizes, the stored level + // set, the internal format, the level range (see BumpShapeVersion). Disjoint from the + // content version on purpose: glTexImage2D(..., nullptr) re-specifies a level's size + // without dirtying a single texel, so a backend that keys its "nothing changed since + // the last sync" skip on content alone keeps a resource of the OLD size alive. + virtual Uint64 GetShapeVersion() const = 0; // Answers IsMipmapCompleteForFilter() from a memo. Sampling completeness is a // property of the texture's SHAPE - level sizes, level count, level range, // internal format - and never of its texel content, but every draw asks about @@ -106,6 +112,7 @@ namespace MobileGL::MG_State::GLState { void SetImmutableLevels(Uint levels) override; Uint16 GetTextureParamsVersion() const override; Uint64 GetContentVersion() const override; + Uint64 GetShapeVersion() const override; Bool IsMipmapCompleteForFilterCached(Bool mipmapped) const override; // Bumps the content version without touching per-level storage-dirty flags. Used when the // set of defined mip levels grows via GPU-side mip generation (glGenerateMipmap): the level diff --git a/MobileGL/MG_Test/Backend/DirectGLES/EsslShaderPassTest.cpp b/MobileGL/MG_Test/Backend/DirectGLES/EsslShaderPassTest.cpp index 6ce2facd..15d4b524 100644 --- a/MobileGL/MG_Test/Backend/DirectGLES/EsslShaderPassTest.cpp +++ b/MobileGL/MG_Test/Backend/DirectGLES/EsslShaderPassTest.cpp @@ -59,9 +59,11 @@ void main() const String out = SplitReadWriteImageUniforms(source); // Both halves: same binding, same format, same type - which is what makes two image - // variables on one image unit legal. - EXPECT_TRUE(Contains(out, "layout(binding = 2, rgba8) uniform readonly highp image2D goku;")); - EXPECT_TRUE(Contains(out, "layout(binding = 2, rgba8) uniform writeonly highp image2D " + WriteAlias("goku") + ";")); + // variables on one image unit legal - and both `coherent`, which is what makes the store + // through one of them visible to the load through the other. + EXPECT_TRUE(Contains(out, "layout(binding = 2, rgba8) uniform coherent readonly highp image2D goku;")); + EXPECT_TRUE(Contains( + out, "layout(binding = 2, rgba8) uniform coherent writeonly highp image2D " + WriteAlias("goku") + ";")); // The load keeps the original name, the store moves to the writeonly half. EXPECT_TRUE(Contains(out, "imageLoad(goku,")); @@ -152,9 +154,9 @@ void main() } )"; const String out = SplitReadWriteImageUniforms(source); - EXPECT_TRUE(Contains(out, "layout(binding = 6, rgba8) uniform readonly highp image2D gohan[3];")); - EXPECT_TRUE(Contains(out, - "layout(binding = 6, rgba8) uniform writeonly highp image2D " + WriteAlias("gohan") + "[3];")); + EXPECT_TRUE(Contains(out, "layout(binding = 6, rgba8) uniform coherent readonly highp image2D gohan[3];")); + EXPECT_TRUE(Contains( + out, "layout(binding = 6, rgba8) uniform coherent writeonly highp image2D " + WriteAlias("gohan") + "[3];")); EXPECT_TRUE(Contains(out, "imageStore(" + WriteAlias("gohan") + "[1],")); EXPECT_TRUE(Contains(out, "imageLoad(gohan[2],")); } @@ -174,9 +176,11 @@ void main() )"; const String out = SplitReadWriteImageUniforms(source); - // goku is read+write -> split; goku_hd is write-only -> qualified in place, not split. - EXPECT_TRUE(Contains(out, "layout(binding = 1, rgba8) uniform readonly highp image2D goku;")); - EXPECT_TRUE(Contains(out, "layout(binding = 1, rgba8) uniform writeonly highp image2D " + WriteAlias("goku") + ";")); + // goku is read+write -> split (and coherent with it); goku_hd is write-only -> qualified in + // place, not split, and left non-coherent because nothing aliases it. + EXPECT_TRUE(Contains(out, "layout(binding = 1, rgba8) uniform coherent readonly highp image2D goku;")); + EXPECT_TRUE(Contains( + out, "layout(binding = 1, rgba8) uniform coherent writeonly highp image2D " + WriteAlias("goku") + ";")); EXPECT_TRUE(Contains(out, "layout(binding = 2, rgba8) uniform writeonly highp image2D goku_hd;")); EXPECT_TRUE(Contains(out, "imageStore(goku_hd,")); EXPECT_FALSE(Contains(out, WriteAlias("goku") + "_hd")); @@ -197,6 +201,36 @@ void main() EXPECT_TRUE(Contains(out, "uniform readonly coherent restrict highp image2D goku;")); EXPECT_TRUE( Contains(out, "uniform writeonly coherent restrict highp image2D " + WriteAlias("goku") + ";")); + // ...and the coherent the split adds is not a SECOND one: a repeated memory qualifier is a + // compile error in ESSL, so the source's own has to be recognized. + EXPECT_EQ(CountOf(out, "coherent"), 2u); +} + +// The visibility half of the split, and the reason it is not cosmetic: GLSL orders a +// same-variable read-after-write within one invocation by construction, but once the store goes +// through `mg_imageWrite_goku` and the load through `goku` the two are DIFFERENT variables, and +// the ordering only holds if both are coherent. Desktop sources almost never say so - they had +// no reason to - which is how KHR-GL4x.shader_image_load_store.advanced-memory-order's +// store/load/compare loop started reading back the value it had not stored yet. +TEST(SplitReadWriteImageUniformsTest, SplitPairIsMadeCoherentEvenWhenTheSourceIsNot) { + const String source = R"(#version 320 es +layout(binding = 2, rgba8) uniform highp image2D goku; +layout(binding = 3, rgba8) uniform highp image2D storeOnly; +layout(location = 0) out highp vec4 mg_FragColor; +void main() +{ + imageStore(goku, ivec2(0), vec4(1.0)); + mg_FragColor = imageLoad(goku, ivec2(0)); + imageStore(storeOnly, ivec2(0), vec4(2.0)); +} +)"; + const String out = SplitReadWriteImageUniforms(source); + EXPECT_TRUE(Contains(out, "uniform coherent readonly highp image2D goku;")) << out; + EXPECT_TRUE(Contains(out, "uniform coherent writeonly highp image2D " + WriteAlias("goku") + ";")) << out; + // Exactly the two halves of the pair, and nothing else: the store-only image is repaired in + // place, has no alias to stay visible to, and must not pay for uncached access. + EXPECT_EQ(CountOf(out, "coherent"), 2u); + EXPECT_TRUE(Contains(out, "uniform writeonly highp image2D storeOnly;")) << out; } // imageSize reads no texels and writes none, so it decides nothing; readonly is what keeps diff --git a/MobileGL/MG_Test/Program/ProgramInterfaceTest.cpp b/MobileGL/MG_Test/Program/ProgramInterfaceTest.cpp index 01e0a1f5..16430327 100644 --- a/MobileGL/MG_Test/Program/ProgramInterfaceTest.cpp +++ b/MobileGL/MG_Test/Program/ProgramInterfaceTest.cpp @@ -716,6 +716,177 @@ void main() { EXPECT_EQ(TakeError(), GL_INVALID_ENUM); } + // Two counters that share a binding AND an offset must fail to link. glslang's own check + // lives in fixOffset(), which the Vulkan-relaxed parse never reaches - it folds the + // atomic_uint into a storage block and returns from declareVariable() first - so the pair + // used to link cleanly and then increment the same four bytes. + TEST_F(ProgramInterfaceTest, OverlappingAtomicCounterOffsetsFailToLink) { + const char* fs = R"(#version 430 +out vec4 color; +layout (binding = 0, offset = 0) uniform atomic_uint a; +layout (binding = 0, offset = 0) uniform atomic_uint b; +void main() { color = vec4(float(atomicCounterIncrement(a) + atomicCounterIncrement(b))); } +)"; + const GLuint p = MakeProgram(kSimpleVs, fs); + LinkProgram(p); + GLint status = -1; + GetProgramiv(p, GL_LINK_STATUS, &status); + EXPECT_EQ(status, GL_FALSE); + char log[4096] = ""; + GetProgramInfoLog(p, sizeof(log), nullptr, log); + EXPECT_NE(std::string(log).find("overlap"), std::string::npos) << "info log was: " << log; + ClearErrors(); + + // Distinct offsets at one binding, and the same offset at two different bindings, are + // both legal and must still link - a check keyed any wider would reject them. + const char* legalFs = R"(#version 430 +out vec4 color; +layout (binding = 0, offset = 0) uniform atomic_uint a; +layout (binding = 0, offset = 4) uniform atomic_uint b; +layout (binding = 1, offset = 0) uniform atomic_uint c; +void main() { + color = vec4(float(atomicCounterIncrement(a) + atomicCounterIncrement(b) + atomicCounterIncrement(c))); +} +)"; + const GLuint legal = MakeProgram(kSimpleVs, legalFs); + LinkProgram(legal); + ExpectLinked(legal); + ClearErrors(); + } + + // GL 4.6 core 7.6 fails the link when a stage's active image uniforms exceed + // GL_MAX_*_IMAGE_UNIFORMS, or when their sum exceeds GL_MAX_COMBINED_IMAGE_UNIFORMS. Nothing + // counted them - glslang keeps those numbers only so gl_Max*ImageUniforms can expand from + // them - so every deliberately-oversized program in + // KHR-GL4x.shader_image_load_store.uniform-limits linked cleanly and then rendered nothing. + // + // Sized off the ADVERTISED limits rather than a constant, because the numbers come from the + // active backend and the whole point of the check is that the two agree. + TEST_F(ProgramInterfaceTest, ImageUniformsOverAStageLimitFailToLink) { + GLint maxFragmentImages = 0; + GLint maxCombinedImages = 0; + GetIntegerv(GL_MAX_FRAGMENT_IMAGE_UNIFORMS, &maxFragmentImages); + GetIntegerv(GL_MAX_COMBINED_IMAGE_UNIFORMS, &maxCombinedImages); + ClearErrors(); + ASSERT_GT(maxFragmentImages, 0); + + // The fragment stage is compiled explicitly so a COMPILE failure can never be mistaken + // for the link failure under test. + const auto linkWithFragmentImages = [](GLint count) { + const std::string n = std::to_string(count); + const std::string source = std::string(R"(#version 430 +out vec4 color; +layout(r32i) uniform iimage2D u_image[)") + n + R"(]; +void main() { + int value = 1; + for (int i = 0; i < )" + n + R"(; ++i) { + value = imageAtomicAdd(u_image[i], ivec2(0), value); + } + color = vec4(float(value)); +} +)"; + const char* sourcePtr = source.c_str(); + const GLuint fs = CreateShader(GL_FRAGMENT_SHADER); + ShaderSource(fs, 1, &sourcePtr, nullptr); + CompileShader(fs); + GLint compiled = 0; + GetShaderiv(fs, GL_COMPILE_STATUS, &compiled); + EXPECT_EQ(compiled, GL_TRUE) << "the fragment stage with " << count << " image uniforms must compile"; + + const GLuint vs = CreateShader(GL_VERTEX_SHADER); + ShaderSource(vs, 1, &kSimpleVs, nullptr); + CompileShader(vs); + + const GLuint program = CreateProgram(); + AttachShader(program, vs); + AttachShader(program, fs); + LinkProgram(program); + return program; + }; + + const GLuint over = linkWithFragmentImages(maxFragmentImages + 1); + GLint status = -1; + GetProgramiv(over, GL_LINK_STATUS, &status); + EXPECT_EQ(status, GL_FALSE); + char log[4096] = ""; + GetProgramInfoLog(over, sizeof(log), nullptr, log); + EXPECT_NE(std::string(log).find("GL_MAX_FRAGMENT_IMAGE_UNIFORMS"), std::string::npos) + << "info log was: " << log; + ClearErrors(); + + // Exactly AT the limit is legal and must still link: the comparison is strictly + // greater-than, and the conformance suite's combined-stage subcase builds a program that + // fills every stage to its own limit and expects it to link whenever the combined limit + // can hold them. + if (maxFragmentImages <= maxCombinedImages) { + const GLuint atLimit = linkWithFragmentImages(maxFragmentImages); + ExpectLinked(atLimit); + ClearErrors(); + } + } + + // glGetProgramiv(GL_ACTIVE_ATOMIC_COUNTER_BUFFERS) and glGetActiveAtomicCounterBufferiv are + // the pre-4.3 spelling of the interface above, and the spec requires the two to agree. + // Neither did: the first counted glslang's atomic counter UNIFORMS - zero, because the + // relaxed parse folds every atomic_uint into a storage block before reflection runs - and + // the second was a stub that wrote nothing and raised nothing. + TEST_F(ProgramInterfaceTest, ActiveAtomicCounterBufferQueriesMatchTheInterface) { + const char* fs = R"(#version 430 +out vec4 color; +layout (binding = 1, offset = 0) uniform atomic_uint a; +layout (binding = 2, offset = 0) uniform atomic_uint b; +layout (binding = 2, offset = 4) uniform atomic_uint c; +void main() { + color = vec4(float(atomicCounterIncrement(a) + atomicCounterIncrement(b) + atomicCounterIncrement(c))); +} +)"; + const GLuint p = MakeProgram(kSimpleVs, fs); + LinkProgram(p); + ExpectLinked(p); + ClearErrors(); + + GLint bufferCount = -12345; + GetProgramiv(p, GL_ACTIVE_ATOMIC_COUNTER_BUFFERS, &bufferCount); + EXPECT_EQ(bufferCount, Interfaceiv(p, GL_ATOMIC_COUNTER_BUFFER, GL_ACTIVE_RESOURCES)); + ASSERT_EQ(bufferCount, 2); + + const auto activeBufferiv = [p](GLuint index, GLenum pname) { + GLint value = -12345; + GetActiveAtomicCounterBufferiv(p, index, pname, &value); + return value; + }; + for (GLuint index = 0; index < static_cast(bufferCount); ++index) { + const std::vector 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 expectedIndices = Props(p, GL_ATOMIC_COUNTER_BUFFER, index, {GL_ACTIVE_VARIABLES}); + ASSERT_FALSE(expectedIndices.empty()); + std::vector 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(bufferCount), GL_ATOMIC_COUNTER_BUFFER_BINDING, &sink); + EXPECT_EQ(TakeError(), GL_INVALID_VALUE); + EXPECT_EQ(sink, -12345) << "a rejected query must not write the caller's output"; + // The interface-query spelling of the same property is NOT accepted here. + GetActiveAtomicCounterBufferiv(p, 0, GL_BUFFER_BINDING, &sink); + EXPECT_EQ(TakeError(), GL_INVALID_ENUM); + EXPECT_EQ(sink, -12345); + } + // --------------------------------------------------------- transform-feedback ------ TEST_F(ProgramInterfaceTest, TransformFeedbackVaryingTypes) { const char* vs = R"(#version 430 diff --git a/MobileGL/MG_Test/Program/ProgramTest.cpp b/MobileGL/MG_Test/Program/ProgramTest.cpp index eeb37f11..faf5c84b 100644 --- a/MobileGL/MG_Test/Program/ProgramTest.cpp +++ b/MobileGL/MG_Test/Program/ProgramTest.cpp @@ -3239,3 +3239,76 @@ TEST_F(ProgramTest, CreateShaderAndCreateShaderProgramvReportTheRightErrorClasse EXPECT_NE(program, 0u); EXPECT_EQ(GetError(), GL_NO_ERROR); } + +// ARB_explicit_uniform_location / GL 4.6 core 7.6.1: a `layout(location = N)` uniform reserves N +// EVEN WHEN IT IS INACTIVE. Dead default-block uniforms are correctly filtered off the GL surface +// (glGetUniformLocation must answer -1 for them), but the implicit allocator used to walk straight +// over the location they claimed and hand it to a uniform that never asked for it +// (KHR-GL43.explicit_uniform_location.uniform-loc-mix-with-implicit3). +TEST_F(ProgramTest, InactiveExplicitUniformLocationIsStillReserved) { + const char* vsSource = R"(#version 430 core +layout(location = 2) uniform vec4 uDeadAtTwo; +uniform vec4 uA; +uniform vec4 uB; +uniform vec4 uC; +uniform vec4 uD; +void main() { gl_Position = uA + uB + uC + uD; } +)"; + const char* fsSource = R"(#version 430 core +out vec4 fragColor; +void main() { fragColor = vec4(1.0); } +)"; + const GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, vsSource); + const GLuint fs = CompileShaderChecked(GL_FRAGMENT_SHADER, fsSource); + const GLuint program = LinkVsFs(vs, fs, GL_TRUE); + + // Reserving a location must not resurrect the uniform: it is still inactive to GL. + EXPECT_EQ(GetUniformLocation(program, "uDeadAtTwo"), -1); + + for (const char* name : {"uA", "uB", "uC", "uD"}) { + const GLint location = GetUniformLocation(program, name); + EXPECT_GE(location, 0) << name << " lost its implicit location"; + EXPECT_NE(location, 2) << name << " was handed the location uDeadAtTwo reserved"; + } + EXPECT_EQ(GetError(), GL_NO_ERROR); +} + +// The GL_MAX_UNIFORM_LOCATIONS boundary, from both sides. MAX_UNIFORM_LOCATIONS - 1 is the LAST +// LEGAL location: it has to link and read back verbatim +// (KHR-GL43.explicit_uniform_location.uniform-loc-max), which is only true while the advertised +// value and what the link accepts are the SAME number - the getter used to advertise one more +// location than any shader could name. +// +// The over-the-ceiling half is asserted through an ARRAY, because that is the only spelling the +// link gets to judge: a bare `layout(location = MAX)` is already a compile error inside glslang +// ("location is too large"), while an array's base compiles fine and only its last element passes +// the ceiling (...uniform-loc-negative-link-max-num-of-locations). +TEST_F(ProgramTest, ExplicitUniformLocationsHonourMaxUniformLocations) { + GLint maxLocations = 0; + GetIntegerv(GL_MAX_UNIFORM_LOCATIONS, &maxLocations); + ASSERT_GE(maxLocations, 1024) << "GL 4.3 requires at least 1024 uniform locations"; + + const char* fsSource = R"(#version 430 core +out vec4 fragColor; +void main() { fragColor = vec4(1.0); } +)"; + const GLuint fs = CompileShaderChecked(GL_FRAGMENT_SHADER, fsSource); + + { + const String source = String("#version 430 core\nlayout(location = ") + + std::to_string(maxLocations - 1) + + ") uniform vec4 uAtLimit;\nvoid main() { gl_Position = uAtLimit; }\n"; + const GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, source.c_str()); + const GLuint program = LinkVsFs(vs, fs, GL_TRUE); + EXPECT_EQ(GetUniformLocation(program, "uAtLimit"), maxLocations - 1) + << "the last location in the pool is legal and must come back verbatim"; + } + { + const String source = String("#version 430 core\nlayout(location = ") + + std::to_string(maxLocations - 4) + + ") uniform vec4 uSpill[8];\nvoid main() { gl_Position = uSpill[0]; }\n"; + const GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, source.c_str()); + (void)LinkVsFs(vs, fs, GL_FALSE); + } + EXPECT_EQ(GetError(), GL_NO_ERROR); +} diff --git a/MobileGL/MG_Test/Program/ProgramUtilTest.cpp b/MobileGL/MG_Test/Program/ProgramUtilTest.cpp index 344d9d17..8c82509e 100644 --- a/MobileGL/MG_Test/Program/ProgramUtilTest.cpp +++ b/MobileGL/MG_Test/Program/ProgramUtilTest.cpp @@ -3840,3 +3840,228 @@ TEST_F(ProgramUtilTest, EsslCoreImageFormatSetIsTheThirteenTheSpecLists) { EXPECT_FALSE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(0x8051 /*GL_RGB8*/)); EXPECT_FALSE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(0 /*GL_NONE*/)); } + +// KHR-GL43.shader_storage_buffer_object.basic-syntax iteration 6. glslang assigns a block's member +// offsets at DECLARATION time, where a member array that is still unsized contributes zero bytes - +// so `vec4 position01[]; vec4 position2;` put both members at offset 0 and the shader read +// position01[0] where it asked for position2. The preprocessor sizes the non-final member from the +// largest constant index the source uses, which is what the language says it means. +TEST_F(ProgramUtilTest, ANonFinalUnsizedBufferBlockMemberIsSizedFromItsLargestConstantIndex) { + using namespace MG_Util::ShaderTranspiler; + + String source = R"(#version 430 core +layout(packed) coherent buffer Buffer { + vec4 position01[]; + vec4 position2; +} g_buffer; +void main() { + if (gl_VertexID == 0) gl_Position = g_buffer.position01[0]; + else if (gl_VertexID == 1) gl_Position = g_buffer.position01[1]; + else if (gl_VertexID == 2) gl_Position = g_buffer.position2; +} +)"; + PreprocessShaderSource(ShaderStage::Vertex, source); + EXPECT_NE(source.find("vec4 position01[2];"), String::npos) << source; + EXPECT_EQ(source.find("position01[];"), String::npos) << source; + + // The LAST member of a storage block is a run-time sized array, which is legal and already + // laid out correctly - sizing it would be a wire-format change, not a repair. + String lastMember = R"(#version 430 core +buffer Buffer { + vec4 head; + vec4 tail[]; +} g_buffer; +void main() { + gl_Position = g_buffer.tail[0] + g_buffer.tail[3]; +} +)"; + PreprocessShaderSource(ShaderStage::Vertex, lastMember); + EXPECT_NE(lastMember.find("vec4 tail[];"), String::npos) << lastMember; + + // A member the shader subscripts with anything but a literal cannot be sized from the source, + // so it is left exactly as it was. + String dynamicIndex = R"(#version 430 core +buffer Buffer { + vec4 head[]; + vec4 tail; +} g_buffer; +uniform int g_index; +void main() { + gl_Position = g_buffer.head[g_index] + g_buffer.tail; +} +)"; + PreprocessShaderSource(ShaderStage::Vertex, dynamicIndex); + EXPECT_NE(dynamicIndex.find("vec4 head[];"), String::npos) << dynamicIndex; + + // `buffer` is also a member memory qualifier; a declaration that uses it must not be mistaken + // for a block header. + String memberQualifier = R"(#version 430 core +coherent buffer Buffer { + buffer vec4 position0; + vec4 position1[]; + vec4 position2; +} g_buffer; +void main() { + gl_Position = g_buffer.position0 + g_buffer.position1[2] + g_buffer.position2; +} +)"; + PreprocessShaderSource(ShaderStage::Vertex, memberQualifier); + EXPECT_NE(memberQualifier.find("vec4 position1[3];"), String::npos) << memberQualifier; +} + +// KHR-GL43.shader_storage_buffer_object.negative-glsl-compileTime: a storage block declared at +// GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS must fail to compile, and so must an arrayed one whose +// LAST element passes the ceiling. The relaxed Vulkan-rules parse enforces neither. +TEST_F(ProgramUtilTest, StorageBlockBindingCeilingIsCheckedAtItsExactBoundary) { + using namespace MG_Util::ShaderTranspiler; + + constexpr Int kMaxBindings = 36; + const auto violation = [](const String& body) { + return FindShaderStorageBindingViolation("#version 430 core\n" + body + "void main() {}\n", kMaxBindings); + }; + + // The boundary itself: max - 1 is the last legal point, max is one past it. + EXPECT_FALSE(violation("layout(binding = 35) buffer Buffer { int x; };\n").has_value()); + EXPECT_TRUE(violation("layout(binding = 36) buffer Buffer { int x; };\n").has_value()); + + // An instance array takes CONSECUTIVE points, so what has to fit is base + count - 1. + EXPECT_FALSE(violation("layout(binding = 32) buffer Buffer { int x; } g_array[4];\n").has_value()); + EXPECT_TRUE(violation("layout(binding = 34) buffer Buffer { int x; } g_array[4];\n").has_value()); + + // Qualifiers and a second layout list may sit between the binding and the keyword. + EXPECT_TRUE(violation("layout(std430) layout(binding = 36) coherent restrict buffer B { int x; };\n") + .has_value()); + + // Things the scanner must NOT judge: a uniform block (a different ceiling), a storage block + // with no explicit binding, the bare default-qualifier form, and an instance array whose size + // is not a literal. + EXPECT_FALSE(violation("layout(binding = 40) uniform Block { int x; };\n" + "layout(binding = 0) buffer Buffer { int y; };\n") + .has_value()); + EXPECT_FALSE(violation("buffer Buffer { int x; };\nconst int binding = 40;\n").has_value()); + EXPECT_FALSE(violation("layout(binding = 1) buffer;\nbuffer Buffer { int x; };\n").has_value()); + EXPECT_FALSE(violation("const int kCount = 4;\nlayout(binding = 34) buffer B { int x; } g[kCount];\n") + .has_value()); + + // A backend that advertises no binding points has no ceiling to enforce. + EXPECT_FALSE(FindShaderStorageBindingViolation("layout(binding = 36) buffer B { int x; };\n", 0).has_value()); +} + +// KHR-GL43.explicit_uniform_location.uniform-loc-nondecimal: GLSL integer literals are C-style, so +// layout(location = 0xA) is 10 and layout(location = 010) is OCTAL 8. The extractor used to accept +// a base-10 digit run and nothing else: the hex spelling failed the test entirely and the +// declaration silently lost its explicit location, while the octal one was read as decimal 10. +// The identical defect sat on every array dimension and on layout(binding = N). +TEST_F(ProgramUtilTest, ExtractExplicitUniformLocationsReadsNonDecimalIntegerLiterals) { + using namespace MG_Util::ShaderTranspiler; + + const String source = R"(#version 430 core +layout(location = 0xA) uniform vec4 hexLower; +layout(location = 0X1f) uniform vec4 hexUpper; +layout(location = 010) uniform vec4 octal; +layout(location = 3u) uniform vec4 unsignedSuffix; +layout(location = 0x2) uniform float hexArray[0x3]; +layout(location = 1.0) uniform vec4 notAnInteger; +layout(location = 7f) uniform vec4 unknownSuffix; +void main() {} +)"; + + const UnorderedMap 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 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 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 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 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); +} diff --git a/MobileGL/MG_Test/Query/QueryTest.cpp b/MobileGL/MG_Test/Query/QueryTest.cpp index 34430c8e..ed7d93e9 100644 --- a/MobileGL/MG_Test/Query/QueryTest.cpp +++ b/MobileGL/MG_Test/Query/QueryTest.cpp @@ -140,6 +140,29 @@ namespace { void StubEndXfbPrimitivesQuery(MG_Backend::BackendQueryHandle) { ++g_stubXfbEndCount; } + // Stub backend occlusion queries. The host has no ES context, and BeginQuery refuses the + // occlusion targets outright when the backend advertises no hook - so a conditional-render + // test cannot get a legal predicate object without these. g_stubResultNs is the sample count + // the "driver" reports, which is the whole input to the predicate. + MG_Backend::BackendQueryHandle StubBeginOcclusionQuery() { + return reinterpret_cast(static_cast(0x54)); + } + + void StubEndOcclusionQuery(MG_Backend::BackendQueryHandle) {} + + void InstallStubBackendOcclusionQueries() { + auto& backendGL = MG_Backend::gBackendFunctionsTable.GL; + backendGL.BeginOcclusionQuery = StubBeginOcclusionQuery; + backendGL.EndOcclusionQuery = StubEndOcclusionQuery; + backendGL.IsQueryResultAvailable = StubIsQueryResultAvailable; + backendGL.GetQueryResult64 = StubGetQueryResult64; + backendGL.DeleteBackendQuery = StubDeleteBackendQuery; + g_stubDeleteCount = 0; + g_stubResultAvailable = true; + g_stubResultObtainable = true; + g_stubResultNs = 0; + } + void InstallStubBackendXfbQueries() { auto& backendGL = MG_Backend::gBackendFunctionsTable.GL; backendGL.BeginXfbPrimitivesQuery = StubBeginXfbPrimitivesQuery; @@ -677,6 +700,108 @@ TEST_F(QueryTest, PrimitivesGeneratedKeepsTheBackendResultUnderTheCpuPreference) // unified truthy rule (set, non-empty, not "0", case-insensitive not "false"). // Running the binary under MOBILEGL_DISABLE_TIMERQUERY=1 therefore exercises // the real end-to-end path rather than the struct field alone. +// KHR-GL43.compute_shader.conditional-dispatching and the conditional_render family. +// glBeginConditionalRender/glEndConditionalRender were bare stubs: every command inside a +// conditional block executed whatever the query said, so the block that should have been +// discarded ran and doubled the atomic counter the case reads back. +TEST_F(QueryTest, ConditionalRenderResolvesItsPredicateFromTheOcclusionQuery) { + ScopedBackendFunctionsOverride backendGuard; + InstallStubBackendOcclusionQueries(); + + GLuint ids[2] = {0, 0}; + MG_Impl::GLImpl::GenQueries(2, ids); + ASSERT_NE(ids[0], 0u); + ASSERT_NE(ids[1], 0u); + + // One span that saw samples and one that saw none, which is exactly the pair the + // conformance case builds out of a passing and a failing depth test. + g_stubResultNs = 1; + MG_Impl::GLImpl::BeginQuery(GL_ANY_SAMPLES_PASSED, ids[0]); + MG_Impl::GLImpl::EndQuery(GL_ANY_SAMPLES_PASSED); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + GLuint passedResult = 0xFFFFFFFFu; + MG_Impl::GLImpl::GetQueryObjectuiv(ids[0], GL_QUERY_RESULT, &passedResult); + ASSERT_EQ(passedResult, 1u); + + g_stubResultNs = 0; + MG_Impl::GLImpl::BeginQuery(GL_ANY_SAMPLES_PASSED, ids[1]); + MG_Impl::GLImpl::EndQuery(GL_ANY_SAMPLES_PASSED); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + // A block on the query that passed executes. + MG_Impl::GLImpl::BeginConditionalRender(ids[0], GL_QUERY_WAIT); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + EXPECT_TRUE(MG_State::pGLContext->IsConditionalRenderActive()); + EXPECT_FALSE(MG_State::pGLContext->ConditionalRenderDiscardsCommands()); + MG_Impl::GLImpl::EndConditionalRender(); + EXPECT_FALSE(MG_State::pGLContext->IsConditionalRenderActive()); + EXPECT_FALSE(MG_State::pGLContext->ConditionalRenderDiscardsCommands()); + + // A block on the query that did not passes nothing through. + MG_Impl::GLImpl::BeginConditionalRender(ids[1], GL_QUERY_WAIT); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + EXPECT_TRUE(MG_State::pGLContext->ConditionalRenderDiscardsCommands()); + MG_Impl::GLImpl::EndConditionalRender(); + + // ...and the _INVERTED modes swap both verdicts. + MG_Impl::GLImpl::BeginConditionalRender(ids[0], GL_QUERY_WAIT_INVERTED); + EXPECT_TRUE(MG_State::pGLContext->ConditionalRenderDiscardsCommands()); + MG_Impl::GLImpl::EndConditionalRender(); + MG_Impl::GLImpl::BeginConditionalRender(ids[1], GL_QUERY_BY_REGION_NO_WAIT_INVERTED); + EXPECT_FALSE(MG_State::pGLContext->ConditionalRenderDiscardsCommands()); + MG_Impl::GLImpl::EndConditionalRender(); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::DeleteQueries(2, ids); +} + +TEST_F(QueryTest, ConditionalRenderRejectsTheErrorsTheSpecNames) { + ScopedBackendFunctionsOverride backendGuard; + InstallStubBackendOcclusionQueries(); + + GLuint ids[2] = {0, 0}; + MG_Impl::GLImpl::GenQueries(2, ids); + g_stubResultNs = 1; + MG_Impl::GLImpl::BeginQuery(GL_ANY_SAMPLES_PASSED, ids[0]); + MG_Impl::GLImpl::EndQuery(GL_ANY_SAMPLES_PASSED); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + // GL 4.6 core 10.9, one rule at a time. + MG_Impl::GLImpl::BeginConditionalRender(ids[0], GL_TIME_ELAPSED); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_ENUM); + EXPECT_FALSE(MG_State::pGLContext->IsConditionalRenderActive()); + + // A generated NAME is not yet a query object. + MG_Impl::GLImpl::BeginConditionalRender(ids[1], GL_QUERY_WAIT); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_VALUE); + MG_Impl::GLImpl::BeginConditionalRender(0, GL_QUERY_WAIT); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_VALUE); + + // A query that is not an occlusion query cannot drive one. + GLuint timerId = 0; + MG_Impl::GLImpl::GenQueries(1, &timerId); + MG_Impl::GLImpl::BeginQuery(GL_TIME_ELAPSED, timerId); + MG_Impl::GLImpl::EndQuery(GL_TIME_ELAPSED); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + MG_Impl::GLImpl::BeginConditionalRender(timerId, GL_QUERY_WAIT); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION); + + // End without a block, and a nested Begin. + MG_Impl::GLImpl::EndConditionalRender(); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION); + MG_Impl::GLImpl::BeginConditionalRender(ids[0], GL_QUERY_WAIT); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + MG_Impl::GLImpl::BeginConditionalRender(ids[0], GL_QUERY_WAIT); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION); + // The rejected nested Begin must not have disturbed the open block. + EXPECT_EQ(MG_State::pGLContext->GetConditionalRenderQuery(), ids[0]); + MG_Impl::GLImpl::EndConditionalRender(); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::DeleteQueries(2, ids); + MG_Impl::GLImpl::DeleteQueries(1, &timerId); +} + TEST_F(QueryTest, DisableTimerQueryFeatureMatchesEnvironment) { const char* raw = std::getenv("MOBILEGL_DISABLE_TIMERQUERY"); Bool expected = false; diff --git a/MobileGL/MG_Test/SanityTest.cpp b/MobileGL/MG_Test/SanityTest.cpp index b42840ca..a2193fce 100644 --- a/MobileGL/MG_Test/SanityTest.cpp +++ b/MobileGL/MG_Test/SanityTest.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -929,6 +930,161 @@ void main() { MG_Backend::pActiveBackendObject.reset(); } +// KHR-GL43.shader_atomic_counters.basic-glsl-built-in, .basic-buffer-bind and .basic-api-get. +// The atomic-counter limits used to live in two unreconciled tables - glslang compiled every +// shader against ONE binding while glGetIntegerv advertised thirty-six - and three of the enums +// had no case in the getter at all, so the query raised INVALID_ENUM and left the caller reading +// whatever was in its own stack slot. +TEST(GetterSanity, AtomicCounterQueriesMatchShaderCompilerLimits) { + using namespace MobileGL; + namespace Transpiler = MG_Util::ShaderTranspiler; + + auto previousContext = Move(MG_State::pGLContext); + auto previousBackend = Move(MG_Backend::pActiveBackendObject); + MG_State::pGLContext = MakeUnique(); + MG_Backend::pActiveBackendObject = MakeUnique(MG_Backend::DynamicBackendParameters{}); + + GLint reported = -1; + MG_Impl::GLImpl::GetIntegerv(GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS, &reported); + EXPECT_EQ(reported, static_cast(Transpiler::MAX_ATOMIC_COUNTER_BUFFER_BINDINGS)); + MG_Impl::GLImpl::GetIntegerv(GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE, &reported); + EXPECT_EQ(reported, static_cast(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(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(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(Transpiler::MAX_ATOMIC_COUNTER_BUFFER_BINDINGS); + MG_Impl::GLImpl::BindBufferBase(GL_ATOMIC_COUNTER_BUFFER, pastLastBinding, buffer); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), static_cast(GL_INVALID_VALUE)); + MG_Impl::GLImpl::GetIntegeri_v(GL_ATOMIC_COUNTER_BUFFER_BINDING, pastLastBinding, &reported); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), static_cast(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_Backend::pActiveBackendObject = MakeUnique(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(env->maxComputeWorkGroupSize[index]), size[index]) << "index " << index; + EXPECT_EQ(static_cast(env->maxComputeWorkGroupCount[index]), count[index]) << "index " << index; + } + + // A negative array size is a compile error, so the stage only compiles when EVERY component + // of both built-in constants equals what the query above reported. Two-sided by construction: + // a resource table that is too permissive fails it exactly like one that is too tight. + const String source = R"(#version 430 core +layout(local_size_x = 1) in; +const int mgAgree = (gl_MaxComputeWorkGroupSize == ivec3()" + + std::to_string(size[0]) + ", " + std::to_string(size[1]) + ", " + + std::to_string(size[2]) + R"() && + gl_MaxComputeWorkGroupCount == ivec3()" + + std::to_string(count[0]) + ", " + std::to_string(count[1]) + ", " + + std::to_string(count[2]) + R"()) ? 1 : -1; +int mgProbe[mgAgree]; +void main() { + mgProbe[0] = 0; +} +)"; + auto compiled = MG_Util::ShaderTranspiler::ShaderCompiler::CompileShader({ + .shaderType = GL_COMPUTE_SHADER, + .sourceStr = source, + .env = env.get(), + }); + EXPECT_TRUE(compiled) << (compiled ? "" : compiled.error().log); + + // The z ceiling is also what glslang checks a declared local_size_z against, so it has to + // reject one invocation past the advertised limit and accept the limit itself. + const String atLimit = "#version 430 core\nlayout(local_size_z = " + std::to_string(size[2]) + + ") in;\nvoid main() {}\n"; + const String pastLimit = "#version 430 core\nlayout(local_size_z = " + std::to_string(size[2] + 1) + + ") in;\nvoid main() {}\n"; + EXPECT_TRUE(MG_Util::ShaderTranspiler::ShaderCompiler::CompileShader({ + .shaderType = GL_COMPUTE_SHADER, + .sourceStr = atLimit, + .env = env.get(), + })); + EXPECT_FALSE(MG_Util::ShaderTranspiler::ShaderCompiler::CompileShader({ + .shaderType = GL_COMPUTE_SHADER, + .sourceStr = pastLimit, + .env = env.get(), + })); + + MG_Backend::pActiveBackendObject = Move(previousBackend); + MG_State::pGLContext = Move(previousContext); +} + TEST(GetterSanity, ReportsKhrSubgroupDynamicParameters) { using namespace MobileGL; diff --git a/MobileGL/MG_Test/Texture/TextureTest.cpp b/MobileGL/MG_Test/Texture/TextureTest.cpp index 953a8929..77b08374 100644 --- a/MobileGL/MG_Test/Texture/TextureTest.cpp +++ b/MobileGL/MG_Test/Texture/TextureTest.cpp @@ -4194,24 +4194,30 @@ TEST_F(TextureTest, CopyTexImage1DReportsUnsupportedInsteadOfTerminating) { ExpectSingleGlError(GL_INVALID_OPERATION); } -TEST_F(TextureTest, GetTexLevelParameterOnBufferStorageReportsErrorInsteadOfTerminating) { - // TextureStorageType is {Mipmap, Buffer} and the level queries only answer out of a mipmap - // chain, so every glGetTexLevelParameter* on a GL_TEXTURE_BUFFER texture reached a - // THROW_UNIMPL_EXCEPTION default: label and killed the process. +TEST_F(TextureTest, GetTexLevelParameterAnswersBufferStorageGeometry) { + // TextureStorageType is {Mipmap, Buffer} and the level queries used to answer only out of a + // mipmap chain, so every glGetTexLevelParameter* on a GL_TEXTURE_BUFFER texture reached a + // THROW_UNIMPL_EXCEPTION default: label and killed the process. It now answers out of the + // attached buffer range instead (GL 4.6 core 8.9): a buffer texture is one-dimensional, and + // with no buffer attached it addresses no texels at all. GLuint texture = 0; MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_BUFFER, 1, &texture); MG_Impl::GLImpl::BindTexture(GL_TEXTURE_BUFFER, texture); MG_Impl::GLImpl::TexBuffer(GL_TEXTURE_BUFFER, GL_R8, 0); DrainPendingGlErrors(); - for (const GLenum pname : {GL_TEXTURE_WIDTH, GL_TEXTURE_HEIGHT, GL_TEXTURE_DEPTH}) { + const std::pair expectations[] = { + {GL_TEXTURE_WIDTH, 0}, {GL_TEXTURE_HEIGHT, 1}, {GL_TEXTURE_DEPTH, 1}}; + for (const auto& [pname, expected] : expectations) { GLint intParam = 0x20202020; MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_BUFFER, 0, pname, &intParam); - ExpectSingleGlError(GL_INVALID_OPERATION); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), static_cast(GL_NO_ERROR)); + EXPECT_EQ(intParam, expected) << "pname " << pname; GLfloat floatParam = 12345.0f; MG_Impl::GLImpl::GetTexLevelParameterfv(GL_TEXTURE_BUFFER, 0, pname, &floatParam); - ExpectSingleGlError(GL_INVALID_OPERATION); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), static_cast(GL_NO_ERROR)); + EXPECT_EQ(floatParam, static_cast(expected)) << "pname " << pname; } } @@ -4809,3 +4815,41 @@ TEST_F(TextureTest, CopyImageSubDataDoesNotApplyMipmapCompletenessToMultisampleT 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(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(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(); +} diff --git a/MobileGL/MG_Util/SelfTest/DriverPost.cpp b/MobileGL/MG_Util/SelfTest/DriverPost.cpp index 4cd7c41d..460060e1 100644 --- a/MobileGL/MG_Util/SelfTest/DriverPost.cpp +++ b/MobileGL/MG_Util/SelfTest/DriverPost.cpp @@ -521,9 +521,11 @@ namespace MobileGL::MG_Util::SelfTest { builder.Warn("64-bit vertex attributes", "not supported (ES has no GL_DOUBLE vertex format, and after the fp64 demotion " "above there is no 64-bit shader input left to feed either); " - "glVertexAttribLFormat / glVertexArrayAttribLFormat report " - "GL_INVALID_OPERATION - feed the attribute with glVertexAttribPointer(GL_FLOAT), " - "which a demoted dvec input reads correctly"); + "glVertexAttribLFormat / glVertexArrayAttribLFormat succeed and their state is " + "queryable, but an ENABLED 64-bit array is DROPPED at draw and the attribute " + "reads its generic current value - feed the attribute with " + "glVertexAttribPointer(GL_FLOAT) instead, which a demoted dvec input reads " + "correctly"); if (glesFuncs.glPatchParameteri != nullptr) { builder.Pass("Tessellation patch parameters", "glPatchParameteri present (GL_PATCH_VERTICES reaches the driver)"); @@ -2336,8 +2338,10 @@ namespace MobileGL::MG_Util::SelfTest { builder.Warn("64-bit vertex attributes", "not supported; there is no 64-bit shader input left to feed after the fp64 demotion " "above, and no VK_FORMAT_R64*_SFLOAT vertex fetch to feed it with on most devices " - "anyway. glVertexAttribLFormat reports GL_INVALID_OPERATION - feed the attribute with " - "glVertexAttribPointer(GL_FLOAT), which a demoted dvec input reads correctly"); + "anyway. glVertexAttribLFormat succeeds and its state is queryable, but an ENABLED " + "64-bit array is DROPPED at pipeline build and the attribute reads its generic " + "current value - feed the attribute with glVertexAttribPointer(GL_FLOAT) instead, " + "which a demoted dvec input reads correctly"); Bool shaderDrawParameters = false; if (vkGetPhysicalDeviceFeatures2Fn != nullptr && properties.apiVersion >= VK_API_VERSION_1_1) { diff --git a/MobileGL/MG_Util/ShaderTranspiler/CompileEnv.cpp b/MobileGL/MG_Util/ShaderTranspiler/CompileEnv.cpp index cb8e3a46..0ef054ae 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/CompileEnv.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/CompileEnv.cpp @@ -28,6 +28,9 @@ namespace MobileGL::MG_Util::ShaderTranspiler { HashValue(state, env.maxComputeWorkGroupSize[0]); HashValue(state, env.maxComputeWorkGroupSize[1]); HashValue(state, env.maxComputeWorkGroupSize[2]); + HashValue(state, env.maxComputeWorkGroupCount[0]); + HashValue(state, env.maxComputeWorkGroupCount[1]); + HashValue(state, env.maxComputeWorkGroupCount[2]); HashValue(state, env.maxComputeWorkGroupInvocations); HashValue(state, env.backend); // DynamicBackendParameters is a plain aggregate of scalars; hashing its object @@ -51,19 +54,23 @@ namespace MobileGL::MG_Util::ShaderTranspiler { env->advertisedExtensions = activeBackend->GetRendererInfo().RendererGLInfo.Extensions; } - // GL_MAX_COMPUTE_WORK_GROUP_SIZE. This is a REAL driver call on DirectGLES; it must - // happen here, on the context thread, and exactly once per context. The frontend - // minimum is the floor, matching what GL_Getter reports. - // TODO: Share these exposed compute limit helpers with GL_Getter.cpp instead of duplicating the frontend minima. - constexpr Uint kFrontendMinComputeWorkGroupSizes[3] = {1024, 1024, 64}; + // GL_MAX_COMPUTE_WORK_GROUP_SIZE / _COUNT. These are REAL driver calls on DirectGLES; they + // must happen here, on the context thread, and exactly once per context. The frontend + // minimum is the floor, matching what GL_Getter reports - both sides now floor at the + // shared MIN_COMPUTE_WORK_GROUP_* constants rather than at their own copy of them. for (Uint index = 0; index < 3; ++index) { - Int backendValue = 0; + Int backendSize = 0; + Int backendCount = 0; if (MG_Backend::gBackendFunctionsTable.GL.GetIntegeri_v) { MG_Backend::gBackendFunctionsTable.GL.GetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_SIZE, index, - &backendValue); + &backendSize); + MG_Backend::gBackendFunctionsTable.GL.GetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_COUNT, index, + &backendCount); } env->maxComputeWorkGroupSize[index] = - std::max(static_cast(std::max(backendValue, 0)), kFrontendMinComputeWorkGroupSizes[index]); + std::max(static_cast(std::max(backendSize, 0)), MIN_COMPUTE_WORK_GROUP_SIZE[index]); + env->maxComputeWorkGroupCount[index] = + std::max(static_cast(std::max(backendCount, 0)), MIN_COMPUTE_WORK_GROUP_COUNT[index]); } constexpr Uint64 kFrontendMaxComputeWorkGroupInvocations = 1024; diff --git a/MobileGL/MG_Util/ShaderTranspiler/CompileEnv.h b/MobileGL/MG_Util/ShaderTranspiler/CompileEnv.h index 2eadd65b..8daf3ce8 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/CompileEnv.h +++ b/MobileGL/MG_Util/ShaderTranspiler/CompileEnv.h @@ -12,6 +12,18 @@ #include namespace MobileGL::MG_Util::ShaderTranspiler { + // GL_MAX_COMPUTE_WORK_GROUP_COUNT / _SIZE core minimums (GL 4.6 core table 23.45), in ONE + // place because three separate readers have to agree on them: CaptureCompileEnv (which floors + // the backend's answer at them), GL_Getter (which answers the same query the same way) and + // BuildTBuiltInResource (whose gl_MaxComputeWorkGroup* constants a shader compares against + // the query - KHR-GL43.compute_shader.max does exactly that). They used to be three copies, + // and the z one disagreed: glslang compiled against 1024 while the context advertised 64. + inline constexpr Uint MIN_COMPUTE_WORK_GROUP_COUNT[3] = {65535, 65535, 65535}; + inline constexpr Uint MIN_COMPUTE_WORK_GROUP_SIZE[3] = {1024, 1024, 64}; + // GL_MAX_COMPUTE_UNIFORM_COMPONENTS, the same invariant with no backend input: the number + // glGetIntegerv answers and the number gl_MaxComputeUniformComponents expands to. + inline constexpr Int MAX_COMPUTE_UNIFORM_COMPONENTS = 1024; + // everything outside (stage, source) this reads - advertised extensions and backend limits - // so the transformation is a pure function of its three arguments and can run on a worker // thread. @@ -34,7 +46,13 @@ namespace MobileGL::MG_Util::ShaderTranspiler { struct CompileEnv { // --- compute limits: the ONLY former real-driver read in the pipeline --- // GL_MAX_COMPUTE_WORK_GROUP_SIZE, already max()'d with the frontend minimum. - Uint maxComputeWorkGroupSize[3] = {1024, 1024, 64}; + Uint maxComputeWorkGroupSize[3] = {MIN_COMPUTE_WORK_GROUP_SIZE[0], MIN_COMPUTE_WORK_GROUP_SIZE[1], + MIN_COMPUTE_WORK_GROUP_SIZE[2]}; + // GL_MAX_COMPUTE_WORK_GROUP_COUNT, likewise. Carried for the same reason the size is: + // gl_MaxComputeWorkGroupCount expands from it at parse time, so the compile pipeline + // needs the number the context advertises without reaching back to the live backend. + Uint maxComputeWorkGroupCount[3] = {MIN_COMPUTE_WORK_GROUP_COUNT[0], MIN_COMPUTE_WORK_GROUP_COUNT[1], + MIN_COMPUTE_WORK_GROUP_COUNT[2]}; // GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS, likewise. Uint64 maxComputeWorkGroupInvocations = 1024; diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp index 7cf8722a..d96e36de 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp @@ -83,18 +83,11 @@ namespace MobileGL { Resources.minProgramTexelOffset = -8; Resources.maxProgramTexelOffset = 7; Resources.maxClipDistances = 8; - Resources.maxComputeWorkGroupCountX = 65535; - Resources.maxComputeWorkGroupCountY = 65535; - Resources.maxComputeWorkGroupCountZ = 65535; - Resources.maxComputeWorkGroupSizeX = 1024; - Resources.maxComputeWorkGroupSizeY = 1024; - // TODO: Drive glslang compute resource limits from the active backend instead of this permissive cap. - Resources.maxComputeWorkGroupSizeZ = 1024; - Resources.maxComputeUniformComponents = 1024; + Resources.maxComputeUniformComponents = MAX_COMPUTE_UNIFORM_COMPONENTS; Resources.maxComputeTextureImageUnits = 16; Resources.maxComputeImageUniforms = 8; - Resources.maxComputeAtomicCounters = 8; - Resources.maxComputeAtomicCounterBuffers = 1; + Resources.maxComputeAtomicCounters = MAX_ATOMIC_COUNTERS_PER_STAGE; + Resources.maxComputeAtomicCounterBuffers = MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE; Resources.maxVaryingComponents = 60; Resources.maxVertexOutputComponents = 64; Resources.maxGeometryInputComponents = 64; @@ -132,16 +125,22 @@ namespace MobileGL { Resources.maxTessControlAtomicCounters = 0; Resources.maxTessEvaluationAtomicCounters = 0; Resources.maxGeometryAtomicCounters = 0; - Resources.maxFragmentAtomicCounters = 8; - Resources.maxCombinedAtomicCounters = 8; - Resources.maxAtomicCounterBindings = 1; + Resources.maxFragmentAtomicCounters = MAX_ATOMIC_COUNTERS_PER_STAGE; + Resources.maxCombinedAtomicCounters = MAX_ATOMIC_COUNTERS_PER_STAGE; + // Every atomic-counter limit below is the one glGetIntegerv answers; the shared + // constants in Types.h are what keeps the two sides from drifting apart again. + // gl_MaxAtomicCounterBindings and gl_MaxAtomicCounterBufferSize expand from these + // (Initialize.cpp), and the binding count is also the ceiling glslang checks a + // `layout(binding = N) uniform atomic_uint` against - it was 1, so every counter + // outside binding 0 failed to compile. + Resources.maxAtomicCounterBindings = MAX_ATOMIC_COUNTER_BUFFER_BINDINGS; Resources.maxVertexAtomicCounterBuffers = 0; Resources.maxTessControlAtomicCounterBuffers = 0; Resources.maxTessEvaluationAtomicCounterBuffers = 0; Resources.maxGeometryAtomicCounterBuffers = 0; - Resources.maxFragmentAtomicCounterBuffers = 1; - Resources.maxCombinedAtomicCounterBuffers = 1; - Resources.maxAtomicCounterBufferSize = 16384; + Resources.maxFragmentAtomicCounterBuffers = MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE; + Resources.maxCombinedAtomicCounterBuffers = MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE; + Resources.maxAtomicCounterBufferSize = MAX_ATOMIC_COUNTER_BUFFER_SIZE; Resources.maxTransformFeedbackBuffers = 4; Resources.maxTransformFeedbackInterleavedComponents = 64; Resources.maxCullDistances = 8; @@ -173,6 +172,25 @@ namespace MobileGL { Resources.maxFragmentImageUniforms = dynamicParameters.MaxFragmentImageUniforms; Resources.maxComputeImageUniforms = dynamicParameters.MaxComputeImageUniforms; Resources.maxCombinedImageUniforms = dynamicParameters.MaxCombinedImageUniforms; + Resources.maxComputeTextureImageUnits = dynamicParameters.MaxComputeTextureImageUnits; + + // The compute work-group limits are the env's, not the backend parameters': they + // are the only ones that come from a REAL indexed driver query, which + // CaptureCompileEnv already issued once on the GL thread and floored at the core + // minimum exactly as GL_Getter does. Reading the same snapshot here is what makes + // gl_MaxComputeWorkGroupSize and glGetIntegeri_v agree by construction + // (KHR-GL43.compute_shader.max compares them); the z component was 1024 here + // against the 64 every ES driver reports. A null env is the standalone/test entry + // point, which has no context to have queried one - the core minimums stand, which + // is what a default-constructed CompileEnv carries anyway. + const Uint* maxWorkGroupSize = env ? env->maxComputeWorkGroupSize : MIN_COMPUTE_WORK_GROUP_SIZE; + const Uint* maxWorkGroupCount = env ? env->maxComputeWorkGroupCount : MIN_COMPUTE_WORK_GROUP_COUNT; + Resources.maxComputeWorkGroupSizeX = static_cast(maxWorkGroupSize[0]); + Resources.maxComputeWorkGroupSizeY = static_cast(maxWorkGroupSize[1]); + Resources.maxComputeWorkGroupSizeZ = static_cast(maxWorkGroupSize[2]); + Resources.maxComputeWorkGroupCountX = static_cast(maxWorkGroupCount[0]); + Resources.maxComputeWorkGroupCountY = static_cast(maxWorkGroupCount[1]); + Resources.maxComputeWorkGroupCountZ = static_cast(maxWorkGroupCount[2]); Resources.limits.nonInductiveForLoops = true; Resources.limits.whileLoops = true; diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp index c0353917..1b15eb1e 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp @@ -10,6 +10,7 @@ #include #include +#include #include #include #include @@ -818,6 +819,116 @@ namespace { ReplaceIdentifier(source, "GL_ARB_gpu_shader_int64", "MG_DISABLED_GL_ARB_gpu_shader_int64"); } + // GLSL 4.30 4.1.9 allows an interface-block member array to be left unsized when it is NOT the + // last member; it is then implicitly sized by the largest constant index the shader uses. + // glslang implements the SIZING - adoptImplicitArraySizes, at link - but computes the block's + // member OFFSETS at DECLARATION time (fixBlockUniformOffsets), where the array is still + // unsized and so contributes zero bytes. Every member after it is therefore laid out on top of + // it: `vec4 a[]; vec4 b;` puts BOTH at offset 0, and a shader reading `b` gets `a[0]` + // (KHR-GL43.shader_storage_buffer_object.basic-syntax iteration 6, whose degenerate triangle + // rasterizes nothing at all). + // + // The source level is the only place the two can be reconciled, because the offset pass runs + // before a single statement has been parsed. Deliberately narrow: it fires only on a `buffer` + // block (no other block kind may hold an unsized member at all), only on a member that is not + // the last one, and only when every subscript of that member's name in the source is a decimal + // literal. Anything outside that shape is left exactly as it was - and the shape itself has no + // correct behaviour today, so the rewrite cannot take a working case away. + void SizeNonFinalUnsizedBufferBlockMembers(MobileGL::String& source) { + // Both tokens must be present for the shape to exist, and "[]" is absent from essentially + // every real shader source, so this is the whole cost for them. + if (source.find("[]") == MobileGL::String::npos || source.find("buffer") == MobileGL::String::npos) { + return; + } + + const auto isDecimalInteger = [](const String& text) { + return !text.empty() && std::all_of(text.begin(), text.end(), [](char ch) { + return ch >= '0' && ch <= '9'; + }); + }; + + const Vector 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 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 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> members; // [begin, end) of each member, ';' excluded + SizeT memberBegin = bodyBegin; + for (SizeT m = bodyBegin; m < bodyEnd; ++m) { + if (tokens[m].text != ";") continue; + members.emplace_back(memberBegin, m); + memberBegin = m + 1; + } + + // The LAST member is deliberately untouched: an unsized array there is a run-time + // sized array, which is both legal and correctly laid out already. + for (SizeT index = 0; index + 1 < members.size(); ++index) { + const SizeT begin = members[index].first; + const SizeT end = members[index].second; + if (end < begin + 3) continue; + if (tokens[end - 1].text != "]" || tokens[end - 2].text != "[") continue; + if (!IsIdentifierToken(tokens[end - 3])) continue; + // A multi-declarator member would need one size per declarator; out of scope. + bool multipleDeclarators = false; + for (SizeT t = begin; t < end; ++t) { + if (tokens[t].text == ",") multipleDeclarators = true; + } + if (multipleDeclarators) continue; + const auto known = subscriptExtent.find(tokens[end - 3].text); + if (known == subscriptExtent.end() || known->second <= 0) continue; + edits.push_back({tokens[end - 1].begin, std::to_string(known->second)}); + } + i = bodyEnd; + } + + for (auto it = edits.rbegin(); it != edits.rend(); ++it) { + source.insert(it->pos, it->text); + } + } + // Rewrite the `packed` / `shared` block-packing qualifiers inside layout(...) declarations to // `std140`. Desktop GL leaves the memory layout of such blocks to the implementation and the // app must query member offsets; MobileGL's SPIR-V pipeline always lays uniform blocks out as @@ -962,6 +1073,11 @@ namespace MobileGL { FilterUnsupportedGpuShaderInt64(env, source); CoerceUniformBlockPackingToStd140(source); + // After the packing coercion: that one rewrites `packed`/`shared` in place and so + // cannot move an offset this pass depends on, and reading the block declarations + // once both qualifiers are normalized keeps the two passes' notions of a block + // declaration identical. + SizeNonFinalUnsizedBufferBlockMembers(source); RenameBuiltinShadowingFunctions(source); @@ -1113,10 +1229,65 @@ namespace MobileGL { return false; } - bool IsDecimalIntegerToken(const String& text) { - if (text.empty()) return false; - return std::all_of(text.begin(), text.end(), - [](char ch) { return ch >= '0' && ch <= '9'; }); + // One GLSL integer literal, spelled the C way: "0x"/"0X" is hexadecimal, a leading + // '0' is OCTAL, everything else decimal, and a single trailing 'u'/'U' is legal. + // strtoll with base 0 already implements exactly that detection, so the only work + // here is deciding what the tail is allowed to be. + // + // Never guesses, which is the discipline every caller depends on: a float ("1.0"), + // an unknown suffix ("3f"), an out-of-range run and a negative value all return + // false, and the caller skips the declaration rather than recording a wrong number. + bool ParseGlslIntegerLiteral(const String& text, long long& out) { + if (text.empty() || text.front() < '0' || text.front() > '9') return false; + errno = 0; + char* tail = nullptr; + const long long value = std::strtoll(text.c_str(), &tail, 0); + if (tail == text.c_str() || errno == ERANGE || value < 0) return false; + const String suffix = text.substr(static_cast(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& dimensions, + long long baseLocation, + MobileGL::UnorderedMap& 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( + std::min(baseLocation + element * innerSpan, + static_cast(INT_MAX / 2)))); + } } // Parses one brace-free depth-0 statement [begin, end) and records its @@ -1129,6 +1300,7 @@ namespace MobileGL { MobileGL::UnorderedMap& locations) { using MobileGL::Int; long long location = -1; + long long literal = 0; bool sawUniform = false; SizeT declaratorBegin = end; @@ -1144,9 +1316,9 @@ namespace MobileGL { } else if (layoutToken == ")") { --parenDepth; } else if (parenDepth == 1 && layoutToken == "location" && j + 2 < end && - tokens[j + 1].text == "=" && IsDecimalIntegerToken(tokens[j + 2].text)) { - location = std::min(std::strtoll(tokens[j + 2].text.c_str(), nullptr, 10), - static_cast(INT_MAX / 2)); + tokens[j + 1].text == "=" && + ParseGlslIntegerLiteral(tokens[j + 2].text, literal)) { + location = std::min(literal, static_cast(INT_MAX / 2)); j += 2; } ++j; @@ -1175,21 +1347,25 @@ namespace MobileGL { const String& name = tokens[k].text; ++k; long long span = 1; + Vector dimensions; while (k < end && tokens[k].text == "[") { ++k; long long dimension = 1; - if (k < end && IsDecimalIntegerToken(tokens[k].text)) { - dimension = std::strtoll(tokens[k].text.c_str(), nullptr, 10); + if (k < end && ParseGlslIntegerLiteral(tokens[k].text, literal)) { + dimension = literal; ++k; } if (k >= end || tokens[k].text != "]") return; // sized by expression; bail out ++k; - span *= std::max(1ll, std::min(dimension, static_cast(INT_MAX / 2))); + dimensions.push_back( + std::max(1ll, std::min(dimension, static_cast(INT_MAX / 2)))); + span *= dimensions.back(); } // Keep the first sighting: a duplicate can only come from alternative // preprocessor branches declaring the same name. locations.emplace(name, static_cast(std::min( nextLocation, static_cast(INT_MAX / 2)))); + RecordArrayOfArraysElementLocations(name, dimensions, nextLocation, locations); nextLocation += span; if (k >= end) break; if (tokens[k].text == "=") { // skip an initializer up to the declarator comma @@ -1225,6 +1401,7 @@ namespace MobileGL { MobileGL::UnorderedMap& bindings) { using MobileGL::Int; long long binding = -1; + long long literal = 0; bool sawUniform = false; SizeT declaratorBegin = end; @@ -1240,9 +1417,9 @@ namespace MobileGL { } else if (layoutToken == ")") { --parenDepth; } else if (parenDepth == 1 && layoutToken == "binding" && j + 2 < end && - tokens[j + 1].text == "=" && IsDecimalIntegerToken(tokens[j + 2].text)) { - binding = std::min(std::strtoll(tokens[j + 2].text.c_str(), nullptr, 10), - static_cast(INT_MAX / 2)); + tokens[j + 1].text == "=" && + ParseGlslIntegerLiteral(tokens[j + 2].text, literal)) { + binding = std::min(literal, static_cast(INT_MAX / 2)); j += 2; } ++j; @@ -1275,7 +1452,7 @@ namespace MobileGL { ++k; while (k < end && tokens[k].text == "[") { ++k; - if (k < end && IsDecimalIntegerToken(tokens[k].text)) ++k; + if (k < end && ParseGlslIntegerLiteral(tokens[k].text, literal)) ++k; if (k >= end || tokens[k].text != "]") return; // sized by expression; bail out ++k; } @@ -1332,6 +1509,100 @@ namespace MobileGL { return bindings; } + namespace { + // Binding points a storage-block declaration starting at `bufferPos` occupies. + // One for a scalar instance (and for the "layout(...) buffer;" default-qualifier + // form, which declares no block at all); the element count for an instance array, + // whose elements take base, base+1, ... (GLSL 4.30 4.4.5). -1 means "the grammar + // here is outside this scanner's narrow subset", i.e. do not judge this one. + long long StorageBlockBindingPointCount(const Vector& 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(1, elementCount); + } + return -1; // sized by an expression, or unsized + } + } // namespace + + std::optional 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 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(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(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 ExtractExplicitUniformLocations(const String& source) { UnorderedMap locations; // Fast path: without the qualifier keyword there is nothing to extract. diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.h b/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.h index d6e41c71..0a55cc78 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.h +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.h @@ -64,6 +64,17 @@ namespace MobileGL { // mapIO can capture them, so they are recovered lexically (same narrow // grammar discipline as ExtractExplicitUniformLocations). UnorderedMap 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 FindShaderStorageBindingViolation(const String& source, Int maxBindings); } // namespace ShaderTranspiler } // namespace MG_Util } // namespace MobileGL diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpvcSession.cpp b/MobileGL/MG_Util/ShaderTranspiler/SpvcSession.cpp index 27ae0380..0cbc0cba 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/SpvcSession.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/SpvcSession.cpp @@ -326,6 +326,55 @@ namespace MobileGL { SPVC_CHK_RETURN } + // "gl_AtomicCounterBlock_5" -> 5, -1 for anything that is not one of those blocks. + // The suffix is the GL atomic-counter binding the application declared, and after + // the relaxed lowering it is the only place that number still exists. + static Int AtomicCounterBlockBinding(const char* blockName) { + if (blockName == nullptr) return -1; + const SizeT prefixLength = std::strlen(ATOMIC_COUNTER_BLOCK_PREFIX); + const String name = blockName; + if (name.length() <= prefixLength + 1) return -1; + if (name.compare(0, prefixLength, ATOMIC_COUNTER_BLOCK_PREFIX) != 0) return -1; + if (name[prefixLength] != '_') return -1; + Int binding = 0; + for (SizeT i = prefixLength + 1; i < name.length(); ++i) { + if (name[i] < '0' || name[i] > '9') return -1; + binding = binding * 10 + (name[i] - '0'); + if (binding > 0x0FFFFFFF) return -1; + } + return binding; + } + + spvc_result SpvcSession::SetAtomicCounterBlockBindings(Int topBinding, Vector& 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(esslBinding)); + outGlBindings.push_back(glBinding); + } + SPVC_CHK_RETURN + } + spvc_result SpvcSession::Compile(const char** result) { if (!(usage & SessionUsageBit::Transpile)) return SPVC_ERROR_INVALID_ARGUMENT; SPVC_CHK_INIT diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpvcSession.h b/MobileGL/MG_Util/ShaderTranspiler/SpvcSession.h index d01434d2..f602514a 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/SpvcSession.h +++ b/MobileGL/MG_Util/ShaderTranspiler/SpvcSession.h @@ -105,6 +105,21 @@ namespace MobileGL { // arrayed block's elements are separate GL resources spelled "B[0]", "B[1]"). // Entries with a negative value mean "never rebound" and are skipped. spvc_result SetShaderStorageBlockBinding(const UnorderedMap& 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_, 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& outGlBindings); spvc_result Compile(const char** result); const SpvcMetadata& GetMetadata() const; const char* GetLastErrorString() const; diff --git a/MobileGL/MG_Util/ShaderTranspiler/Types.h b/MobileGL/MG_Util/ShaderTranspiler/Types.h index 4f0322d9..3eda5f31 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/Types.h +++ b/MobileGL/MG_Util/ShaderTranspiler/Types.h @@ -14,6 +14,35 @@ namespace MobileGL { namespace MG_Util { namespace ShaderTranspiler { inline const char* GLOBAL_UBO_NAME = "MGL_GLOBAL_UBO"; + // glslang's Vulkan-relaxed parse rewrites every atomic_uint into a member of a + // synthesized storage block named "_" + // (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 {};